Files
gitea-dashboard/docs/plans/v1.2.0-plan.md
2026-03-12 03:50:07 +01:00

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

  1. 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)
    
  2. 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     |
    +-----------------+--------+------------------+----------------+
    
  3. Repo sans commit :

    | repo-vide       |      0 | ---              | ---            |
    

Tests

test_client.py (ajouts)

  • test_get_with_retry_success_first_attempt : pas de timeout, reponse directe
  • test_get_with_retry_success_after_timeout : premier appel timeout, deuxieme OK
  • test_get_with_retry_all_timeouts : 3 timeouts -> leve requests.Timeout
  • test_get_latest_commit_returns_first : retourne le premier commit de la liste
  • test_get_latest_commit_empty_repo : retourne None si pas de commits
  • test_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 RepoData
  • test_collect_all_calls_get_latest_commit : verifie que get_latest_commit est 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

  1. 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]
    
  2. Tri par issues :

    $ gitea-dashboard --sort issues
    # Repos ordonnes par nombre d'issues decroissant
    
  3. Tri par activite :

    $ gitea-dashboard --sort activity
    # Repos ordonnes par date du dernier commit (plus recent d'abord)
    
  4. 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 rouge
  • test_colorize_milestone_soon : echeance dans 3 jours -> style jaune
  • test_colorize_milestone_ok : echeance dans 15 jours -> style vert
  • test_colorize_milestone_no_due : pas d'echeance -> pas de style
  • test_sort_repos_by_name : tri alphabetique
  • test_sort_repos_by_issues : tri decroissant par issues
  • test_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

  1. 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"
      }
    ]
    
  2. Le JSON est ecrit sur stdout, les erreurs sur stderr (Console(stderr=True) deja en place).

  3. Les options --repo, --exclude, --sort sont compatibles avec --format json :

    $ gitea-dashboard --repo dashboard --sort issues --format json
    # Export JSON filtre et trie
    
  4. 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 -> dict
  • test_repos_to_dicts_empty : liste vide -> liste vide
  • test_repos_to_dicts_preserves_all_fields : tous les champs sont presents
  • test_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_repos reutilisable)
  • 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.pyget_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.