22 KiB
Plan de version v1.2.0 — gitea-dashboard
Objectif
Enrichir le dashboard avec l'export JSON, l'affichage de l'activite recente (dernier commit), le tri configurable des repos, la coloration des milestones selon l'echeance, et corriger la gestion des timeouts API.
Track
Minor : 6 -> 7 -> 8 -> 9 -> 10 -> 11 -> (12) -> 13
Budget de scope
| Critere | Valeur |
|---|---|
| Max fichiers par phase | 5 |
| Total fichiers estimes | 8 (4 modules modifies + 1 nouveau + 3 fichiers de tests modifies/crees) |
Inclus
- Export JSON du dashboard (
--format json) - Date du dernier commit par repo (nouvelle colonne)
- Gestion robuste des timeouts API (retry + message utilisateur)
- Tri configurable des repos (
--sort name|issues|release|activity) - Coloration des milestones selon la proximite de l'echeance
Exclus
- Parallelisation des appels API (ADR-003, differee)
- Export CSV (hors scope, pas de demande)
- Filtrage par owner/organisation (differe)
- Cache des reponses API (differe)
- Sous-commandes CLI (argparse suffit, ADR-004)
Differe (v1.3+)
- Export CSV
- Cache API local (fichier/SQLite)
- Parallelisation des appels API
- Dashboard interactif (TUI)
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 #6-#10 et ce plan |
| 4 | Research | Pas de technologie nouvelle (json est stdlib, API commit connue) |
| 5 | Roadmap | Minor — milestone v1.2.0 deja creee sur Gitea |
| 12 | Deploy | Outil CLI local, pas de deploiement serveur |
Analyse des dependances entre issues
#8 (timeout) -- aucune dependance, fondation
#7 (dernier commit) -- necessite nouveau endpoint client.py
#9 (tri) -- necessite #7 si tri par activite
#10 (coloration milestones) -- aucune dependance
#6 (export JSON) -- necessite toutes les donnees disponibles (#7, #9, #10)
Ordre logique : #8 -> #7 -> #10 -> #9 -> #6
Phase 1 : Robustesse API et donnees d'activite (#8, #7)
Goal : Corriger la gestion des timeouts avec retry automatique, puis ajouter la date du dernier commit par repo.
Issues Gitea : fixes #8, fixes #7
Fichiers
| Action | Fichier | Modifications | Cross-references |
|---|---|---|---|
| Modify | src/gitea_dashboard/client.py |
Ajouter retry sur timeout (max 2 retries avec backoff), ajouter methode get_latest_commit(owner, repo) |
collector.py (consomme les nouvelles donnees) |
| Modify | src/gitea_dashboard/collector.py |
Ajouter champ last_commit_date a RepoData, appeler get_latest_commit() dans collect_all() |
client.py (nouvelle methode), display.py (nouveau champ) |
| Modify | src/gitea_dashboard/display.py |
Ajouter colonne "Dernier commit" au tableau principal | collector.py (champ last_commit_date) |
| Modify | tests/test_client.py |
Tests retry sur timeout, tests get_latest_commit() |
client.py |
| Modify | tests/test_collector.py |
Tests avec last_commit_date dans RepoData |
collector.py |
Interfaces
client.py (modifications)
class GiteaClient:
_MAX_RETRIES = 2
_RETRY_DELAY = 1.0 # secondes
def _get_with_retry(self, url: str, params: dict | None = None) -> requests.Response:
"""GET avec retry automatique sur timeout.
Retente jusqu'a _MAX_RETRIES fois avec backoff lineaire.
Leve requests.Timeout apres epuisement des retries.
"""
def get_latest_commit(self, owner: str, repo: str) -> dict | None:
"""Retourne le dernier commit du repo, ou None si aucun.
Endpoint: GET /api/v1/repos/{owner}/{repo}/commits?limit=1
Retourne le premier element de la liste, ou None si vide.
Structure retournee : {sha, created, commit: {message, ...}}
"""
Pourquoi un retry dans client.py et non dans cli.py : le retry est une preoccupation du transport HTTP, pas de l'orchestration CLI. Le client est le bon endroit car il connait le contexte de chaque requete. Le CLI garde sa responsabilite de gestion d'erreur finale (message utilisateur).
Pourquoi _get_with_retry en methode interne : elle sera utilisee par _get_paginated et les appels directs (get_latest_release, get_latest_commit). Cela centralise la logique de retry sans dupliquer.
Pourquoi pas urllib3.Retry : requests utilise urllib3 en interne, mais configurer le retry via HTTPAdapter est plus complexe et moins lisible. Un retry manuel simple (boucle + sleep) est plus explicite et testable pour ce cas d'usage.
collector.py (modifications)
@dataclass
class RepoData:
name: str
full_name: str
description: str
open_issues: int
is_fork: bool
is_archived: bool
is_mirror: bool
latest_release: dict | None
milestones: list[dict]
last_commit_date: str | None # ISO 8601, ex: "2026-03-10T14:30:00Z"
Pourquoi str | None et non datetime : coherent avec latest_release qui stocke les dates en format brut. La conversion en date relative est la responsabilite de display.py (qui a deja _format_relative_date).
Comportement attendu
-
Timeout avec retry :
# Premier appel timeout, deuxieme reussit -> transparent pour l'utilisateur # Les 3 tentatives echouent -> message d'erreur existant (cli.py l.82-85) -
Dernier commit affiche dans le tableau :
Gitea Dashboard +-----------------+--------+------------------+----------------+ | Repo | Issues | Release | Dernier commit | +-----------------+--------+------------------+----------------+ | mon-projet | 3 | v1.0.0 (il y a 5j) | il y a 2j | | autre-repo [F] | 0 | --- | il y a 30j | +-----------------+--------+------------------+----------------+ -
Repo sans commit :
| repo-vide | 0 | --- | --- |
Tests
test_client.py (ajouts)
test_get_with_retry_success_first_attempt: pas de timeout, reponse directetest_get_with_retry_success_after_timeout: premier appel timeout, deuxieme OKtest_get_with_retry_all_timeouts: 3 timeouts -> leverequests.Timeouttest_get_latest_commit_returns_first: retourne le premier commit de la listetest_get_latest_commit_empty_repo: retourne None si pas de commitstest_get_latest_commit_404: retourne None si repo non trouve
test_collector.py (ajouts)
test_repo_data_has_last_commit_date: le champ est present dans RepoDatatest_collect_all_calls_get_latest_commit: verifie queget_latest_commitest appele pour chaque repo
Livrable
Les appels API sont robustes face aux timeouts (retry transparent). Le tableau affiche la date du dernier commit. Tous les tests existants et nouveaux passent.
Phase 2 : Coloration et tri (#10, #9)
Goal : Ajouter la coloration des milestones selon l'echeance et le tri configurable des repos.
Issues Gitea : fixes #10, fixes #9
Fichiers
| Action | Fichier | Modifications | Cross-references |
|---|---|---|---|
| Modify | src/gitea_dashboard/display.py |
Coloration milestones (rouge si echeance depassee, jaune si < 7j, vert sinon). Logique de tri des repos avant affichage | collector.py (champ last_commit_date pour tri par activite) |
| Modify | src/gitea_dashboard/cli.py |
Ajouter option --sort (choices: name, issues, release, activity) |
display.py (passe le critere de tri) |
| Modify | tests/test_display.py |
Tests coloration milestones, tests tri | display.py |
| Modify | tests/test_cli.py |
Tests parsing --sort |
cli.py |
Interfaces
cli.py (modifications)
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
"""Parse les arguments CLI.
Options existantes : --repo, --exclude
Nouvelle option :
--sort / -s : critere de tri (name, issues, release, activity)
defaut: name
"""
display.py (modifications)
def _colorize_milestone_due(due_on: str | None) -> str:
"""Retourne le style Rich selon la proximite de l'echeance.
- Rouge : echeance depassee
- Jaune : echeance dans les 7 prochains jours
- Vert : echeance dans plus de 7 jours
- Pas de style : pas d'echeance definie
"""
def _sort_repos(repos: list[RepoData], sort_key: str) -> list[RepoData]:
"""Trie la liste des repos selon le critere donne.
Args:
repos: Liste des repos a trier.
sort_key: Critere de tri parmi :
- "name" : alphabetique par nom (defaut)
- "issues" : par nombre d'issues ouvertes (decroissant)
- "release" : par date de derniere release (plus recent d'abord)
- "activity" : par date du dernier commit (plus recent d'abord)
"""
def render_dashboard(
repos: list[RepoData],
console: Console | None = None,
sort_key: str = "name",
) -> None:
"""Affiche le dashboard. Nouveau parametre sort_key pour le tri."""
Pourquoi le tri est dans display.py et non collector.py : le tri est une preoccupation d'affichage, pas de collecte. Le collecteur fournit les donnees brutes, l'affichage decide de l'ordre de presentation. Cela respecte la separation des responsabilites (ADR-002).
Pourquoi la coloration est calculee dans display.py : la couleur est purement visuelle. collector.py ne doit pas connaitre les seuils de couleur (7 jours, etc.). Le display est le bon endroit car il possede deja _format_relative_date.
Comportement attendu
-
Coloration des milestones :
Milestones mon-projet / v1.3.0 : 2/5 (40%) -- echeance 2026-03-15 [jaune: dans 3j] autre / v2.0.0 : 0/3 (0%) -- echeance 2026-03-01 [rouge: depassee] lib / v0.5.0 : 8/10 (80%) -- echeance 2026-04-01 [vert: dans 20j] -
Tri par issues :
$ gitea-dashboard --sort issues # Repos ordonnes par nombre d'issues decroissant -
Tri par activite :
$ gitea-dashboard --sort activity # Repos ordonnes par date du dernier commit (plus recent d'abord) -
Sans
--sort, le tri par defaut est par nom (retrocompatible avec v1.1.0 si l'API retournait dans un ordre aleatoire, desormais garanti alphabetique).
Tests
test_display.py (ajouts)
test_colorize_milestone_overdue: echeance passee -> style rougetest_colorize_milestone_soon: echeance dans 3 jours -> style jaunetest_colorize_milestone_ok: echeance dans 15 jours -> style verttest_colorize_milestone_no_due: pas d'echeance -> pas de styletest_sort_repos_by_name: tri alphabetiquetest_sort_repos_by_issues: tri decroissant par issuestest_sort_repos_by_release: tri par date release (repos sans release en dernier)test_sort_repos_by_activity: tri par date commit (repos sans commit en dernier)
test_cli.py (ajouts)
test_parse_args_sort_default: sans--sort->Namespace(sort="name")test_parse_args_sort_issues:--sort issues->Namespace(sort="issues")test_parse_args_sort_invalid:--sort invalid-> erreur argparse
Livrable
Les milestones sont colorees selon l'echeance. Les repos sont triables par --sort. La retrocompatibilite est preservee (defaut : tri par nom). Tous les tests passent.
Phase 3 : Export JSON (#6)
Goal : Permettre l'export du dashboard complet en format JSON sur stdout.
Issues Gitea : fixes #6
Fichiers
| Action | Fichier | Modifications | Cross-references |
|---|---|---|---|
| Create | src/gitea_dashboard/exporter.py |
Nouveau module : serialisation des RepoData en dict/JSON | collector.py (consomme RepoData) |
| Modify | src/gitea_dashboard/cli.py |
Ajouter option --format (choices: table, json), router vers exporter ou display |
exporter.py (nouveau), display.py (existant) |
| Create | tests/test_exporter.py |
Tests du module exporter | exporter.py |
| Modify | tests/test_cli.py |
Tests parsing --format, tests integration export JSON |
cli.py |
Interfaces
exporter.py (nouveau module)
"""Export des donnees du dashboard en formats structures."""
from __future__ import annotations
import json
from gitea_dashboard.collector import RepoData
def repos_to_dicts(repos: list[RepoData]) -> list[dict]:
"""Convertit une liste de RepoData en liste de dicts serialisables.
Chaque dict contient toutes les donnees du RepoData,
pret pour json.dumps().
"""
def export_json(repos: list[RepoData], indent: int = 2) -> str:
"""Exporte les repos en JSON formate.
Returns:
Chaine JSON indentee, prete pour stdout ou ecriture fichier.
"""
Pourquoi un nouveau module exporter.py plutot que dans display.py : l'export JSON n'est pas de l'affichage Rich. C'est une serialisation de donnees. Melanger les deux violerait la separation des responsabilites. De plus, exporter.py pourra accueillir d'autres formats (CSV, YAML) dans le futur sans polluer display.py.
Pourquoi cela ne viole pas ADR-002 (4 modules max) : ADR-002 definissait un maximum pour la v1.0.0. Le projet grandit avec de nouvelles fonctionnalites. 5 modules restent raisonnables (chacun a une responsabilite unique). Un ADR-006 est ajoute pour documenter cette evolution.
cli.py (modifications)
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
"""Parse les arguments CLI.
Options existantes : --repo, --exclude, --sort
Nouvelle option :
--format / -f : format de sortie (table, json)
defaut: table
"""
Comportement attendu
-
Export JSON :
$ gitea-dashboard --format json [ { "name": "mon-projet", "full_name": "admin/mon-projet", "description": "...", "open_issues": 3, "is_fork": false, "is_archived": false, "is_mirror": false, "latest_release": {"tag_name": "v1.0.0", "published_at": "..."}, "milestones": [...], "last_commit_date": "2026-03-10T14:30:00Z" } ] -
Le JSON est ecrit sur stdout, les erreurs sur stderr (Console(stderr=True) deja en place).
-
Les options
--repo,--exclude,--sortsont compatibles avec--format json:$ gitea-dashboard --repo dashboard --sort issues --format json # Export JSON filtre et trie -
Format table par defaut (retrocompatible) :
$ gitea-dashboard # Comportement identique a v1.1.0 (tableau Rich)
Tests
test_exporter.py (nouveau)
test_repos_to_dicts_basic: conversion RepoData -> dicttest_repos_to_dicts_empty: liste vide -> liste videtest_repos_to_dicts_preserves_all_fields: tous les champs sont presentstest_export_json_valid: le resultat est du JSON valide (json.loads ne leve pas)test_export_json_indent: le JSON est indente par defaut
test_cli.py (ajouts)
test_parse_args_format_default: sans--format->Namespace(format="table")test_parse_args_format_json:--format json->Namespace(format="json")test_main_format_json_outputs_json: verifie que stdout contient du JSON valide
Livrable
L'option --format json exporte toutes les donnees du dashboard en JSON sur stdout. Compatible avec le filtrage et le tri. Le format table reste le defaut. Tous les tests passent.
Architecture des modules (impact v1.2.0)
gitea-dashboard v1.2.0
=====================
Terminal Application Gitea API
-------- ----------- ---------
+------------------+
$ gitea-dashboard | cli.py |
--format json | - parse args |
--sort issues | - route format |
| - gere erreurs |
+--------+---------+
|
v
+------------------+
| collector.py |
| - orchestre la |
| collecte | +------------------+
| - agrege en |---->| client.py |
| RepoData | | - retry timeout |-----> GET /api/v1/user/repos
+--------+---------+ | - auth token |-----> GET .../releases/latest
| | - pagination |-----> GET .../milestones
+------+------+ +------------------+-----> GET .../commits?limit=1
| |
v v
+------------+ +-------------+
| display.py | | exporter.py |
| - tableau | | - JSON |
<------------| - tri | | - stdout |----------> stdout (JSON)
Output Rich | - couleurs | +-------------+
(tableaux) +------------+
| Module | Impact | Detail |
|---|---|---|
cli.py |
Modifie | Options --sort, --format, routage vers display ou exporter |
client.py |
Modifie | Retry sur timeout, nouvelle methode get_latest_commit() |
collector.py |
Modifie | Nouveau champ last_commit_date dans RepoData |
display.py |
Modifie | Colonne "Dernier commit", tri, coloration milestones |
exporter.py |
Nouveau | Serialisation JSON des RepoData |
Decisions architecturales
ADR-006 : Ajout du module exporter.py (v1.2.0)
Contexte : L'export JSON est une nouvelle responsabilite. L'ajouter a display.py melangerait serialisation structuree et formatage Rich. ADR-002 limitait a 4 modules pour v1.0.0.
Decision : Creer exporter.py pour la serialisation des donnees (JSON, et futurs formats). Le projet passe a 5 modules.
Consequences :
- Separation claire :
display.py= rendu terminal,exporter.py= serialisation donnees - ADR-002 est relaxe (4 -> 5 modules), pas invalide (le principe "un module = une responsabilite" reste)
- Le module est independant de Rich (pas de dependance supplementaire)
- Extensible pour CSV/YAML sans modifier l'existant
ADR-007 : Retry simple plutot que urllib3.Retry (v1.2.0)
Contexte : Les timeouts API causent un crash. Deux strategies : configurer HTTPAdapter avec urllib3.Retry, ou implementer un retry manuel dans le client.
Decision : Retry manuel (boucle + time.sleep) dans GiteaClient._get_with_retry(). Maximum 2 retries, backoff lineaire (1s, 2s).
Consequences :
- Code explicite et testable (mock de
time.sleep) - Pas de dependance sur l'API interne de urllib3
- Applicable a tous les appels HTTP du client de maniere uniforme
- Limite : pas d'exponential backoff (acceptable pour un outil CLI local)
ADR-008 : Tri dans display.py, pas dans collector.py (v1.2.0)
Contexte : Le tri des repos peut etre place dans le collecteur (donnees ordonnees) ou dans l'affichage (presentation ordonnee).
Decision : Le tri est dans display.py. Le collecteur retourne les donnees dans l'ordre de l'API. L'affichage decide de l'ordre de presentation.
Consequences :
- Le collecteur reste un simple agregateur de donnees (SRP)
- Le tri est teste independamment de la collecte
- L'export JSON peut aussi appliquer le tri (via
_sort_reposreutilisable) - Le critere de tri par defaut ("name") garantit un affichage stable entre les executions
Risques d'audit
| Zone | Risque | Severite estimee |
|---|---|---|
client.py — retry |
time.sleep dans les tests ralentit l'execution si non mocke |
minor |
client.py — retry |
Le retry masque des erreurs reseau persistantes (l'utilisateur attend plus longtemps avant le message d'erreur) | minor |
client.py — get_latest_commit |
L'endpoint /commits?limit=1 peut ne pas exister sur d'anciennes versions de Gitea |
major |
collector.py — N+1 |
Ajout d'un appel API supplementaire par repo (get_latest_commit) aggrave le temps de reponse |
minor |
display.py — coloration |
Le calcul de la proximite d'echeance depend de datetime.now(), difficile a tester sans freeze |
minor |
display.py — tri |
Le tri par "release" sur des repos sans release necessite une valeur sentinelle pour la date | minor |
exporter.py — serialisation |
dataclasses.asdict peut echouer si des champs contiennent des objets non serialisables |
minor |
cli.py — retrocompatibilite |
Les nouveaux parametres de render_dashboard() doivent avoir des valeurs par defaut |
major |
Issues Gitea rattachees
| Issue | Titre | Phase |
|---|---|---|
| #8 | Crash sur timeout API sans message clair | Phase 1 |
| #7 | Afficher la date du dernier commit par repo | Phase 1 |
| #10 | Coloration des milestones selon l'echeance | Phase 2 |
| #9 | Tri configurable des repos | Phase 2 |
| #6 | Export du dashboard en JSON | Phase 3 |
Dependances
| Dependance | Type | Version |
|---|---|---|
| Python | Runtime | >= 3.10 |
| argparse | Stdlib | inclus dans Python |
| json | Stdlib | inclus dans Python |
| dataclasses | Stdlib | inclus dans Python |
| time | Stdlib | inclus dans Python |
| requests | Librairie | >= 2.31 (inchange) |
| rich | Librairie | >= 13.0 (inchange) |
| pytest | Dev | >= 7.0 (inchange) |
| ruff | Dev | >= 0.4 (inchange) |
| Instance Gitea | Service externe | 192.168.0.106:3000 |
Criteres de validation par issue
| Issue | Critere de validation |
|---|---|
| #6 | gitea-dashboard --format json produit du JSON valide sur stdout contenant tous les champs de RepoData. Compatible avec --repo, --exclude, --sort. |
| #7 | Le tableau affiche une colonne "Dernier commit" avec la date relative. Les repos sans commit affichent "---". |
| #8 | Un timeout API unique ne fait pas crasher le dashboard (retry transparent). Apres 3 echecs, le message d'erreur est clair et sans token expose. |
| #9 | --sort name|issues|release|activity trie les repos correctement. Le defaut (name) est retrocompatible. |
| #10 | Les milestones dont l'echeance est depassee sont en rouge, celles a moins de 7 jours en jaune, les autres en vert. Sans echeance : pas de couleur. |