feat(display): add Rich dashboard rendering (fixes #3)

Render repos in a Rich table with [F]ork/[A]rchive/[M]irror indicators,
color-coded issue counts, relative release dates, and a milestones section.
Handles empty repo lists gracefully.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
sylvain
2026-03-10 18:54:25 +01:00
parent b52bc72ce8
commit 8fbdfcafd4
2 changed files with 344 additions and 0 deletions

View File

@@ -0,0 +1,128 @@
"""Formatage et affichage Rich du dashboard Gitea."""
from __future__ import annotations
from datetime import datetime, timezone
from rich.console import Console
from rich.table import Table
from gitea_dashboard.collector import RepoData
def _format_repo_name(repo: RepoData) -> str:
"""Formate le nom du repo avec les indicateurs visuels."""
indicators = []
if repo.is_fork:
indicators.append("[F]")
if repo.is_archived:
indicators.append("[A]")
if repo.is_mirror:
indicators.append("[M]")
if indicators:
return f"{repo.name} {' '.join(indicators)}"
return repo.name
def _format_relative_date(iso_date: str) -> str:
"""Convertit une date ISO en date relative lisible."""
try:
dt = datetime.fromisoformat(iso_date.replace("Z", "+00:00"))
except (ValueError, AttributeError):
return ""
now = datetime.now(timezone.utc)
delta = now - dt
days = delta.days
if days < 0:
return "dans le futur"
if days == 0:
return "aujourd'hui"
if days == 1:
return "il y a 1j"
if days < 30:
return f"il y a {days}j"
months = days // 30
if months < 12:
return f"il y a {months}m"
years = days // 365
return f"il y a {years}a"
def _format_release(release: dict | None) -> str:
"""Formate la release pour l'affichage."""
if release is None:
return "\u2014"
tag = release.get("tag_name", "")
published = release.get("published_at", "")
if published:
relative = _format_relative_date(published)
if relative:
return f"{tag} ({relative})"
return tag
def render_dashboard(repos: list[RepoData], console: Console | None = None) -> None:
"""Affiche le dashboard complet dans le terminal.
- Tableau principal : nom repo, indicateurs (fork/archive/mirror),
issues ouvertes, derniere release (tag + date relative)
- Section milestones : par repo ayant des milestones,
nom, progression (closed/total), date echeance
Le parametre console permet l'injection pour les tests.
"""
if console is None:
console = Console()
if not repos:
console.print("Aucun repo trouve.")
return
# Tableau principal
table = Table(title="Gitea Dashboard")
table.add_column("Repo", style="bold")
table.add_column("Issues", justify="right")
table.add_column("Release")
for repo in repos:
name = _format_repo_name(repo)
issues_str = str(repo.open_issues)
issues_style = "red" if repo.open_issues > 0 else "green"
release_str = _format_release(repo.latest_release)
table.add_row(name, f"[{issues_style}]{issues_str}[/{issues_style}]", release_str)
console.print(table)
# Section milestones — uniquement si au moins un repo en a
repos_with_milestones = [r for r in repos if r.milestones]
if repos_with_milestones:
console.print()
console.print("[bold]Milestones[/bold]")
for repo in repos_with_milestones:
for ms in repo.milestones:
title = ms["title"]
closed = ms["closed_issues"]
total = ms["open_issues"] + ms["closed_issues"]
pct = round(closed / total * 100) if total > 0 else 0
line = f" {repo.name} / {title} : {closed}/{total} ({pct}%)"
due_on = ms.get("due_on")
if due_on:
# Extraire juste la date (YYYY-MM-DD)
try:
dt = datetime.fromisoformat(due_on.replace("Z", "+00:00"))
line += f" \u2014 echeance {dt.strftime('%Y-%m-%d')}"
except (ValueError, AttributeError):
pass
console.print(line)