Compare commits

..

20 Commits

Author SHA1 Message Date
sylvain
22590d7250 docs(analyse): workflow analysis v1.0.0 — complete breakdown
13 steps, 11 agents, 19 MCP calls, chronology, metrics, recommendations.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 19:42:13 +01:00
sylvain
5c8e833d8b chore(workflow): complete step 13 (retrospective), v1.0.0 done
Metrics, analysis, MEMORY.md written. Milestone v1.0.0 closed on Gitea.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 19:31:45 +01:00
sylvain
cf88ba0ef5 chore(workflow): complete step 10, start step 11
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 19:29:00 +01:00
sylvain
8b4677a6f7 docs(v1.0.0): update README and create CHANGELOG
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 19:28:25 +01:00
sylvain
6ed666fb66 chore(workflow): complete step 9 (smoke test OK), start step 10
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 19:08:48 +01:00
sylvain
b15ba9eea8 fix(cli): add __main__.py for python -m gitea_dashboard
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 19:08:25 +01:00
sylvain
6fa8990cae chore(workflow): complete step 8 (audit 97/100), start step 9
Reviewer: 81→100, Guardian: 91→97, 5 findings corrected.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 19:03:09 +01:00
sylvain
01f88a0eca fix(audit): correct findings from review round 1
- client: add raise_for_status() in get_latest_release() for non-404 errors (FINDING-001)
- client: add timeout parameter (default 30s) passed to all session.get() calls (FINDING-004/SEC-002)
- cli: replace return with sys.exit(1) in all except blocks (FINDING-002)
- test_cli: remove duplicate test_exits_when_token_missing, assert GITEA_TOKEN in stderr (FINDING-006)
- test_cli: update connection error tests to expect SystemExit(1) after exit code fix
- test_cli: rework token masking test to inject token into exception message (FINDING-007)
- test_client: add test_raises_on_server_error for HTTP 500 path (FINDING-001)
- test_client: add tests for default and custom timeout values (FINDING-004)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 19:00:57 +01:00
sylvain
e05578676f chore(workflow): complete step 7, start step 8
35 tests pass, 4 modules implemented (client, collector, display, cli).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 18:55:22 +01:00
sylvain
4aa648fa8c feat(cli): add main entry point with error handling (fixes #4)
Read GITEA_TOKEN (required) and GITEA_URL (default) from env vars,
orchestrate client/collect/render pipeline, handle connection and
timeout errors gracefully. Never expose token in error messages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 18:54:29 +01:00
sylvain
8fbdfcafd4 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>
2026-03-10 18:54:25 +01:00
sylvain
b52bc72ce8 feat(collector): add RepoData dataclass and collect_all (fixes #2)
- RepoData dataclass with all repo fields
- collect_all enriches each repo with release and milestones
- Computes open_issues = open_issues_count - open_pr_counter
- 6 unit tests with mocked GiteaClient

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 18:51:17 +01:00
sylvain
4d66aea6ed feat(client): add GiteaClient with auth and pagination (fixes #1)
- GiteaClient with requests.Session and token auth header
- _get_paginated for automatic pagination (limit=50)
- get_repos, get_latest_release (None on 404), get_milestones
- 9 unit tests with mocked requests.Session
- Fix setuptools build backend in pyproject.toml

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 18:50:28 +01:00
sylvain
18ce3b953e docs(v1.0.0): plan de version, architecture, ADR-002/003
- Plan en 2 phases dev : client+collecteur puis affichage+CLI
- Architecture 4 modules avec interfaces definies
- ADR-002: 4 modules max, ADR-003: pas de parallelisation en v1

chore(workflow): complete step 6, start step 7

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 18:44:11 +01:00
sylvain
e757c35767 docs(v1.0.0): version plan and ADR
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 18:43:29 +01:00
sylvain
0bd64d64a9 chore(workflow): complete step 5, start step 6
Gitea milestone v1.0.0 + issues #1-#4 + labels created.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 18:39:18 +01:00
sylvain
de56585840 docs(research): API Gitea endpoints, pagination, auth, cas limites
chore(workflow): complete step 4, start step 5

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 18:28:03 +01:00
sylvain
2eec10c61a chore(workflow): complete steps 1-3, start step 4
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 18:22:04 +01:00
sylvain
4e72ddc32f chore: init project structure
Discovery synthesis, docs tree, Python src layout, CLAUDE.md, pyproject.toml.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 18:21:33 +01:00
sylvain
11e5def11c chore(workflow): init v1.0.0 (major-initial)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 18:19:00 +01:00
27 changed files with 2686 additions and 1 deletions

View File

@@ -0,0 +1,90 @@
# Workflow — gitea-dashboard
## Metadata
| Champ | Valeur |
|-------|--------|
| Projet | gitea-dashboard |
| Chemin | /home/sylvain/nas/perso/sylvain/conserver/code/application_temp/gitea-dashboard |
| Date de creation | 2026-03-10 |
| Origine | gitea@192.168.0.106:admin/gitea-dashboard.git |
| Version courante | v1.0.0 |
| Track | major-initial |
| Phase courante | 5 — POST-RELEASE |
| Etape courante | 13 |
| workflow_version | v1.0 |
---
## Phase 1 — FRAMING
| # | Etape | Statut | Date | Agent/Skill | Validation | Notes |
|---|-------|--------|------|-------------|------------|-------|
| 1 | Discovery | done | 2026-03-10 | /forge --discovery | Auto (synthesis.md existe) | step_1: done, deliverable: docs/discovery/synthesis.md |
| 2 | Creation projet | done | 2026-03-10 | forge | Auto (fichiers existent) | step_2: done, files_created: 14 |
| 3 | Specs | done | 2026-03-10 | - | Auto (descriptif.md existe avec contexte, objectifs, perimetre, contraintes) | step_3: done |
| 4 | Recherche | done | 2026-03-10 | researcher | Auto (research.md existe) | step_4: done, topics_researched: 6 (auth, endpoints, pagination, strategie, cas limites, decisions) |
| 5 | Roadmap | done | 2026-03-10 | - | Auto (Gitea milestones + issues created) | step_5: done, milestone: v1.0.0 (id:29), issues: #1-#4, labels: feature/bug/improvement/backlog |
## Phase 2 — DEV (v1.0.0)
| # | Etape | Statut | Date | Agent/Skill | Validation | Notes |
|---|-------|--------|------|-------------|------------|-------|
| 6 | Plan de version | done | 2026-03-10 | architect | Auto (plan avec phases, budget scope, inclusions/exclusions) | step_6: done, plan: docs/plans/v1.0.0-plan.md, phases: 2+1, gitea_milestone: exists (id:29) |
| 7 | Developpement | done | 2026-03-10 | builder | Auto (tests passent) | step_7: done, commits: 4, files_created: 7, files_modified: 1, tests: 35 passed |
| 8 | Audit + corrections | done | 2026-03-10 | reviewer + guardian + fixer | Auto (score 100, floor 50) | step_8: done, audit_initial: 81, audit_final: 97, rounds: 2, corrections: 5, remaining_findings: 1 (minor contextuel) |
## Phase 3 — PRE-RELEASE
| # | Etape | Statut | Date | Agent/Skill | Validation | Notes |
|---|-------|--------|------|-------------|------------|-------|
| 9 | Smoke test | done | 2026-03-10 | tester + checklist | Auto (tests E2E) + Manuel (checklist) | step_9: done, mode: cli, rounds: 1, fix: __main__.py manquant |
| 10 | Documentation | done | 2026-03-10 | documenter | Auto (README.md et CHANGELOG.md a jour) | step_10: done |
## Phase 4 — PUBLICATION
| # | Etape | Statut | Date | Agent/Skill | Validation | Notes |
|---|-------|--------|------|-------------|------------|-------|
| 11 | Release | done | 2026-03-10 | /release | Auto (release creee) | step_11: done, tag: v1.0.0 |
| 12 | Deploy (optionnel) | skipped | 2026-03-10 | script | Auto (health check OK) | Outil CLI local, pas de deploiement serveur |
## Phase 5 — POST-RELEASE
| # | Etape | Statut | Date | Agent/Skill | Validation | Notes |
|---|-------|--------|------|-------------|------------|-------|
| 13 | Retrospective | done | 2026-03-10 | - | Auto (metriques et MEMORY.md ecrits) | step_13: done, metrics_written: true, analysis_written: true, gitea_milestone: closed |
---
## Version interrompue (hotfix)
- Version interrompue :
- Etape :
- Phase dev :
- Branche :
---
## Decisions de workflow
| Date | Action | Raison |
|------|--------|--------|
| 2026-03-10 | init v1.0.0 major-initial | Nouveau projet, workflow complet |
| 2026-03-10 | step 1 done | Discovery synthesis produite via /forge |
| 2026-03-10 | step 2 done | Structure projet creee via /forge |
| 2026-03-10 | step 3 done | descriptif.md complet (contexte, objectifs, perimetre, contraintes) |
| 2026-03-10 | step 4 done | Recherche API Gitea : endpoints, pagination, auth, cas limites |
| 2026-03-10 | step 5 done | Milestone v1.0.0 + 4 issues + 4 labels crees sur Gitea |
| 2026-03-10 | step 6 done | Plan v1.0.0 (2 phases dev + 1 finalisation), architecture, ADR-002/003 |
| 2026-03-10 | step 7 done | 4 commits, 35 tests passent, issues #1-#4 fermees |
| 2026-03-10 | step 8 done | Audit: reviewer 81→100, guardian 91→97, 5 corrections, score final 97 |
| 2026-03-10 | step 9 done | Smoke test CLI reel, 13 repos affiches, fix __main__.py, milestone dupliquee nettoyee |
| 2026-03-10 | step 10 done | README complet, CHANGELOG v1.0.0, version bump pyproject.toml |
| 2026-03-10 | step 11 done | Tag v1.0.0, release Gitea creee, push origin |
| 2026-03-10 | step 12 skipped | CLI local, pas de deploy |
| 2026-03-10 | step 11 done | Tag v1.0.0, release Gitea, push origin |
| 2026-03-10 | step 13 done | Retrospective, metriques, analyse, MEMORY.md, milestone fermee |
## Versions completees
| Version | Date debut | Date fin | Notes |
|---------|-----------|----------|-------|
| v1.0.0 | 2026-03-10 | 2026-03-10 | major-initial, 12/13 steps, audit 97, 37 tests |

15
.gitignore vendored Normal file
View File

@@ -0,0 +1,15 @@
__pycache__/
*.py[cod]
*$py.class
*.egg-info/
dist/
build/
.eggs/
*.egg
.venv/
venv/
.env
.pytest_cache/
.ruff_cache/
.coverage
htmlcov/

19
CHANGELOG.md Normal file
View File

@@ -0,0 +1,19 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/).
## [Unreleased]
## [1.0.0] - 2026-03-10
### Added
- Client API Gitea avec authentification par token et pagination automatique
- Collecteur de données avec dataclass `RepoData`
- Affichage Rich du dashboard avec tableau repos et section milestones
- Point d'entrée CLI `gitea-dashboard` avec configuration par variables d'environnement
- Indicateurs visuels pour les repos forks, archives et miroirs
- Gestion des erreurs réseau (connexion refusée, timeout, erreurs API)
- Masquage du token dans les messages d'erreur

49
CLAUDE.md Normal file
View File

@@ -0,0 +1,49 @@
# gitea-dashboard
Dashboard CLI Python affichant l'etat des repos Gitea (issues, releases, milestones).
## Stack
- **Langage** : Python 3.x
- **Dependances** : requests, rich
- **API** : Gitea REST v1
- **Instance** : http://192.168.0.106:3000
## Principes
1. **Lecture seule** — le dashboard ne modifie jamais de donnees Gitea
2. **Configuration externalisee** — token et URL en variables d'environnement, jamais dans le code
3. **Separation des responsabilites** — client API / formatage / point d'entree distincts
4. **Gestion gracieuse** — un repo sans release ou milestone ne casse pas l'affichage
## Points d'attention
- Ne jamais committer de token ou secret
- Gerer la pagination API Gitea (reponses potentiellement tronquees)
- Tester avec des repos sans release et sans milestone
- L'API Gitea peut varier selon la version — tester sur l'instance reelle
## Commandes
```bash
# Installation
pip install -e .
# Execution
gitea-dashboard
# ou
python -m gitea_dashboard
# Tests
pytest
# Lint
ruff check src/ tests/
```
## Variables d'environnement
| Variable | Description | Defaut |
|----------|-------------|--------|
| `GITEA_URL` | URL de l'instance Gitea | http://192.168.0.106:3000 |
| `GITEA_TOKEN` | Token API Gitea | (requis) |

View File

@@ -1,3 +1,72 @@
# gitea-dashboard
CLI Python dashboard for Gitea repos status (issues, releases, milestones)
Dashboard CLI affichant en une commande l'état de tous les repos d'une instance Gitea : issues ouvertes, dernières releases et progression des milestones.
## Prérequis
- Python >= 3.10
- Accès à une instance Gitea avec un token API
## Installation
```bash
pip install -e .
```
## Configuration
Le dashboard se configure via deux variables d'environnement :
| Variable | Description | Défaut |
|----------|-------------|--------|
| `GITEA_URL` | URL de l'instance Gitea | `http://192.168.0.106:3000` |
| `GITEA_TOKEN` | Token API Gitea (requis) | — |
Pour créer un token : Gitea > Settings > Applications > Generate Token.
Exemple de configuration dans votre shell :
```bash
export GITEA_URL=https://gitea.tsmse.fr
# Définir GITEA_TOKEN avec la valeur obtenue depuis Gitea > Settings > Applications
```
## Usage
```bash
gitea-dashboard
# ou
python -m gitea_dashboard
```
### Exemple de sortie
```
Gitea Dashboard
┏━━━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━┓
┃ Repo ┃ Issues ┃ Release ┃
┡━━━━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━┩
│ mon-projet │ 3 │ v1.2.0 (il y a 2j) │
│ autre-repo │ 0 │ — │
└─────────────────┴────────┴──────────────────────┘
Milestones
mon-projet / v2.0 : 3/5 (60%)
```
## Développement
```bash
# Installer avec les dépendances de développement
pip install -e ".[dev]"
# Lancer les tests
pytest
# Vérifier le style
ruff check src/ tests/
```
## Licence
Usage personnel.

48
docs/README.md Normal file
View File

@@ -0,0 +1,48 @@
<!-- Type: reference (Diataxis). Style: index de navigation, structure de la doc. -->
# Documentation — gitea-dashboard
## Structure
```
docs/
├── project/ # Pilotage projet
│ ├── descriptif.md # Brief initial (etape 3)
│ └── demandes.md # Inbox features (/workflow demande)
├── technical/ # Architecture et decisions
│ ├── ARCHITECTURE.md # Architecture technique (etape 6, architect)
│ ├── decisions.md # ADR (Architecture Decision Records)
│ └── research.md # Resultats de recherche (etape 4, researcher)
├── discovery/ # Livrables discovery (etape 1, majeur uniquement)
│ └── synthesis.md # Synthese d'interview
├── plans/ # Plans de version (etape 6)
│ └── vX.Y.Z-plan.md # Un plan par version (architect)
├── guides/ # Documentation utilisateur (optionnel)
│ └── [sujet].md # deployment.md, api-usage.md, getting-started.md
├── dev/ # Notes de developpement (optionnel)
│ └── [sujet].md # setup.md, conventions.md, troubleshooting.md
└── README.md # Ce fichier
```
## Conventions
- **MAJUSCULES.md** : documents officiels et de pilotage (ARCHITECTURE, CHANGELOG)
- **minuscules.md** : documents de travail et notes (descriptif, demandes, research, decisions)
- **Nommage fichiers** : minuscules, tirets pour separer les mots (deployment.md, api-usage.md)
- Les dossiers `guides/` et `dev/` sont crees quand le besoin apparait
- Chaque document a un emplacement unique — pas de doc ailleurs que dans cette arborescence
- **Roadmap** : Gitea milestones (pas de ROADMAP.md local)
- **Backlog** : Gitea issues avec label `backlog` (pas de BACKLOG.md local)
## Ou mettre ma doc ?
| Type de doc | Ou |
|-------------|-----|
| Guide utilisateur (install, usage, config) | `guides/` |
| Notes dev (setup, conventions, debug) | `dev/` |
| Architecture, ADR, recherche | `technical/` |
| Brief, demandes | `project/` |
| Plan de version | `plans/` |
| Synthese discovery | `discovery/` |
| Roadmap, jalons | Gitea milestones |
| Backlog, idees, dette technique | Gitea issues (label `backlog`) |

View File

@@ -0,0 +1,64 @@
<!-- Type: explanation (Diataxis). Style: discursif, retour d'experience, redige par documenter a l'etape 13. -->
# Analyse workflow — gitea-dashboard v1.0.0
**Projet** : gitea-dashboard
**Version** : v1.0.0
**Track** : major-initial
**Date** : 2026-03-10
**Duree** : 1 session
---
## Metriques
| Metrique | Valeur |
|----------|--------|
| Fichiers source | 11 (5 modules + 6 tests) |
| Lignes de code | 958 |
| Tests | 37 |
| Couverture | non mesuree (pas de coverage configure) |
| Score audit initial | 81/100 (reviewer), 91/100 (guardian) |
| Score audit final | 100/100 (reviewer), 97/100 (guardian) |
| Rounds audit | 2 |
| Findings corriges | 5 |
| Findings restants | 1 (minor contextuel : pagination sans borne max) |
| Commits | 19 |
| Etapes effectuees | 12/13 |
| Etapes skippees | 12 (deploy — CLI local) |
| Agents utilises | researcher, architect, builder (x2), reviewer (x2), guardian (x2), fixer, documenter |
---
## Ce qui a bien fonctionne
- **Pipeline complet en 1 session** : de la discovery a la release en une seule conversation, workflow fluide
- **Separation des agents efficace** : chaque agent a produit un livrable propre sans chevauchement
- **Audit adversarial productif** : 5 findings reels corriges (raise_for_status, timeout, exit codes, tests), score 81 → 100/97
- **Smoke test revelateur** : a detecte le __main__.py manquant et la milestone dupliquee
- **Integration Gitea MCP** : creation de milestone, issues, labels, release sans quitter le workflow
## Ce qui a mal fonctionne
- **Milestone creee en double** : l'architect a cree une 2e milestone v1.0.0 (id 30) alors qu'il en existait deja une (id 29). Nettoyage manuel necessaire.
- **Token API pour smoke test** : le premier token fourni etait invalide (401), necessite un 2e essai. L'URL par defaut (192.168.0.106) ne correspondait pas a l'URL publique (gitea.tsmse.fr).
- **__main__.py oublie** : le builder n'a pas cree le fichier necessaire pour `python -m gitea_dashboard`. Detecte au smoke test.
## Friction workflow
- **Steps 1-3 rapides mais ceremoniels** : pour un projet simple et bien defini, les etapes discovery/creation/specs pourraient etre fusionnees.
- **Pas de coverage** : pytest-cov n'est pas dans les deps dev, metriques de couverture absentes.
- **Duplication reviewer/guardian** : les 2 agents ont trouve les memes findings (raise_for_status, timeout). La deduplication est manuelle.
## Suggestions d'amelioration
- [projet] Ajouter pytest-cov dans les deps dev et configurer la couverture
- [projet] Mettre a jour l'URL par defaut vers https://gitea.tsmse.fr dans cli.py
- [generique] Le builder devrait verifier la presence de __main__.py quand pyproject.toml definit un entry point CLI
- [generique] Deduplication automatique des findings entre reviewer et guardian avant de les passer au fixer
---
## Contexte projet
Projet Python simple (4 modules, 1 dataclass, API REST). Stack classique sans complexite particuliere. Le workflow major-initial est lourd pour ce type de projet mais a permis de structurer proprement la documentation et les decisions architecturales des le depart.

View File

@@ -0,0 +1,566 @@
# Analyse complete du workflow — gitea-dashboard v1.0.0
**Objectif** : documenter chaque etape du workflow macro major-initial tel qu'il s'est deroule, avec les agents, outils et decisions pris a chaque moment, pour pouvoir analyser et ameliorer le workflow.
**Projet** : gitea-dashboard (dashboard CLI Python pour Gitea)
**Track** : major-initial (1 → 13)
**Date** : 2026-03-10
**Duree totale** : ~1h17 (18h14 → 19h31)
**Resultat** : v1.0.0 released, 37 tests, audit 97/100
---
## Table des matieres
1. [Vue chronologique](#1-vue-chronologique)
2. [Detail par etape](#2-detail-par-etape)
3. [Agents utilises](#3-agents-utilises)
4. [Outils MCP et integrations](#4-outils-mcp-et-integrations)
5. [Metriques finales](#5-metriques-finales)
6. [Analyse critique](#6-analyse-critique)
7. [Recommandations](#7-recommandations)
---
## 1. Vue chronologique
```
18h14 ████ Init + Steps 1-3 (framing) 8 min
18h22 ████████ Step 4 (recherche API) 17 min
18h39 ██ Steps 5-6 (roadmap + plan) 5 min
18h44 █████ Step 7 (developpement 4 modules) 11 min
18h55 ████ Step 8 (audit + corrections) 8 min
19h03 ██ Step 9 (smoke test) 5 min
19h08 ██████████ Step 10 (documentation) 21 min
19h29 █ Steps 11+13 (release + retro) 2 min
19h31 DONE
```
**Observation** : la documentation (step 10, 21 min) a pris plus de temps que le developpement (step 7, 11 min). Le framing (steps 1-3) et la release (steps 11-13) sont les phases les plus rapides.
---
## 2. Detail par etape
### Phase 1 — FRAMING (steps 1-5)
#### Step 1 — Discovery | 18h14-18h19
| | |
|---|---|
| **Skill/Agent** | `/forge` (mode new project, inclut discovery) |
| **Modele** | opus (orchestrateur principal) |
| **Action** | Interview reduite (contexte deja fourni par l'utilisateur), challenge de 2 hypotheses (requests vs MCP, filtre repos) |
| **Outils** | `AskUserQuestion` (1 question sur le filtre repos) |
| **Livrable** | `docs/discovery/synthesis.md` |
| **Decision** | Tout afficher sans filtre en v1 |
**Deroulement** : L'utilisateur avait deja defini clairement le besoin (stack, scope, API). La discovery a ete acceleree : 1 seule question posee au lieu des 5-8 prevues. 2 hypotheses challengees : (1) requests vs client MCP → requests confirme pour autonomie du CLI, (2) filtre par owner → differe.
**Analyse** : Pour un projet simple et bien defini, la discovery formelle est surdimensionnee. Le temps de redaction du synthesis.md ne se justifie pas quand l'utilisateur a deja tout explicite.
---
#### Step 2 — Creation projet | 18h19-18h21
| | |
|---|---|
| **Skill/Agent** | `/forge` (mode new project, suite) |
| **Modele** | opus (orchestrateur principal) |
| **Action** | Generation de la structure complete du projet |
| **Outils** | `Write` (14 fichiers), `Read` (7 templates), `Bash` (mkdir, git) |
| **Livrable** | Structure projet complete |
**Fichiers crees** :
- `CLAUDE.md`, `.gitignore`, `README.md`, `pyproject.toml`
- `src/gitea_dashboard/__init__.py`, `src/gitea_dashboard/cli.py` (squelette)
- `tests/__init__.py`
- `docs/` (7 fichiers : README, synthesis, descriptif, demandes, ARCHITECTURE, decisions, research)
**Incident** : Le hook `secret-scanner` a bloque l'ecriture du README.md car il contenait le mot "token" dans un exemple. Reformulation sans valeur d'exemple → OK.
**Analyse** : La generation depuis des templates est efficace et coherente. Le hook de securite est un bon garde-fou mais genere un faux positif ici.
---
#### Step 3 — Specs | 18h21
| | |
|---|---|
| **Skill/Agent** | Aucun (validation automatique) |
| **Modele** | — |
| **Action** | Verification que `descriptif.md` contient les 4 sections requises |
| **Livrable** | `docs/project/descriptif.md` (deja cree au step 2) |
**Analyse** : Step valide automatiquement car le descriptif avait ete cree complet au step 2. Pas de travail supplementaire. Cette etape sert de checkpoint de qualite.
---
#### Step 4 — Recherche technique | 18h22-18h28
| | |
|---|---|
| **Skill/Agent** | `researcher` |
| **Modele** | opus |
| **Action** | Investigation API Gitea REST v1 |
| **Outils** | `WebFetch` (swagger.json de l'instance), `Read`, `Write` |
| **Livrable** | `docs/technical/research.md` (164 lignes) |
| **Duree agent** | ~4.5 min (274s) |
**Sujets investigues** (6) :
1. Authentification (format header, methodes deprecated)
2. Endpoints necessaires (4 : /user/repos, /releases/latest, /milestones, /issues)
3. Pagination (page/limit, headers X-Total-Count et Link)
4. Strategie d'appels (cout en requetes : ceil(N/50) + 2N)
5. Cas limites (404 releases, tableaux vides, forks, archives, open_issues inclut PRs)
6. Decisions recommandees (endpoint repos, comptage issues, affichage forks)
**Analyse** : Etape tres productive. Le researcher a produit un document exhaustif qui a guide tout le developpement. La decouverte que `open_issues_count` inclut les PRs (avec `open_pr_counter` en champ separe) a evite un bug en production. Le swagger de l'instance reelle a ete utilise comme source primaire → donnees fiables.
---
#### Step 5 — Roadmap | 18h28-18h39
| | |
|---|---|
| **Skill/Agent** | Orchestrateur principal (pas d'agent dedie) |
| **Modele** | opus |
| **Action** | Creation milestone + issues + labels sur Gitea |
| **Outils MCP** | `mcp__gitea__milestone_write`, `mcp__gitea__issue_write` (x4), `mcp__gitea__label_write` (x4), `mcp__gitea__label_read`, `mcp__gitea__issue_write.add_labels` (x4) |
| **Livrable** | Milestone v1.0.0 (id:29) + issues #1-#4 + 4 labels |
**Labels crees** :
| Label | ID | Couleur |
|-------|----|---------|
| feature | 58 | #0075ca |
| bug | 59 | #d73a4a |
| improvement | 60 | #a2eeef |
| backlog | 61 | #e4e669 |
**Issues crees** :
| # | Titre | Phase |
|---|-------|-------|
| #1 | Client API Gitea avec authentification et pagination | Phase 1 |
| #2 | Collecte des donnees : repos, issues, releases, milestones | Phase 1 |
| #3 | Affichage dashboard avec Rich (tableaux, couleurs) | Phase 2 |
| #4 | Point d'entree CLI et configuration | Phase 2 |
**Particularite MCP** : Le `issue_write(method="create")` de Gitea MCP ignore le parametre `labels`. Il faut un appel separe `issue_write(method="add_labels")` apres creation. Le workflow documente cette contrainte.
**Analyse** : L'integration MCP Gitea est fluide pour les operations CRUD simples. 13 appels MCP necessaires (1 milestone + 4 labels + 4 issues + 4 add_labels) pour une etape qui pourrait etre plus compacte.
---
### Phase 2 — DEV (steps 6-8)
#### Step 6 — Plan de version | 18h39-18h44
| | |
|---|---|
| **Skill/Agent** | `architect` |
| **Modele** | opus |
| **Action** | Production du plan detaille avec phases, interfaces, budget de scope |
| **Outils** | `Read` (5 fichiers contexte), `Write` (3 fichiers) |
| **Livrable** | `docs/plans/v1.0.0-plan.md`, `docs/technical/ARCHITECTURE.md`, `docs/technical/decisions.md` (ADR-002, ADR-003) |
| **Duree agent** | ~3.2 min (194s) |
**Decisions architecturales** :
- **ADR-002** : 4 modules max (client / collector / display / cli)
- **ADR-003** : Pas de parallelisation en v1 (sequentiel, < 20 repos acceptable)
**Plan structure** :
- **Phase 1** : client.py + collector.py + 2 fichiers tests (fixes #1, #2)
- **Phase 2** : display.py + cli.py (modif) + 2 fichiers tests (fixes #3, #4)
- **Budget** : 8 fichiers max (4 modules + 4 tests)
**Interfaces definies** : signatures de `GiteaClient`, `RepoData` dataclass, `collect_all()`, `render_dashboard()` — le builder n'a pas eu a decider de l'API.
**Incident** : L'architect a cree une 2e milestone v1.0.0 (id:30) sur Gitea alors qu'il en existait deja une (id:29). Cause : pas de verification d'existence avant creation. Nettoyage fait au step 9.
**Analyse** : Le plan est tres detaille (295 lignes) et inclut meme les risques d'audit anticipes. Les interfaces pre-definies accelerent le travail du builder. La section "risques d'audit" s'est revelee presciente (3 des 4 risques identifies se sont retrouves dans les findings reels).
---
#### Step 7 — Developpement | 18h44-18h55
| | |
|---|---|
| **Skill/Agent** | `builder` (x2, une invocation par phase) |
| **Modele** | opus |
| **Action** | Implementation TDD des 4 modules |
| **Outils** | `Read`, `Write`, `Edit`, `Bash` (pytest, pip install, ruff) |
| **Livrable** | 4 modules + 4 fichiers tests, 35 tests passent |
| **Duree agents** | Phase 1: ~4 min (239s) + Phase 2: ~3 min (181s) = ~7 min |
**Phase 1 — Client + Collecteur** (builder invocation 1) :
- `src/gitea_dashboard/client.py` (81 lignes) : GiteaClient, auth, pagination, get_repos, get_latest_release (None sur 404), get_milestones
- `src/gitea_dashboard/collector.py` (52 lignes) : dataclass RepoData, collect_all()
- `tests/test_client.py` (160 lignes) : 9 tests
- `tests/test_collector.py` (130 lignes) : 6 tests
- 2 commits : `feat(client): ... (fixes #1)` + `feat(collector): ... (fixes #2)`
- **15 tests passent**
**Correction pyproject.toml** : Le builder a detecte et corrige le build-backend (`_legacy:_Backend``build_meta`). Bug present depuis la creation au step 2.
**Phase 2 — Display + CLI** (builder invocation 2) :
- `src/gitea_dashboard/display.py` (128 lignes) : render_dashboard(), tableau Rich, milestones, indicateurs [F]/[A]/[M], dates relatives
- `src/gitea_dashboard/cli.py` (58 lignes) : main(), lecture env vars, pipeline, gestion erreurs, masquage secret
- `tests/test_display.py` (216 lignes) : 12 tests
- `tests/test_cli.py` (127 lignes) : 8 tests
- 2 commits : `feat(display): ... (fixes #3)` + `feat(cli): ... (fixes #4)`
- **35 tests passent** (15 + 20)
**Ratio tests/code** : 633 lignes de tests / 325 lignes de code = 1.95x.
**Analyse** : Le decoupage en 2 invocations du builder (1 par phase du plan) a bien fonctionne. Chaque invocation a produit du code fonctionnel avec tests. Le builder a suivi les interfaces definies par l'architect sans deviation. Temps effectif de codage : 7 min pour 958 lignes — tres rapide grace aux interfaces pre-definies.
---
#### Step 8 — Audit + corrections | 18h55-19h03
| | |
|---|---|
| **Skill/Agent** | `reviewer` (x2), `guardian` (x2), `fixer` (x1) |
| **Modele** | reviewer/guardian: opus, fixer: sonnet |
| **Action** | Audit qualite 5 axes + securite OWASP, corrections, re-audit |
| **Outils** | `Read` (fichiers source + tests), `Edit`, `Bash` (pytest, pip-audit) |
| **Livrable** | 5 findings corriges, score 81 → 97 |
| **Duree agents** | Round 1: ~2.5 min (parallele) + fixer: ~3 min + Round 2: ~1.5 min (parallele) |
**Round 1 — Audit initial** (reviewer + guardian en parallele) :
*Reviewer (81/100)* — 7 findings :
| ID | Severite | Fichier | Probleme |
|----|----------|---------|----------|
| FINDING-001 | major (-10) | client.py:62 | raise_for_status() manquant dans get_latest_release |
| FINDING-002 | minor (-3) | cli.py:42-49 | return au lieu de sys.exit(1) sur erreurs |
| FINDING-003 | minor (-3) | client.py:17 | Session jamais fermee |
| FINDING-004 | minor (-3) | client.py:17 | Pas de timeout (except Timeout = code mort) |
| FINDING-005 | minor | pyproject.toml:7 | Version 0.0.0 vs v1.0.0 |
| FINDING-006 | minor | test_cli.py:55 | Test duplique sans assertion message |
| FINDING-007 | minor | test_cli.py:101 | Test masquage ne teste pas le mecanisme reel |
*Guardian (91/100)* — 3 findings :
| ID | Severite | Categorie | Probleme |
|----|----------|-----------|----------|
| SEC-001 | minor (-3) | OWASP-A02 | HTTP en clair (reseau local) |
| SEC-002 | minor (-3) | OWASP-A07 | Timeout manquant |
| SEC-003 | minor (-3) | OWASP-A07 | raise_for_status manquant |
**Convergence findings** : SEC-002 = FINDING-004, SEC-003 = FINDING-001. Les 2 agents ont trouve les memes problemes independamment.
**Fixer** — 5 corrections appliquees :
| Finding | Correction |
|---------|-----------|
| FINDING-001/SEC-003 | `resp.raise_for_status()` ajoute + test server error |
| FINDING-004/SEC-002 | `timeout: int = 30` dans constructeur + propagation + 2 tests |
| FINDING-002 | `sys.exit(1)` dans les 3 blocs except |
| FINDING-006 | Test duplique supprime, assertion message ajoutee |
| FINDING-007 | Test renforce avec exception contenant la valeur sensible |
**Non corriges (acceptes)** : FINDING-003 (session.close — negligeable pour CLI), FINDING-005 (version — bump au step 10), SEC-001 (HTTP local — reseau prive).
**Round 2 — Re-audit** (reviewer + guardian en parallele) :
- Reviewer : **100/100**, 0 findings, APPROVED
- Guardian : **97/100**, 1 finding minor (boucle pagination sans borne max — contextuel), APPROVED
- Score de reference : min(100, 97) = **97/100**
- Delta potentiel round 3 : <= 0 → boucle adversariale terminee
**Analyse** : L'audit adversarial est la phase la plus productive du workflow en termes de qualite. Le finding major (raise_for_status) aurait cause des erreurs silencieuses en production. Le timeout manquant rendait le catch Timeout inutile. La parallelisation reviewer/guardian est efficace (meme temps que 1 seul agent). Le fixer (sonnet) a corrige 5 findings en 3 min sans erreur.
---
### Phase 3 — PRE-RELEASE (steps 9-10)
#### Step 9 — Smoke test | 19h03-19h08
| | |
|---|---|
| **Skill/Agent** | Orchestrateur principal (test CLI reel) |
| **Modele** | opus |
| **Action** | Execution du CLI contre l'instance Gitea reelle |
| **Outils** | `Bash` (python3 -m gitea_dashboard), `AskUserQuestion`, `Write` (__main__.py), `mcp__gitea__milestone_read`, `mcp__gitea__milestone_write` (delete) |
| **Livrable** | Dashboard affiche avec 13 repos reels |
**Deroulement** :
1. Variable d'environnement `GITEA_TOKEN` non definie → demande a l'utilisateur
2. Premier essai : **401 Unauthorized** (valeur invalide)
3. Test curl : confirme que la valeur est invalide
4. Deuxieme essai : **erreur** `No module named gitea_dashboard.__main__`
5. **Bug decouvert** : `__main__.py` manquant → creation du fichier
6. Re-execution : **succes**, 13 repos affiches
7. **Bug donnees** : milestone v1.0.0 dupliquee (id 29 vide + id 30 avec issues) → suppression id 29
8. Re-execution : dashboard propre
**Checklist validee** :
- [x] 13 repos affiches
- [x] Issues comptees correctement (iot: 28, gitea-dashboard: 4)
- [x] Releases avec dates relatives, "—" si aucune
- [x] Milestones avec progression
- [x] Erreurs gerees
**Analyse** : Le smoke test a decouvert 2 vrais problemes : (1) `__main__.py` manquant — le builder ne l'a pas cree car ce n'etait pas dans les interfaces du plan, et (2) la milestone dupliquee creee par l'architect. Sans smoke test, la v1.0.0 aurait ete inutilisable via `python -m`. C'est l'etape qui justifie le plus son existence dans le workflow.
---
#### Step 10 — Documentation | 19h08-19h29
| | |
|---|---|
| **Skill/Agent** | `documenter` |
| **Modele** | sonnet |
| **Action** | Mise a jour README.md complet + creation CHANGELOG.md + version bump |
| **Outils** | `Read`, `Write`, `Edit` |
| **Livrable** | README.md (72 lignes), CHANGELOG.md (19 lignes), pyproject.toml (version 1.0.0) |
| **Duree agent** | ~1.6 min (95s) |
**README.md** : description, prerequis, installation, configuration (variables d'env avec tableau), usage, exemple de sortie, section developpement.
**CHANGELOG.md** : format Keep a Changelog, 7 entrees "Added" pour v1.0.0.
**Incident** : Le hook secret-scanner a necessite une reformulation de la section configuration (eviter d'ecrire une valeur d'exemple pour la variable sensible).
**Analyse** : 21 min pour la documentation semble long, mais c'est principalement du temps d'attente MCP/hooks. Le documenter (sonnet) a produit le contenu en 95s.
---
### Phase 4 — PUBLICATION (steps 11-12)
#### Step 11 — Release | 19h29-19h31
| | |
|---|---|
| **Skill/Agent** | Orchestrateur principal |
| **Modele** | opus |
| **Action** | Push, tag git, release Gitea |
| **Outils** | `Bash` (git push, git tag, git push tag), `mcp__gitea__create_release` |
| **Livrable** | Tag v1.0.0, release Gitea avec notes |
**Sequence** :
1. `git push origin main` (18 commits)
2. `git tag -a v1.0.0` + `git push origin v1.0.0`
3. `mcp__gitea__create_release` avec body detaille (Added + Details)
**Analyse** : Etape rapide et sans friction. L'integration MCP pour la release Gitea evite de passer par l'interface web.
---
#### Step 12 — Deploy | skipped
Raison : outil CLI local, pas de deploiement serveur.
---
### Phase 5 — POST-RELEASE (step 13)
#### Step 13 — Retrospective | 19h31
| | |
|---|---|
| **Skill/Agent** | Orchestrateur principal |
| **Modele** | opus |
| **Action** | Collecte metriques, analyse, MEMORY.md, fermeture milestone |
| **Outils** | `Bash` (find, wc, pytest, git log), `Write` (analyse + MEMORY.md), `mcp__gitea__milestone_write` (close), `Edit` (workflow-progress.md) |
| **Livrable** | `docs/analyse/gitea-dashboard-v1.0.0-2026-03-10.md`, MEMORY.md |
**Analyse** : La retrospective est principalement un travail de synthese. La fermeture automatique du milestone Gitea via MCP est un bon point d'integration.
---
## 3. Agents utilises
### Tableau recapitulatif
| Agent | Modele | Invocations | Role | Duree totale | Read-only |
|-------|--------|-------------|------|-------------|-----------|
| **researcher** | opus | 1 | Investigation API Gitea | ~4.5 min | oui |
| **architect** | opus | 1 | Plan v1.0.0, architecture, ADR | ~3.2 min | non |
| **builder** | opus | 2 | Implementation phase 1 + phase 2 | ~7 min | non |
| **reviewer** | opus | 2 | Audit qualite round 1 + 2 | ~2 min | oui |
| **guardian** | opus | 2 | Audit securite round 1 + 2 | ~2.5 min | oui |
| **fixer** | sonnet | 1 | Corrections findings | ~3 min | non |
| **documenter** | sonnet | 1 | README + CHANGELOG | ~1.6 min | non |
| **scout** | sonnet | 1 | Exploration pour cette analyse | ~1.3 min | oui |
**Total** : 11 invocations d'agents, ~25 min de temps agent cumule.
### Agents non utilises
| Agent | Raison |
|-------|--------|
| **orchestrator** | < 6 fichiers, pas necessaire |
| **tester** | Smoke test fait manuellement par l'orchestrateur principal |
### Repartition par modele
| Modele | Agents | Invocations | Usage |
|--------|--------|-------------|-------|
| **opus** | researcher, architect, builder, reviewer, guardian | 8 | Taches complexes (recherche, planification, implementation, audit) |
| **sonnet** | fixer, documenter, scout | 3 | Taches plus simples (corrections, docs, exploration) |
| **haiku** | — | 0 | Jamais utilise (interdit par les regles) |
---
## 4. Outils MCP et integrations
### Outils Gitea MCP
| Outil | Appels | Etape | Usage |
|-------|--------|-------|-------|
| `mcp__gitea__milestone_write` (create) | 1 (+1 par architect) | 5 | Creation milestone v1.0.0 |
| `mcp__gitea__milestone_write` (delete) | 1 | 9 | Suppression milestone dupliquee |
| `mcp__gitea__milestone_write` (edit) | 1 | 13 | Fermeture milestone |
| `mcp__gitea__milestone_read` (list) | 1 | 9 | Verification doublons |
| `mcp__gitea__issue_write` (create) | 4 | 5 | Creation issues #1-#4 |
| `mcp__gitea__issue_write` (add_labels) | 4 | 5 | Ajout labels aux issues |
| `mcp__gitea__label_write` (create) | 4 | 5 | Creation labels |
| `mcp__gitea__label_read` (list) | 1 | 5 | Verification labels existants |
| `mcp__gitea__create_release` | 1 | 11 | Creation release v1.0.0 |
| `mcp__gitea__get_me` | 1 | 9 | Verification connectivite |
| **Total** | **19 appels MCP** | | |
### Outils Claude Code
| Outil | Usage principal |
|-------|----------------|
| `Read` | Lecture fichiers (templates, code, config) — utilise massivement |
| `Write` | Creation de nouveaux fichiers (docs, source, tests) |
| `Edit` | Modifications ciblees (workflow-progress.md surtout) |
| `Bash` | Git operations, pytest, pip, curl, find/wc |
| `Glob` | Recherche de fichiers par pattern |
| `AskUserQuestion` | 3 questions posees (filtre repos, smoke test mode, valeur sensible) |
| `Agent` | 11 invocations (voir section 3) |
| `Skill` | 2 invocations (/workflow, /forge) |
| `ToolSearch` | Chargement dynamique des outils MCP et AskUserQuestion |
### Hooks actifs
| Hook | Declenchements | Effet |
|------|---------------|-------|
| `secret-scanner` | 2 | Blocage ecriture README (faux positif), reformulation necessaire |
| `UserPromptSubmit` | Chaque message | Validation OK |
---
## 5. Metriques finales
### Code
| Metrique | Valeur |
|----------|--------|
| Fichiers source Python | 6 (5 modules + __main__.py) |
| Fichiers tests Python | 5 (4 test_*.py + __init__.py) |
| Lignes code source | 325 |
| Lignes tests | 633 |
| Ratio tests/code | 1.95x |
| Tests | 37 |
| Couverture | Non mesuree |
### Workflow
| Metrique | Valeur |
|----------|--------|
| Commits | 20 |
| Etapes effectuees | 12/13 |
| Etapes skippees | 1 (deploy) |
| Agents invoques | 11 |
| Appels MCP Gitea | 19 |
| Questions posees a l'utilisateur | 3 |
| Incidents/bugs | 3 (milestone doublon, __main__.py, valeur invalide) |
### Audit
| Metrique | Round 1 | Round 2 |
|----------|---------|---------|
| Reviewer | 81/100 | 100/100 |
| Guardian | 91/100 | 97/100 |
| Findings totaux | 10 (1 major, 9 minor) | 1 (minor contextuel) |
| Findings corriges | 5 | — |
| Findings acceptes | 3 | 1 |
| Findings differes | 1 (version pyproject) | — |
---
## 6. Analyse critique
### Forces du workflow
**1. Pipeline structure et tracable**
Chaque etape produit un livrable identifie, commite, et logue dans workflow-progress.md. La tracabilite est excellente : on peut suivre chaque decision, chaque agent, chaque commit.
**2. Audit adversarial efficace**
Le round 1 a trouve un vrai bug major (raise_for_status manquant) et un code mort (timeout). Le mecanisme reviewer + guardian en parallele puis fixer est productif. La convergence des findings entre les 2 agents valide la pertinence.
**3. Smoke test revelateur**
A decouvert 2 bugs que ni le builder ni l'audit n'avaient trouves : __main__.py manquant et milestone dupliquee. Justifie pleinement son existence.
**4. Integration Gitea MCP fluide**
Issues, milestones, labels, releases — tout cree depuis le workflow sans quitter le terminal. 19 appels MCP sans erreur technique.
**5. Separation des responsabilites agents**
Chaque agent a un perimetre clair. Le builder n'a pas eu a decider de l'architecture (c'est l'architect), le fixer n'a pas eu a trouver les bugs (c'est le reviewer/guardian).
### Faiblesses du workflow
**1. Ceremonie excessive pour un petit projet**
13 etapes pour 325 lignes de code. Les steps 1-3 (framing) produisent 7 fichiers de documentation pour un projet dont le besoin tenait en 3 phrases. Le ratio documentation/code est disproportionne.
**2. Duplication reviewer/guardian**
Les 2 agents ont trouve les memes findings (raise_for_status, timeout). Pas de deduplication avant le fixer. 2 rapports a lire et comparer manuellement.
**3. L'architect cree des doublons**
La milestone v1.0.0 a ete creee 2 fois (orchestrateur au step 5 + architect au step 6). Pas de verification d'existence avant creation.
**4. __main__.py oublie**
Ni le plan de l'architect ni le builder n'ont prevu `__main__.py`. Le plan definissait un entry point dans pyproject.toml mais pas le support de `python -m`. Decouvert au smoke test.
**5. Pas de couverture de code**
37 tests mais aucune mesure de couverture. `pytest-cov` absent des deps dev. Impossible de savoir si les tests couvrent tout le code.
**6. URL par defaut incorrecte**
Le code utilise `http://192.168.0.106:3000` mais l'instance repond sur `https://gitea.tsmse.fr`. Le smoke test a necessite un override manuel. Non corrige.
### Temps par categorie
| Categorie | Duree | % du total |
|-----------|-------|------------|
| Framing (steps 1-3) | 8 min | 10% |
| Recherche (step 4) | 17 min | 22% |
| Planification (steps 5-6) | 5 min | 6% |
| Developpement (step 7) | 11 min | 14% |
| Audit (step 8) | 8 min | 10% |
| Test (step 9) | 5 min | 6% |
| Documentation (step 10) | 21 min | 27% |
| Release + retro (steps 11-13) | 2 min | 3% |
**Observation** : la documentation (27%) et la recherche (22%) consomment presque la moitie du temps. Le developpement proprement dit ne represente que 14%.
---
## 7. Recommandations
### Pour le workflow (generique)
| # | Recommandation | Impact | Effort |
|---|---------------|--------|--------|
| 1 | **Deduplication findings** : fusionner les rapports reviewer/guardian avant le fixer | Evite le travail en double | Moyen |
| 2 | **Fusion steps 1-3** pour les projets simples (< 10 fichiers estimes) | Reduit la ceremonie | Faible |
| 3 | **Checklist builder** : verifier __main__.py si pyproject.toml a un entry point CLI | Evite un bug recurrent | Faible |
| 4 | **Verification existence** : avant de creer un milestone/issue Gitea, verifier s'il existe deja | Evite les doublons | Faible |
| 5 | **Coverage obligatoire** : ajouter pytest-cov au template pyproject.toml des projets Python | Metriques manquantes | Faible |
| 6 | **Mode "light"** : pour les projets < 500 lignes, proposer un workflow reduit (6-7-8-11-13) | Adapte la ceremonie au projet | Moyen |
### Pour ce projet (gitea-dashboard)
| # | Recommandation | Priorite |
|---|---------------|----------|
| 1 | Ajouter pytest-cov et mesurer la couverture | Haute |
| 2 | Changer l'URL par defaut vers `https://gitea.tsmse.fr` | Moyenne |
| 3 | Ajouter une limite max de pages dans la pagination | Basse |
| 4 | Parallelisation des appels API (ThreadPoolExecutor) pour v1.1 | Basse |

View File

@@ -0,0 +1,51 @@
<!-- Type: explanation (Diataxis). Style: discursif, synthese des besoins et decisions, produit par /forge --discovery. -->
# Discovery Synthesis — gitea-dashboard
## Contexte
Besoin d'un outil de supervision rapide pour l'instance Gitea personnelle.
Actuellement, il faut naviguer dans l'interface web repo par repo pour voir
l'etat des issues, releases et milestones. Un dashboard CLI permet d'avoir
une vue d'ensemble en une commande.
## Utilisateurs cibles
Utilisateur unique (admin de l'instance Gitea). Usage en terminal, execution
ponctuelle pour avoir un snapshot de l'etat des repos.
## Besoins identifies
### Fonctionnels
- Lister tous les repos de l'utilisateur
- Afficher le nombre d'issues ouvertes par repo
- Afficher la derniere release par repo (tag, date)
- Afficher l'etat des milestones (ouvertes, progression)
- Formatage terminal lisible et structure (tableaux, couleurs)
### Non-fonctionnels
- Temps de reponse acceptable (< 10s pour une dizaine de repos)
- Code maintenable, teste, structure proprement
- Configuration externalisee (URL, token)
## Contraintes
- API Gitea REST a http://192.168.0.106:3000
- Authentification par token API (variable d'environnement)
- Python 3.x avec requests et rich
- Affichage unique (pas de mode watch/refresh)
- Pas de filtre par owner/org en v1 — tous les repos
## Decisions prises
| Decision | Justification |
|----------|---------------|
| Python + requests | Stack simple, maitrisee, suffisante pour des appels REST |
| rich pour le formatage | Tableaux, couleurs, mise en page terminal de qualite |
| Token en variable d'env | Securite : pas de secret dans le code ou les fichiers |
| Pas de filtre en v1 | Nombre de repos limite, simplifier le scope initial |
## Questions ouvertes
- Pagination API : verifier si l'API Gitea pagine les resultats (a traiter en recherche step 4)
- Gestion des repos sans release ou sans milestone (affichage gracieux)

294
docs/plans/v1.0.0-plan.md Normal file
View File

@@ -0,0 +1,294 @@
<!-- Type: reference (Diataxis). Style: factuel, structure par phases, actionnable par le builder. -->
# Plan de version v1.0.0 — gitea-dashboard
## Objectif
Livrer un dashboard CLI fonctionnel qui affiche en une commande l'etat de tous les repos Gitea : issues ouvertes, derniere release, milestones. Premiere version utilisable.
## Track
**Major initial** (v1.0.0, nouveau projet) : 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 -> 9 -> 10 -> 11 -> (12) -> 13
---
## Budget de scope
| Critere | Valeur |
|---------|--------|
| Max fichiers par phase | 4 |
| Total fichiers estimes | 8 (4 modules + 4 tests) |
### Inclus
- Client API Gitea avec auth token et pagination
- Collecte des donnees : repos, issues (hors PRs), derniere release, milestones ouvertes
- Affichage Rich : tableau repos + section milestones + indicateurs visuels
- Point d'entree CLI avec configuration par variables d'environnement
- Gestion des cas limites (repos sans release, sans milestone, forks, archives)
### Exclus
- Interface web ou GUI
- Mode watch / rafraichissement automatique
- Filtrage par owner/organisation
- Modification de donnees Gitea (lecture seule)
- Notifications ou alertes
- Framework CLI (argparse, click, typer)
### Differe (v1.1+)
- Parallelisation des appels API (ThreadPoolExecutor)
- Filtrage/tri des repos (par nom, activite, owner)
- Cache des reponses API
- Options CLI (--format, --filter, --sort)
---
## Etapes skippees
| Etape | Nom | Raison |
|-------|-----|--------|
| 12 | Deploy | Outil CLI local, pas de deploiement serveur |
---
## Phase 1 : Client API et collecte de donnees
**Goal** : Disposer d'un client API Gitea fonctionnel et d'un collecteur qui agrege les donnees de tous les repos.
**Issues Gitea** : fixes #1, fixes #2
### Fichiers
| Action | Fichier | Modifications | Cross-references |
|--------|---------|---------------|------------------|
| Create | `src/gitea_dashboard/client.py` | Client API Gitea : auth, requetes GET, pagination | `collector.py` (consommateur) |
| Create | `src/gitea_dashboard/collector.py` | Collecte et agregation des donnees repos | `client.py` (dependance), `display.py` (producteur de donnees) |
| Create | `tests/test_client.py` | Tests unitaires du client API avec mocks | `client.py` |
| Create | `tests/test_collector.py` | Tests unitaires du collecteur avec mocks | `collector.py` |
### Interfaces
#### client.py
```python
class GiteaClient:
"""Client API Gitea en lecture seule."""
def __init__(self, base_url: str, token: str) -> None:
"""Initialise le client avec l'URL de base et le token API."""
def get_repos(self) -> list[dict]:
"""Retourne tous les repos de l'utilisateur (pagination automatique).
Endpoint: GET /api/v1/user/repos
"""
def get_latest_release(self, owner: str, repo: str) -> dict | None:
"""Retourne la derniere release du repo, ou None si aucune.
Endpoint: GET /api/v1/repos/{owner}/{repo}/releases/latest
Gere HTTP 404 en retournant None.
"""
def get_milestones(self, owner: str, repo: str) -> list[dict]:
"""Retourne les milestones ouvertes du repo.
Endpoint: GET /api/v1/repos/{owner}/{repo}/milestones?state=open
"""
```
Methode privee interne pour la pagination :
```python
def _get_paginated(self, endpoint: str, params: dict | None = None) -> list[dict]:
"""Requete GET avec pagination automatique.
Boucle tant que len(page) == limit (50).
"""
```
#### collector.py
```python
@dataclass
class RepoData:
"""Donnees agregees d'un repo."""
name: str
full_name: str
description: str
open_issues: int # open_issues_count - open_pr_counter
is_fork: bool
is_archived: bool
is_mirror: bool
latest_release: dict | None # {tag_name, published_at} ou None
milestones: list[dict] # [{title, open_issues, closed_issues, due_on}]
def collect_all(client: GiteaClient) -> list[RepoData]:
"""Collecte les donnees de tous les repos.
Pour chaque repo : enrichit avec release et milestones.
"""
```
### Comportement attendu
1. `GiteaClient("http://192.168.0.106:3000", "token123")` cree un client
2. `client.get_repos()` retourne la liste complete (pagination transparente si > 50 repos)
3. `client.get_latest_release("admin", "mon-repo")` retourne `{"tag_name": "v1.0", "published_at": "..."}` ou `None`
4. `collect_all(client)` retourne une liste de `RepoData` pour chaque repo, meme ceux sans release ou milestone
### Tests
- `test_client.py` : mock de `requests.Session` pour tester auth header, pagination (1 page, 2 pages), 404 sur release
- `test_collector.py` : mock de `GiteaClient` pour tester l'agregation, les cas limites (repo sans release, sans milestone, fork, archive)
### Livrable
Le client peut interroger l'API Gitea et le collecteur produit une liste de `RepoData` prete pour l'affichage. Les tests passent.
---
## Phase 2 : Affichage Rich et point d'entree CLI
**Goal** : Afficher les donnees collectees dans un dashboard terminal lisible et fournir le point d'entree executable.
**Issues Gitea** : fixes #3, fixes #4
### Fichiers
| Action | Fichier | Modifications | Cross-references |
|--------|---------|---------------|------------------|
| Create | `src/gitea_dashboard/display.py` | Formatage et affichage Rich | `collector.py` (consomme `RepoData`) |
| Modify | `src/gitea_dashboard/cli.py` | Orchestration : config -> client -> collecte -> affichage | `client.py`, `collector.py`, `display.py` |
| Create | `tests/test_display.py` | Tests du formatage (capture console Rich) | `display.py` |
| Create | `tests/test_cli.py` | Tests d'integration du point d'entree | `cli.py` |
### Interfaces
#### display.py
```python
from rich.console import Console
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.
"""
```
#### cli.py (modification)
```python
import sys
import os
def main() -> None:
"""Point d'entree principal.
1. Lit GITEA_URL (defaut: http://192.168.0.106:3000) et GITEA_TOKEN (requis)
2. Cree le GiteaClient
3. Collecte les donnees via collect_all()
4. Affiche via render_dashboard()
5. Gere les erreurs : config manquante, connexion refusee, timeout
"""
```
### Comportement attendu
Execution nominale :
```
$ export GITEA_TOKEN=abc123
$ gitea-dashboard
Gitea Dashboard
┏━━━━━━━━━━━━━━━━━━┳━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━┓
┃ Repo ┃ Issues ┃ Release ┃
┡━━━━━━━━━━━━━━━━━━╇━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━┩
│ mon-projet │ 3 │ v1.2.0 (il y a 2j) │
│ autre-repo [F] │ 0 │ — │
│ archive [A] │ 1 │ v0.1.0 (il y a 6m) │
└──────────────────┴────────┴─────────────────────┘
Milestones
mon-projet / v2.0 : 3/5 (60%) — echeance 2026-04-01
```
Erreur de configuration :
```
$ gitea-dashboard
Erreur : GITEA_TOKEN non defini. Exportez la variable d'environnement.
```
### Tests
- `test_display.py` : capture de la sortie Rich via `Console(file=StringIO())`, verification du contenu (noms de repos, indicateurs, gestion des valeurs None)
- `test_cli.py` : mock des variables d'environnement, mock du client API, verification des messages d'erreur
### Livrable
`gitea-dashboard` est executable, affiche un dashboard lisible, et gere proprement les erreurs. Tous les tests passent.
---
## Phase 3 : Smoke test et documentation
**Goal** : Valider le fonctionnement reel sur l'instance Gitea et documenter le projet.
**Dependances** : phases 1 et 2 terminees, acces a l'instance Gitea (192.168.0.106:3000)
**Composants cles** :
- Test E2E manuel sur l'instance reelle
- Checklist de validation (repos avec/sans release, avec/sans milestone, forks)
- README.md a jour (installation, usage, configuration)
- CHANGELOG.md pour la v1.0.0
---
## Architecture des modules
| Module | Responsabilite | Dependances |
|--------|---------------|-------------|
| `client.py` | Communication API Gitea (auth, requetes, pagination) | `requests` |
| `collector.py` | Agregation des donnees de tous les repos | `client.py` |
| `display.py` | Formatage et rendu terminal | `rich`, `collector.py` (types) |
| `cli.py` | Point d'entree, configuration, orchestration | tous les modules |
---
## Risques d'audit
| Zone | Risque | Severite estimee |
|------|--------|-----------------|
| `client.py` — pagination | Boucle infinie si l'API repond toujours `limit` elements | major |
| `client.py` — gestion erreurs | Erreurs HTTP non-404 non gerees (500, timeout, connexion) | major |
| `cli.py` — token en clair | Affichage accidentel du token dans les logs/erreurs | critical |
| `collector.py` — types | Champs API manquants ou renommes entre versions Gitea | minor |
---
## Issues Gitea rattachees
| Issue | Titre | Phase |
|-------|-------|-------|
| [#1](https://gitea.tsmse.fr/admin/gitea-dashboard/issues/1) | Client API Gitea avec authentification et pagination | Phase 1 |
| [#2](https://gitea.tsmse.fr/admin/gitea-dashboard/issues/2) | Collecte des donnees : repos, issues, releases, milestones | Phase 1 |
| [#3](https://gitea.tsmse.fr/admin/gitea-dashboard/issues/3) | Affichage dashboard avec Rich (tableaux, couleurs) | Phase 2 |
| [#4](https://gitea.tsmse.fr/admin/gitea-dashboard/issues/4) | Point d'entree CLI et configuration | Phase 2 |
**Milestone** : [v1.0.0](https://gitea.tsmse.fr/admin/gitea-dashboard/milestone/30)
---
## Dependances
| Dependance | Type | Version |
|------------|------|---------|
| Python | Runtime | >= 3.10 |
| requests | Librairie | >= 2.31 |
| rich | Librairie | >= 13.0 |
| pytest | Dev | >= 7.0 |
| ruff | Dev | >= 0.4 |
| Instance Gitea | Service externe | 1.25.1 a 192.168.0.106:3000 |

23
docs/project/demandes.md Normal file
View File

@@ -0,0 +1,23 @@
<!-- Type: reference (Diataxis). Style: inbox format libre, traite par /workflow demande. -->
# Demandes — gitea-dashboard
> **Note** : ce projet utilise Gitea, les issues Gitea sont la source de verite.
> Ce fichier sert de fallback.
> Utiliser `/workflow demande` pour traiter les demandes.
## En attente de classification
-
## Classifiees — en attente
-
## Terminees
-
## Demandes ecosystem
-

View File

@@ -0,0 +1,50 @@
<!-- Type: reference (Diataxis). Style: factuel, fige apres creation. Capture l'intention originale. -->
# Descriptif — gitea-dashboard
## Contexte
Supervision de l'instance Gitea personnelle (192.168.0.106:3000).
Pas de vue d'ensemble disponible sans naviguer repo par repo dans l'interface web.
## Objectifs
- Afficher en une commande l'etat de tous les repos Gitea
- Visualiser les issues ouvertes, dernieres releases et milestones
- Fournir un output terminal lisible et structure
## Perimetre
### Inclus
- Connexion API Gitea avec authentification token
- Liste de tous les repos de l'utilisateur
- Nombre d'issues ouvertes par repo
- Derniere release par repo (tag + date)
- Etat des milestones (nom, progression open/closed)
- Formatage rich (tableaux, couleurs)
### Exclus
- Interface web ou GUI
- Mode watch / rafraichissement automatique
- Filtrage par owner/organisation
- Modification de donnees (lecture seule)
- Notifications ou alertes
## Utilisateurs cibles
Administrateur unique de l'instance Gitea. Usage terminal.
## Contraintes
- Python 3.x
- Dependances : requests, rich
- API Gitea REST v1
- Token en variable d'environnement (GITEA_TOKEN)
- Instance locale : http://192.168.0.106:3000
## References
- API Gitea : https://gitea.io/en-us/ (documentation Swagger disponible sur l'instance)
- Rich : https://rich.readthedocs.io/

View File

@@ -0,0 +1,145 @@
<!-- Type: reference (Diataxis). Style: factuel, exhaustif, structure par le code. Pas de tutoriel ici. -->
# Architecture — gitea-dashboard
## Vue d'ensemble
```
gitea-dashboard
==============
Terminal Application Gitea API
-------- ----------- ---------
+------------------+
$ gitea-dashboard | cli.py |
------------------->| - lit env vars |
| - configure |
| - gere erreurs |
+--------+---------+
|
v
+------------------+
| collector.py |
| - orchestre la |
| collecte | +------------------+
| - agrege en |---->| client.py |
| RepoData | | - auth token |-----> GET /api/v1/user/repos
+--------+---------+ | - pagination |-----> GET /repos/{o}/{r}/releases/latest
| | - erreurs HTTP |-----> GET /repos/{o}/{r}/milestones
v +------------------+
+------------------+
| display.py |
| - tableau repos |
<-------------------| - milestones |
Output Rich | - indicateurs |
(tableaux, +------------------+
couleurs)
```
## Composants
### cli.py — Point d'entree
Responsabilite : orchestration du flux principal.
- Lit les variables d'environnement `GITEA_URL` (defaut: `http://192.168.0.106:3000`) et `GITEA_TOKEN` (requis)
- Valide la configuration, affiche un message d'erreur clair si manquante
- Cree le `GiteaClient`, lance la collecte, passe les resultats a l'affichage
- Gere les erreurs globales (connexion refusee, timeout, erreurs API)
- Enregistre comme entry point : `gitea-dashboard = "gitea_dashboard.cli:main"`
### client.py — Client API Gitea
Responsabilite : communication avec l'API REST Gitea.
- Authentification via header `Authorization: token <GITEA_TOKEN>`
- Methode interne `_get_paginated()` pour la pagination transparente (limit=50, boucle tant que page pleine)
- Methodes publiques : `get_repos()`, `get_latest_release(owner, repo)`, `get_milestones(owner, repo)`
- Gestion du 404 sur `/releases/latest` (retourne `None`)
- Utilise `requests.Session` pour reutiliser les connexions HTTP
### collector.py — Collecteur de donnees
Responsabilite : agregation des donnees de tous les repos.
- Definit le dataclass `RepoData` qui normalise les donnees d'un repo
- Calcul des issues ouvertes reelles : `open_issues_count - open_pr_counter`
- Pour chaque repo : enrichit avec la derniere release et les milestones ouvertes
- Fonction principale `collect_all(client) -> list[RepoData]`
### display.py — Affichage Rich
Responsabilite : formatage et rendu terminal.
- Tableau principal : nom du repo, indicateurs visuels ([F]ork, [A]rchive, [M]irror), issues ouvertes, derniere release (tag + date)
- Section milestones : affichee uniquement pour les repos ayant des milestones ouvertes, avec progression (closed/total)
- Accepte un parametre `Console` optionnel pour l'injection dans les tests
- Fonction principale `render_dashboard(repos, console=None)`
## Structure du projet
```
gitea-dashboard/
CLAUDE.md # Instructions projet
pyproject.toml # Config build, deps, entry point
README.md # Documentation utilisateur
src/
gitea_dashboard/
__init__.py # Docstring du package
cli.py # Point d'entree, config, orchestration
client.py # Client API Gitea (auth, pagination)
collector.py # Agregation des donnees repos
display.py # Formatage Rich (tableaux, couleurs)
tests/
__init__.py
test_client.py # Tests client API (mocks requests)
test_collector.py # Tests collecteur (mocks client)
test_display.py # Tests affichage (capture console)
test_cli.py # Tests integration CLI (mocks env)
docs/
plans/
v1.0.0-plan.md # Plan de version
technical/
ARCHITECTURE.md # Ce fichier
decisions.md # ADR
research.md # Recherche technique API
discovery/
synthesis.md # Synthese discovery
project/
descriptif.md # Descriptif du projet
```
## Flux de donnees principal
```
1. cli.main()
|
+-- Lit os.environ["GITEA_TOKEN"] et os.environ.get("GITEA_URL", default)
|
+-- GiteaClient(url, token)
|
+-- collect_all(client)
| |
| +-- client.get_repos() -> list[dict] (pagine)
| |
| +-- Pour chaque repo:
| +-- client.get_latest_release(owner, name) -> dict | None
| +-- client.get_milestones(owner, name) -> list[dict]
| +-- RepoData(...)
|
+-- render_dashboard(repos)
|
+-- Rich Table (repos)
+-- Rich Table (milestones)
+-- Console.print()
```
## Decisions architecturales
Les decisions sont tracees dans `docs/technical/decisions.md` (format ADR).
Decisions cles pour v1.0.0 :
- **ADR-001** : Stack Python + requests + rich
- **ADR-002** : 4 modules maximum (client, collector, display, cli)
- **ADR-003** : Pas de parallelisation en v1 (sequentiel, plus simple a deboguer)

View File

@@ -0,0 +1,48 @@
<!-- Type: reference (Diataxis). Style: factuel, format ADR Nygard (Contexte/Decision/Consequences). Jamais supprimer un ADR. -->
# Architecture Decision Records — gitea-dashboard
## ADR-001 : Stack Python + requests + rich
**Date** : 2026-03-10
**Statut** : accepte
**Contexte** : Besoin d'un outil CLI de dashboard pour Gitea. Choix du langage et des librairies.
**Decision** : Python avec requests pour les appels API et rich pour le formatage terminal.
**Consequences** :
- Stack simple et maitrisee par l'utilisateur
- Pas de framework CLI lourd (argparse suffit si besoin)
- rich offre des tableaux et couleurs sans configuration complexe
- Dependance a requests (pas de client async, acceptable pour un affichage unique)
## ADR-002 : 4 modules maximum (client, collector, display, cli)
**Date** : 2026-03-10
**Statut** : accepte
**Contexte** : Definir la granularite des modules Python pour un projet simple (dashboard CLI).
**Decision** : Limiter a 4 modules avec des responsabilites claires : `client.py` (API), `collector.py` (agregation), `display.py` (formatage), `cli.py` (entree).
**Consequences** :
- Separation des responsabilites sans over-engineering
- Chaque module est testable independamment
- Un fichier = une responsabilite = un jeu de tests
- Si le projet grandit, chaque module peut evoluer independamment
## ADR-003 : Pas de parallelisation en v1
**Date** : 2026-03-10
**Statut** : accepte
**Contexte** : La collecte de donnees necessite N appels par repo (release + milestones). La parallelisation (ThreadPoolExecutor) est possible mais ajoute de la complexite.
**Decision** : V1 en sequentiel. La parallelisation est differee a v1.1+.
**Consequences** :
- Code plus simple a ecrire, deboguer et tester
- Temps de reponse acceptable pour < 20 repos (estimee < 10s)
- Pas de problemes de concurrence
- Facile a ajouter plus tard sans changer les interfaces (le collecteur est le seul point d'appel)

164
docs/technical/research.md Normal file
View File

@@ -0,0 +1,164 @@
<!-- Type: explanation (Diataxis). Style: discursif, comparaisons argumentees, sources citees. -->
# Recherche technique — gitea-dashboard
Date : 2026-03-10 | Gitea 1.25.1 | Source : swagger.v1.json + docs.gitea.com
## Contexte
Le dashboard CLI doit afficher pour chaque repo Gitea : le nombre d'issues ouvertes, la derniere release, et l'etat des milestones. Cette recherche identifie les endpoints API necessaires, la strategie de pagination, le format d'authentification, et les cas limites.
---
## 1. Authentification
### Format recommande
```
Authorization: token <GITEA_TOKEN>
```
Le header `Authorization` avec le prefixe `token ` (suivi d'un espace) est la methode recommandee. Les anciennes methodes (query parameter `?token=` ou `?access_token=`) sont **deprecated depuis Gitea 1.23** et seront supprimees.
**Source** : schema `securityDefinitions` du swagger + https://docs.gitea.com/development/api-usage
---
## 2. Endpoints API necessaires
### 2.1 Liste des repos de l'utilisateur
**Endpoint** : `GET /api/v1/user/repos`
Retourne les repos dont l'utilisateur authentifie est proprietaire.
| Parametre | Type | Description |
|-----------|------|-------------|
| `page` | integer | Numero de page (defaut : 1) |
| `limit` | integer | Elements par page (defaut : 50, max : 50) |
**Reponse** : Array de `Repository`
**Champs cles** :
| Champ | Type | Description |
|-------|------|-------------|
| `id` | int64 | Identifiant unique |
| `name` | string | Nom du repo |
| `full_name` | string | `owner/name` |
| `description` | string | Description |
| `fork` | boolean | Est un fork |
| `archived` | boolean | Est archive |
| `mirror` | boolean | Est un miroir |
| `open_issues_count` | int64 | Issues ouvertes (inclut les PRs) |
| `open_pr_counter` | int64 | PRs ouvertes (champ separe) |
| `release_counter` | int64 | Nombre total de releases |
| `owner.login` | string | Login du proprietaire |
| `updated_at` | datetime | Derniere mise a jour |
**Alternative** : `GET /api/v1/repos/search` retourne tous les repos visibles (pas seulement ceux de l'utilisateur). Reponse enveloppee dans `{"ok": true, "data": [...]}`.
### 2.2 Derniere release par repo
**Endpoint** : `GET /api/v1/repos/{owner}/{repo}/releases/latest`
Retourne la release la plus recente (hors draft et pre-release). Un seul objet, pas de pagination.
**Champs cles** :
| Champ | Type | Description |
|-------|------|-------------|
| `tag_name` | string | Nom du tag git |
| `name` | string | Titre de la release |
| `published_at` | datetime | Date de publication |
| `prerelease` | boolean | Est une pre-release |
**Retourne HTTP 404** si le repo n'a aucune release.
### 2.3 Milestones par repo
**Endpoint** : `GET /api/v1/repos/{owner}/{repo}/milestones`
| Parametre | Type | Description |
|-----------|------|-------------|
| `state` | string | `open`, `closed`, `all` |
| `page` | integer | Numero de page |
| `limit` | integer | Elements par page |
**Champs cles** :
| Champ | Type | Description |
|-------|------|-------------|
| `title` | string | Nom du milestone |
| `state` | StateType | `open` ou `closed` |
| `open_issues` | int64 | Issues ouvertes dans le milestone |
| `closed_issues` | int64 | Issues fermees dans le milestone |
| `due_on` | datetime | Date d'echeance |
Le modele contient deja `open_issues` et `closed_issues` — progression calculable sans appel supplementaire.
---
## 3. Pagination
| Parametre | Defaut | Maximum |
|-----------|--------|---------|
| `page` | 1 | - |
| `limit` | 50 | 50 |
### Headers de reponse
| Header | Description |
|--------|-------------|
| `X-Total-Count` | Nombre total d'elements |
| `Link` | URLs de navigation (`rel="next"`, `rel="last"`) |
### Strategie recommandee
1. Premiere requete avec `?limit=50&page=1`
2. Lire `X-Total-Count` dans les headers
3. Si `total > limit` : calculer le nombre de pages, lancer les requetes suivantes
4. Alternative simple : boucler tant que `len(results) == limit`
**Source** : https://docs.gitea.com/development/api-usage (section Pagination)
---
## 4. Strategie d'appels
Pour N repos :
| Donnee | Appels | Endpoint |
|--------|--------|----------|
| Liste des repos | ceil(N/50) | `GET /user/repos` |
| Derniere release | N | `GET /repos/{o}/{r}/releases/latest` |
| Milestones | N | `GET /repos/{o}/{r}/milestones` |
| **Total** | **ceil(N/50) + 2N** | |
Issues ouvertes : derivees de `open_issues_count - open_pr_counter` du modele Repository (zero appel supplementaire).
**Parallelisation** : `concurrent.futures.ThreadPoolExecutor` avec 5-10 workers. La lib `requests` est thread-safe.
---
## 5. Cas limites
| Cas | Comportement API | Gestion |
|-----|-----------------|---------|
| Repo sans release | `/releases/latest` retourne 404 | Capturer, afficher "—" |
| Repo sans milestone | `/milestones` retourne `[]` | Tableau vide, rien a afficher |
| Repo sans issues | `open_issues_count` = 0 | Comportement standard |
| Repo fork | `fork` = true | Afficher avec indicateur visuel |
| Repo archive | `archived` = true | Afficher avec indicateur visuel |
| `open_issues_count` inclut PRs | Herite de GitHub | Calculer `open_issues_count - open_pr_counter` |
---
## 6. Decisions recommandees
| # | Decision | Recommandation | Justification |
|---|----------|---------------|---------------|
| 1 | Endpoint repos | `GET /user/repos` | Plus simple, reponse directe, suffisant pour v1 |
| 2 | Comptage issues | `open_issues_count - open_pr_counter` | Zero appel supplementaire |
| 3 | Forks/archives | Tout afficher avec indicateurs | Exhaustivite, pas de filtre en v1 |
| 4 | Parallelisation | ThreadPoolExecutor (5 workers) | Acceptable pour < 200 repos |

32
pyproject.toml Normal file
View File

@@ -0,0 +1,32 @@
[build-system]
requires = ["setuptools>=68.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "gitea-dashboard"
version = "1.0.0"
description = "CLI dashboard for Gitea repos status"
requires-python = ">=3.10"
dependencies = [
"requests>=2.31",
"rich>=13.0",
]
[project.optional-dependencies]
dev = [
"pytest>=7.0",
"ruff>=0.4",
]
[project.scripts]
gitea-dashboard = "gitea_dashboard.cli:main"
[tool.setuptools.packages.find]
where = ["src"]
[tool.pytest.ini_options]
testpaths = ["tests"]
[tool.ruff]
src = ["src", "tests"]
line-length = 100

View File

@@ -0,0 +1 @@
"""Gitea Dashboard — CLI dashboard for Gitea repos status."""

View File

@@ -0,0 +1,5 @@
"""Allow running with python -m gitea_dashboard."""
from gitea_dashboard.cli import main
main()

View File

@@ -0,0 +1,58 @@
"""Point d'entree pour le CLI gitea-dashboard."""
from __future__ import annotations
import os
import sys
import requests
from rich.console import Console
from gitea_dashboard.client import GiteaClient
from gitea_dashboard.collector import collect_all
from gitea_dashboard.display import render_dashboard
_DEFAULT_URL = "http://192.168.0.106:3000"
def main() -> None:
"""Point d'entree principal.
1. Lit GITEA_URL (defaut: http://192.168.0.106:3000) et GITEA_TOKEN (requis)
2. Cree le GiteaClient
3. Collecte les donnees via collect_all()
4. Affiche via render_dashboard()
5. Gere les erreurs : config manquante, connexion refusee, timeout
"""
console = Console(stderr=True)
token = os.environ.get("GITEA_TOKEN")
if not token:
console.print(
"[red]Erreur : GITEA_TOKEN non defini. Exportez la variable d'environnement.[/red]"
)
sys.exit(1)
url = os.environ.get("GITEA_URL", _DEFAULT_URL)
client = GiteaClient(url, token)
try:
repos = collect_all(client)
except requests.ConnectionError:
console.print("[red]Erreur : connexion refusee. Verifiez l'URL et le serveur Gitea.[/red]")
sys.exit(1)
except requests.Timeout:
console.print(
"[red]Erreur : delai d'attente depasse. Le serveur Gitea ne repond pas.[/red]"
)
sys.exit(1)
except requests.RequestException as exc:
# Ne jamais afficher le token dans les messages d'erreur
msg = str(exc)
if token in msg:
msg = msg.replace(token, "***")
console.print(f"[red]Erreur API : {msg}[/red]")
sys.exit(1)
render_dashboard(repos)

View File

@@ -0,0 +1,81 @@
"""Client API Gitea en lecture seule."""
from __future__ import annotations
import requests
class GiteaClient:
"""Client API Gitea en lecture seule.
Utilise requests.Session pour reutiliser les connexions HTTP.
Authentification via header Authorization: token <TOKEN>.
"""
_PAGE_LIMIT = 50
def __init__(self, base_url: str, token: str, timeout: int = 30) -> None:
"""Initialise le client avec l'URL de base et le token API.
Args:
base_url: URL de base de l'instance Gitea.
token: Token API pour l'authentification.
timeout: Delai maximum en secondes pour chaque requete (defaut: 30).
"""
self.base_url = base_url.rstrip("/")
self.timeout = timeout
self.session = requests.Session()
self.session.headers["Authorization"] = f"token {token}"
def _get_paginated(self, endpoint: str, params: dict | None = None) -> list[dict]:
"""Requete GET avec pagination automatique.
Boucle tant que len(page) == limit (50).
"""
all_items: list[dict] = []
page = 1
merged_params = dict(params) if params else {}
while True:
merged_params["limit"] = self._PAGE_LIMIT
merged_params["page"] = page
url = f"{self.base_url}{endpoint}"
resp = self.session.get(url, params=merged_params, timeout=self.timeout)
resp.raise_for_status()
items = resp.json()
all_items.extend(items)
if len(items) < self._PAGE_LIMIT:
break
page += 1
return all_items
def get_repos(self) -> list[dict]:
"""Retourne tous les repos de l'utilisateur (pagination automatique).
Endpoint: GET /api/v1/user/repos
"""
return self._get_paginated("/api/v1/user/repos")
def get_latest_release(self, owner: str, repo: str) -> dict | None:
"""Retourne la derniere release du repo, ou None si aucune.
Endpoint: GET /api/v1/repos/{owner}/{repo}/releases/latest
Gere HTTP 404 en retournant None.
"""
url = f"{self.base_url}/api/v1/repos/{owner}/{repo}/releases/latest"
resp = self.session.get(url, timeout=self.timeout)
if resp.status_code == 404:
return None
resp.raise_for_status()
return resp.json()
def get_milestones(self, owner: str, repo: str) -> list[dict]:
"""Retourne les milestones ouvertes du repo.
Endpoint: GET /api/v1/repos/{owner}/{repo}/milestones?state=open
"""
return self._get_paginated(
f"/api/v1/repos/{owner}/{repo}/milestones",
params={"state": "open"},
)

View File

@@ -0,0 +1,52 @@
"""Collecte et agregation des donnees repos Gitea."""
from __future__ import annotations
from dataclasses import dataclass
from gitea_dashboard.client import GiteaClient
@dataclass
class RepoData:
"""Donnees agregees d'un repo."""
name: str
full_name: str
description: str
open_issues: int # open_issues_count - open_pr_counter
is_fork: bool
is_archived: bool
is_mirror: bool
latest_release: dict | None # {tag_name, published_at} ou None
milestones: list[dict] # [{title, open_issues, closed_issues, due_on}]
def collect_all(client: GiteaClient) -> list[RepoData]:
"""Collecte les donnees de tous les repos.
Pour chaque repo : enrichit avec release et milestones.
Calcule open_issues = open_issues_count - open_pr_counter.
"""
repos = client.get_repos()
result: list[RepoData] = []
for repo in repos:
owner = repo["owner"]["login"]
name = repo["name"]
result.append(
RepoData(
name=name,
full_name=repo["full_name"],
description=repo.get("description", "") or "",
open_issues=repo["open_issues_count"] - repo["open_pr_counter"],
is_fork=repo["fork"],
is_archived=repo["archived"],
is_mirror=repo["mirror"],
latest_release=client.get_latest_release(owner, name),
milestones=client.get_milestones(owner, name),
)
)
return result

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)

0
tests/__init__.py Normal file
View File

127
tests/test_cli.py Normal file
View File

@@ -0,0 +1,127 @@
"""Tests for CLI entry point."""
from unittest.mock import MagicMock, patch
import pytest
import requests
from gitea_dashboard.cli import main
class TestMainNominal:
"""Test main() happy path."""
@patch("gitea_dashboard.cli.render_dashboard")
@patch("gitea_dashboard.cli.collect_all")
@patch("gitea_dashboard.cli.GiteaClient")
def test_main_runs_full_pipeline(self, mock_client_cls, mock_collect, mock_render):
"""main() creates client, collects, and renders."""
env = {"GITEA_TOKEN": "test-token-123", "GITEA_URL": "http://localhost:3000"}
mock_client = MagicMock()
mock_client_cls.return_value = mock_client
mock_collect.return_value = []
with patch.dict("os.environ", env, clear=False):
main()
mock_client_cls.assert_called_once_with("http://localhost:3000", "test-token-123")
mock_collect.assert_called_once_with(mock_client)
mock_render.assert_called_once_with(mock_collect.return_value)
@patch("gitea_dashboard.cli.render_dashboard")
@patch("gitea_dashboard.cli.collect_all")
@patch("gitea_dashboard.cli.GiteaClient")
def test_main_uses_default_url(self, mock_client_cls, mock_collect, mock_render):
"""main() uses default URL when GITEA_URL is not set."""
env = {"GITEA_TOKEN": "my-token"}
mock_client_cls.return_value = MagicMock()
mock_collect.return_value = []
with patch.dict("os.environ", env, clear=True):
main()
mock_client_cls.assert_called_once_with("http://192.168.0.106:3000", "my-token")
class TestMainMissingToken:
"""Test main() when GITEA_TOKEN is not set."""
def test_error_message_when_token_missing(self, capsys):
"""main() exits with code 1 and prints message mentioning GITEA_TOKEN."""
with patch.dict("os.environ", {}, clear=True):
with pytest.raises(SystemExit) as exc_info:
main()
assert exc_info.value.code == 1
captured = capsys.readouterr()
assert "GITEA_TOKEN" in captured.err
class TestMainConnectionErrors:
"""Test main() error handling for network issues."""
@patch("gitea_dashboard.cli.collect_all")
@patch("gitea_dashboard.cli.GiteaClient")
def test_connection_error_handled(self, mock_client_cls, mock_collect):
"""ConnectionError is caught and exits with code 1."""
env = {"GITEA_TOKEN": "test-token"}
mock_client_cls.return_value = MagicMock()
mock_collect.side_effect = requests.ConnectionError("Connection refused")
with patch.dict("os.environ", env, clear=True):
with pytest.raises(SystemExit) as exc_info:
main()
assert exc_info.value.code == 1
@patch("gitea_dashboard.cli.collect_all")
@patch("gitea_dashboard.cli.GiteaClient")
def test_timeout_error_handled(self, mock_client_cls, mock_collect):
"""Timeout is caught and exits with code 1."""
env = {"GITEA_TOKEN": "test-token"}
mock_client_cls.return_value = MagicMock()
mock_collect.side_effect = requests.Timeout("Request timed out")
with patch.dict("os.environ", env, clear=True):
with pytest.raises(SystemExit) as exc_info:
main()
assert exc_info.value.code == 1
@patch("gitea_dashboard.cli.collect_all")
@patch("gitea_dashboard.cli.GiteaClient")
def test_request_exception_handled(self, mock_client_cls, mock_collect):
"""Generic RequestException is caught and exits with code 1."""
env = {"GITEA_TOKEN": "test-token"}
mock_client_cls.return_value = MagicMock()
mock_collect.side_effect = requests.RequestException("Something went wrong")
with patch.dict("os.environ", env, clear=True):
with pytest.raises(SystemExit) as exc_info:
main()
assert exc_info.value.code == 1
@patch("gitea_dashboard.cli.collect_all")
@patch("gitea_dashboard.cli.GiteaClient")
def test_token_not_in_error_output(self, mock_client_cls, mock_collect, capsys):
"""Token must never appear in error messages, even when present in the exception."""
env = {"GITEA_TOKEN": "super-secret-token-xyz"}
mock_client_cls.return_value = MagicMock()
# Build exception message that embeds the token value from env
# to simulate a real-world leak scenario
def make_exc(environ):
leaked = environ["GITEA_TOKEN"]
return requests.RequestException(f"HTTP Error: Authorization token {leaked} rejected")
import os as _os
with patch.dict("os.environ", env, clear=True):
mock_collect.side_effect = make_exc(_os.environ)
with pytest.raises(SystemExit):
main()
captured = capsys.readouterr()
assert env["GITEA_TOKEN"] not in captured.out
assert env["GITEA_TOKEN"] not in captured.err

160
tests/test_client.py Normal file
View File

@@ -0,0 +1,160 @@
"""Tests for GiteaClient API client."""
from unittest.mock import MagicMock, patch
from gitea_dashboard.client import GiteaClient
class TestGiteaClientInit:
"""Test client initialization and auth."""
def test_auth_header_is_set(self):
"""Session must carry Authorization header with token prefix."""
client = GiteaClient("http://gitea.local:3000", "my-secret-token")
assert client.session.headers["Authorization"] == "token my-secret-token"
def test_base_url_stored(self):
"""Base URL is stored without trailing slash."""
client = GiteaClient("http://gitea.local:3000/", "tok")
assert client.base_url == "http://gitea.local:3000"
def test_default_timeout_is_30(self):
"""Default timeout is 30 seconds."""
client = GiteaClient("http://gitea.local:3000", "tok")
assert client.timeout == 30
def test_custom_timeout_is_stored(self):
"""Custom timeout is stored and used."""
client = GiteaClient("http://gitea.local:3000", "tok", timeout=10)
assert client.timeout == 10
class TestGetPaginated:
"""Test internal pagination logic."""
def _make_client(self):
return GiteaClient("http://gitea.local:3000", "tok")
def test_single_page(self):
"""When response has fewer items than limit, stop after one request."""
client = self._make_client()
# Return 3 items (< 50 limit) -> single page
mock_resp = MagicMock()
mock_resp.raise_for_status = MagicMock()
mock_resp.json.return_value = [{"id": 1}, {"id": 2}, {"id": 3}]
with patch.object(client.session, "get", return_value=mock_resp) as mock_get:
result = client._get_paginated("/api/v1/user/repos")
assert result == [{"id": 1}, {"id": 2}, {"id": 3}]
# Called exactly once (single page)
mock_get.assert_called_once()
def test_two_pages(self):
"""When first page is full (limit items), fetch second page."""
client = self._make_client()
page1 = [{"id": i} for i in range(50)] # Exactly limit=50
page2 = [{"id": i} for i in range(50, 60)] # 10 items -> last page
mock_resp1 = MagicMock()
mock_resp1.raise_for_status = MagicMock()
mock_resp1.json.return_value = page1
mock_resp2 = MagicMock()
mock_resp2.raise_for_status = MagicMock()
mock_resp2.json.return_value = page2
with patch.object(client.session, "get", side_effect=[mock_resp1, mock_resp2]):
result = client._get_paginated("/api/v1/user/repos")
assert len(result) == 60
assert result == page1 + page2
def test_pagination_params_forwarded(self):
"""Extra params are merged with pagination params."""
client = self._make_client()
mock_resp = MagicMock()
mock_resp.raise_for_status = MagicMock()
mock_resp.json.return_value = []
with patch.object(client.session, "get", return_value=mock_resp) as mock_get:
client._get_paginated("/api/v1/repos/o/r/milestones", params={"state": "open"})
call_params = mock_get.call_args[1]["params"]
assert call_params["state"] == "open"
assert call_params["limit"] == 50
assert call_params["page"] == 1
class TestGetRepos:
"""Test get_repos method."""
def test_get_repos_calls_paginated(self):
"""get_repos delegates to _get_paginated with correct endpoint."""
client = GiteaClient("http://gitea.local:3000", "tok")
with patch.object(client, "_get_paginated", return_value=[{"id": 1}]) as mock_pag:
result = client.get_repos()
mock_pag.assert_called_once_with("/api/v1/user/repos")
assert result == [{"id": 1}]
class TestGetLatestRelease:
"""Test get_latest_release method."""
def test_returns_release_on_success(self):
"""Returns release dict when repo has a release."""
client = GiteaClient("http://gitea.local:3000", "tok")
mock_resp = MagicMock()
mock_resp.status_code = 200
mock_resp.json.return_value = {"tag_name": "v1.0", "published_at": "2026-01-01"}
with patch.object(client.session, "get", return_value=mock_resp):
result = client.get_latest_release("admin", "my-repo")
assert result == {"tag_name": "v1.0", "published_at": "2026-01-01"}
def test_returns_none_on_404(self):
"""Returns None when repo has no release (404)."""
client = GiteaClient("http://gitea.local:3000", "tok")
mock_resp = MagicMock()
mock_resp.status_code = 404
with patch.object(client.session, "get", return_value=mock_resp):
result = client.get_latest_release("admin", "no-release-repo")
assert result is None
def test_raises_on_server_error(self):
"""HTTP 500 raises an exception instead of silently returning bad data."""
import pytest
import requests as req
client = GiteaClient("http://gitea.local:3000", "tok")
mock_resp = MagicMock()
mock_resp.status_code = 500
mock_resp.raise_for_status.side_effect = req.HTTPError("500 Server Error")
with patch.object(client.session, "get", return_value=mock_resp):
with pytest.raises(req.HTTPError):
client.get_latest_release("admin", "my-repo")
class TestGetMilestones:
"""Test get_milestones method."""
def test_get_milestones_calls_paginated_with_state_open(self):
"""get_milestones delegates to _get_paginated with state=open."""
client = GiteaClient("http://gitea.local:3000", "tok")
milestones = [{"title": "v2.0", "open_issues": 3, "closed_issues": 2}]
with patch.object(client, "_get_paginated", return_value=milestones) as mock_pag:
result = client.get_milestones("admin", "my-repo")
mock_pag.assert_called_once_with(
"/api/v1/repos/admin/my-repo/milestones",
params={"state": "open"},
)
assert result == milestones

130
tests/test_collector.py Normal file
View File

@@ -0,0 +1,130 @@
"""Tests for data collector."""
from unittest.mock import MagicMock
from gitea_dashboard.collector import RepoData, collect_all
def _make_repo(
name="my-repo",
full_name="admin/my-repo",
description="A repo",
open_issues_count=5,
open_pr_counter=2,
fork=False,
archived=False,
mirror=False,
owner_login="admin",
):
"""Build a fake repo dict as returned by the Gitea API."""
return {
"name": name,
"full_name": full_name,
"description": description,
"open_issues_count": open_issues_count,
"open_pr_counter": open_pr_counter,
"fork": fork,
"archived": archived,
"mirror": mirror,
"owner": {"login": owner_login},
}
class TestCollectAll:
"""Test collect_all function."""
def test_basic_repo(self):
"""Collects repo data with release and milestones."""
client = MagicMock()
client.get_repos.return_value = [_make_repo()]
client.get_latest_release.return_value = {
"tag_name": "v1.0",
"published_at": "2026-01-01",
}
client.get_milestones.return_value = [
{"title": "v2.0", "open_issues": 3, "closed_issues": 2, "due_on": None},
]
result = collect_all(client)
assert len(result) == 1
repo = result[0]
assert isinstance(repo, RepoData)
assert repo.name == "my-repo"
assert repo.full_name == "admin/my-repo"
assert repo.description == "A repo"
assert repo.open_issues == 3 # 5 - 2
assert repo.is_fork is False
assert repo.is_archived is False
assert repo.is_mirror is False
assert repo.latest_release == {"tag_name": "v1.0", "published_at": "2026-01-01"}
assert len(repo.milestones) == 1
def test_repo_without_release(self):
"""Repo with no release gets None for latest_release."""
client = MagicMock()
client.get_repos.return_value = [_make_repo()]
client.get_latest_release.return_value = None
client.get_milestones.return_value = []
result = collect_all(client)
assert result[0].latest_release is None
def test_repo_without_milestones(self):
"""Repo with no milestones gets empty list."""
client = MagicMock()
client.get_repos.return_value = [_make_repo()]
client.get_latest_release.return_value = None
client.get_milestones.return_value = []
result = collect_all(client)
assert result[0].milestones == []
def test_open_issues_subtracts_prs(self):
"""open_issues = open_issues_count - open_pr_counter."""
client = MagicMock()
client.get_repos.return_value = [
_make_repo(open_issues_count=10, open_pr_counter=4),
]
client.get_latest_release.return_value = None
client.get_milestones.return_value = []
result = collect_all(client)
assert result[0].open_issues == 6
def test_multiple_repos(self):
"""Collects data for multiple repos."""
client = MagicMock()
client.get_repos.return_value = [
_make_repo(name="repo-a", full_name="admin/repo-a"),
_make_repo(name="repo-b", full_name="org/repo-b", owner_login="org"),
]
client.get_latest_release.return_value = None
client.get_milestones.return_value = []
result = collect_all(client)
assert len(result) == 2
assert result[0].name == "repo-a"
assert result[1].name == "repo-b"
# Verify correct owner/repo passed to enrichment calls
client.get_latest_release.assert_any_call("admin", "repo-a")
client.get_latest_release.assert_any_call("org", "repo-b")
def test_fork_and_archived_flags(self):
"""Fork and archived flags are propagated."""
client = MagicMock()
client.get_repos.return_value = [
_make_repo(fork=True, archived=True, mirror=True),
]
client.get_latest_release.return_value = None
client.get_milestones.return_value = []
result = collect_all(client)
assert result[0].is_fork is True
assert result[0].is_archived is True
assert result[0].is_mirror is True

216
tests/test_display.py Normal file
View File

@@ -0,0 +1,216 @@
"""Tests for Rich dashboard display."""
from io import StringIO
from rich.console import Console
from gitea_dashboard.collector import RepoData
from gitea_dashboard.display import render_dashboard
def _make_console():
"""Create a console that captures output for testing.
highlight=False desactive le highlighting automatique de Rich
qui fragmente les nombres et noms avec des codes ANSI.
"""
buf = StringIO()
return Console(file=buf, force_terminal=True, width=120, highlight=False), buf
def _make_repo(
name="my-repo",
full_name="admin/my-repo",
description="A repo",
open_issues=3,
is_fork=False,
is_archived=False,
is_mirror=False,
latest_release=None,
milestones=None,
):
"""Build a RepoData for testing."""
return RepoData(
name=name,
full_name=full_name,
description=description,
open_issues=open_issues,
is_fork=is_fork,
is_archived=is_archived,
is_mirror=is_mirror,
latest_release=latest_release,
milestones=milestones if milestones is not None else [],
)
class TestRenderDashboardTable:
"""Test the main repos table rendering."""
def test_basic_repo_displayed(self):
"""Repo name and issues count appear in output."""
console, buf = _make_console()
repos = [
_make_repo(
name="mon-projet",
open_issues=3,
latest_release={"tag_name": "v1.2.0", "published_at": "2026-03-08T10:00:00Z"},
),
]
render_dashboard(repos, console=console)
output = buf.getvalue()
assert "mon-projet" in output
assert "v1.2.0" in output
def test_repo_without_release_shows_dash(self):
"""Repo with no release shows a dash character."""
console, buf = _make_console()
repos = [_make_repo(name="no-release", latest_release=None)]
render_dashboard(repos, console=console)
output = buf.getvalue()
assert "no-release" in output
# The dash character for "no release"
assert "\u2014" in output or "" in output
def test_fork_indicator(self):
"""Fork repos show [F] indicator."""
console, buf = _make_console()
repos = [_make_repo(name="forked-repo", is_fork=True)]
render_dashboard(repos, console=console)
output = buf.getvalue()
assert "forked-repo" in output
assert "[F]" in output
def test_archive_indicator(self):
"""Archived repos show [A] indicator."""
console, buf = _make_console()
repos = [_make_repo(name="old-repo", is_archived=True)]
render_dashboard(repos, console=console)
output = buf.getvalue()
assert "[A]" in output
def test_mirror_indicator(self):
"""Mirror repos show [M] indicator."""
console, buf = _make_console()
repos = [_make_repo(name="mirror-repo", is_mirror=True)]
render_dashboard(repos, console=console)
output = buf.getvalue()
assert "[M]" in output
def test_multiple_indicators(self):
"""Repo with multiple flags shows all indicators."""
console, buf = _make_console()
repos = [_make_repo(name="special", is_fork=True, is_archived=True)]
render_dashboard(repos, console=console)
output = buf.getvalue()
assert "[F]" in output
assert "[A]" in output
def test_issues_zero(self):
"""Repos with 0 issues display 0."""
console, buf = _make_console()
repos = [_make_repo(name="clean-repo", open_issues=0)]
render_dashboard(repos, console=console)
output = buf.getvalue()
assert "0" in output
def test_multiple_repos(self):
"""Multiple repos all appear in the output."""
console, buf = _make_console()
repos = [
_make_repo(name="repo-alpha", open_issues=1),
_make_repo(name="repo-beta", open_issues=5),
]
render_dashboard(repos, console=console)
output = buf.getvalue()
assert "repo-alpha" in output
assert "repo-beta" in output
class TestRenderDashboardMilestones:
"""Test the milestones section rendering."""
def test_milestones_displayed(self):
"""Repos with milestones show milestone info."""
console, buf = _make_console()
repos = [
_make_repo(
name="mon-projet",
milestones=[
{
"title": "v2.0",
"open_issues": 2,
"closed_issues": 3,
"due_on": "2026-04-01T00:00:00Z",
},
],
),
]
render_dashboard(repos, console=console)
output = buf.getvalue()
assert "v2.0" in output
assert "3/5" in output # closed/total
assert "60%" in output
def test_milestone_without_due_date(self):
"""Milestone without due_on omits the deadline."""
console, buf = _make_console()
repos = [
_make_repo(
name="projet",
milestones=[
{
"title": "backlog",
"open_issues": 5,
"closed_issues": 1,
"due_on": None,
},
],
),
]
render_dashboard(repos, console=console)
output = buf.getvalue()
assert "backlog" in output
assert "1/6" in output
def test_no_milestones_section_when_none(self):
"""When no repos have milestones, milestone section is absent."""
console, buf = _make_console()
repos = [_make_repo(name="simple", milestones=[])]
render_dashboard(repos, console=console)
output = buf.getvalue()
assert "Milestones" not in output
class TestRenderDashboardEmpty:
"""Test empty repo list handling."""
def test_empty_list_shows_message(self):
"""Empty repo list shows informative message."""
console, buf = _make_console()
render_dashboard([], console=console)
output = buf.getvalue()
assert "Aucun repo" in output