chore(workflow): complete step 6 (plan v1.3.0), start step 7

3 phases: corrections/robustesse (#11,#12), tests edge (#13), features (#14,#15)
ADR-009 (retry 429), ADR-010 (sanitize JSON), ADR-011 (--health flag)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
sylvain
2026-03-12 19:07:57 +01:00
parent 7dab240dce
commit 9783389bfb
4 changed files with 611 additions and 2 deletions

View File

@@ -10,7 +10,7 @@
| Version courante | v1.3.0 |
| Track | minor |
| Phase courante | 2 — DEV |
| Etape courante | 6 (pending) |
| Etape courante | 6 (done) |
| workflow_version | v1.1 |
---
@@ -83,7 +83,7 @@
| # | Etape | Statut | Date | Agent/Skill | Validation | Notes |
|---|-------|--------|------|-------------|------------|-------|
| 6 | Plan de version | pending | - | architect | Auto (plan avec phases, budget scope) | - |
| 6 | Plan de version | done | 2026-03-12 | architect | Auto (plan avec phases, budget scope) | step_6: done, plan: docs/plans/v1.3.0-plan.md, phases: 3, ADR-009/010/011, gitea_milestone: exists (id:46) |
| 7 | Developpement | - | - | build / builder | Auto (tests passent) | - |
| 8 | Audit + corrections | - | - | reviewer + guardian + fixer | Auto (score 100) | - |
| 9 | Smoke test | - | - | tester + checklist | Auto (E2E + checklist) | - |
@@ -140,6 +140,7 @@
| 2026-03-12 | step 12 skipped | CLI local, pas de deploy |
| 2026-03-12 | step 13 done | Retrospective, metriques, analyse, milestone fermee |
| 2026-03-12 | Start v1.3.0 at step 6 | Minor track, 5 issues ouvertes: #11 (429 retry), #12 (JSON faux positif), #13 (tests edge), #14 (--health), #15 (description repos) |
| 2026-03-12 | step 6 done | Plan v1.3.0 (3 phases, 9 fichiers, ADR-009/010/011), milestone exists (id:46), labels #14/#15 ajoutés |
## Versions completees

558
docs/plans/v1.3.0-plan.md Normal file
View File

@@ -0,0 +1,558 @@
<!-- Type: reference (Diataxis). Style: factuel, structure par phases, actionnable par le builder. -->
# Plan de version v1.3.0 — gitea-dashboard
## Objectif
Corriger la gestion du rate limiting HTTP 429 dans le retry, investiguer et corriger les caracteres de controle dans l'export JSON, ajouter des tests edge cases manquants, une commande `--health` de diagnostic, et l'affichage de la description des repos.
## Track
**Minor** : 6 -> 7 -> 8 -> 9 -> 10 -> 11 -> (12) -> 13
---
## Budget de scope
| Critere | Valeur |
|---------|--------|
| Max fichiers par phase | 5 |
| Total fichiers estimes | 9 (5 modules modifies + 4 fichiers de tests modifies) |
| Fichiers crees | 0 |
| Tests estimes | ~25 nouveaux (total ~113) |
### Inclus
- Gestion du HTTP 429 (rate limiting) dans le retry (#11)
- Investigation et correction des caracteres de controle dans l'export JSON (#12)
- Tests edge cases : unicode, repos vides, 429, API malformee, caracteres de controle (#13)
- Commande `--health` pour verifier la connexion Gitea (#14)
- Colonne "Description" dans le tableau avec troncature a 40 chars et option `--no-desc` (#15)
### Exclus
- Parallelisation des appels API (ADR-003, differee)
- Export CSV (hors scope)
- Cache API local (differe)
- Dashboard interactif TUI (differe)
### Differe (v1.4+)
- Parallelisation des appels API
- Export CSV/YAML
- Cache API local (fichier/SQLite)
- Dashboard interactif (TUI)
---
## Etapes skippees
| Etape | Nom | Raison |
|-------|-----|--------|
| 1 | Discovery | Projet existant, discovery v1.0.0 suffisante |
| 2 | Project creation | Projet existant |
| 3 | Specs | Minor — specs couvertes par les issues #11-#15 et ce plan |
| 4 | Research | Pas de technologie nouvelle, API connue |
| 5 | Roadmap | Minor — milestone v1.3.0 deja creee sur Gitea |
| 12 | Deploy | Outil CLI local, pas de deploiement serveur |
---
## Analyse des dependances entre issues
```
#11 (retry 429) -- fondation, aucune dependance
#12 (JSON caracteres de controle) -- aucune dependance, module exporter.py
#13 (edge cases) -- necessite #11 (tests 429) et #12 (tests caracteres controle)
#14 (--health) -- aucune dependance, nouveau endpoint client
#15 (description repos) -- aucune dependance, display + cli
```
Regroupement logique :
- #11 + #12 + #13 sont lies : les bugs (#11, #12) doivent etre corriges avant que les tests edge cases (#13) puissent les couvrir.
- #14 et #15 sont des features independantes.
Ordre : (#11 + #12) -> #13 -> (#14 + #15)
---
## Phase 1 : Corrections et robustesse (#11, #12)
**Goal** : Corriger le retry pour gerer le rate limiting HTTP 429, et sanitizer les caracteres de controle dans l'export JSON.
**Issues Gitea** : fixes #11, fixes #12
### Fichiers
| Action | Fichier | Modifications | Cross-references |
|--------|---------|---------------|------------------|
| Modify | `src/gitea_dashboard/client.py` | `_get_with_retry` : intercepter HTTPError pour status 429, respecter le header `Retry-After`, retenter apres le delai indique (ou backoff par defaut si absent) | `cli.py` (gestion erreur finale) |
| Modify | `src/gitea_dashboard/exporter.py` | `export_json` : sanitizer les caracteres de controle (ASCII 0x00-0x1F sauf \n \r \t) dans les champs texte avant serialisation JSON. Ou bien desactiver le markup Rich si le format est JSON | `cli.py` (routage format) |
| Modify | `tests/test_client.py` | Tests retry sur HTTP 429, avec et sans Retry-After | `client.py` |
| Modify | `tests/test_exporter.py` | Tests export JSON avec caracteres de controle dans description | `exporter.py` |
### Interfaces
#### client.py (modifications)
```python
class GiteaClient:
def _get_with_retry(self, url: str, params: dict | None = None) -> requests.Response:
"""GET avec retry automatique sur timeout ET rate limiting (HTTP 429).
Comportement actuel : retry sur requests.Timeout uniquement.
Ajout : si la reponse HTTP est 429 (Too Many Requests),
respecter le header Retry-After (en secondes) pour le delai d'attente.
Si Retry-After est absent, utiliser le backoff lineaire standard.
Retente jusqu'a _MAX_RETRIES fois.
Leve requests.HTTPError si 429 persiste apres epuisement des retries.
Leve requests.Timeout si timeout persiste.
"""
```
**Pourquoi intercepter le 429 dans `_get_with_retry`** : le rate limiting est une preoccupation du transport HTTP, au meme titre que le timeout. Le client est le bon endroit car il centralise deja la logique de retry (ADR-007). L'alternative serait de verifier le status code apres chaque appel dans `_get_paginated`, mais cela dupliquerait la logique.
**Pourquoi respecter Retry-After** : c'est le mecanisme standard HTTP (RFC 7231 Section 7.1.3). Gitea peut indiquer un delai specifique. L'ignorer revient a retenter trop tot et echouer de nouveau.
#### exporter.py (modifications)
```python
def _sanitize_control_chars(text: str) -> str:
"""Supprime les caracteres de controle ASCII (0x00-0x1F) sauf newline,
carriage return et tab.
Ces caracteres peuvent provenir de descriptions de repos Gitea
et causent des erreurs JSON ('Invalid control character').
"""
def repos_to_dicts(repos: list[RepoData]) -> list[dict]:
"""Convertit une liste de RepoData en liste de dicts serialisables.
Sanitize les champs texte (name, full_name, description) pour
supprimer les caracteres de controle invalides en JSON.
"""
```
**Pourquoi sanitizer dans exporter.py et non dans collector.py** : les caracteres de controle ne posent probleme que pour la serialisation JSON. Le rendu Rich les gere nativement. Sanitizer dans le collecteur modifierait les donnees pour tous les consommateurs, ce qui n'est pas souhaitable. Le point de sortie (exporter) est le bon endroit.
**Pourquoi ne pas simplement desactiver Rich** : le probleme n'est pas Rich (les codes ANSI ne sont pas injectes dans l'export JSON car `print()` est utilise, pas `Console.print()`). Le probleme vient des caracteres de controle dans les donnees source (descriptions de repos). La sanitisation est la correction correcte.
### Comportement attendu
1. HTTP 429 avec Retry-After :
```
GET /api/v1/user/repos -> 429, Retry-After: 5
# Attend 5 secondes
GET /api/v1/user/repos -> 200 OK
# Transparent pour l'utilisateur
```
2. HTTP 429 sans Retry-After :
```
GET /api/v1/user/repos -> 429
# Attend 1s (backoff lineaire standard)
GET /api/v1/user/repos -> 200 OK
```
3. HTTP 429 persistant (apres max retries) :
```
GET -> 429, GET -> 429, GET -> 429
# Leve HTTPError, attrape par cli.py (RequestException handler)
# Message : "Erreur API : 429 Too Many Requests"
```
4. Export JSON avec caracteres de controle dans la description :
```
$ gitea-dashboard --format json | python3 -m json.tool
# Plus d'erreur "Invalid control character"
# Les caracteres de controle sont supprimes silencieusement
```
### Tests
#### test_client.py (ajouts)
- `test_retry_on_429_with_retry_after` : reponse 429 avec Retry-After: 2, puis 200. Verifie que `time.sleep` est appele avec 2.0 et que la reponse finale est 200.
- `test_retry_on_429_without_retry_after` : reponse 429 sans header, puis 200. Verifie que le backoff lineaire standard est utilise.
- `test_retry_on_429_exhausted` : 3 reponses 429 -> leve HTTPError.
- `test_retry_on_429_then_timeout` : 429 puis Timeout. Verifie que les deux types sont geres dans la meme boucle.
#### test_exporter.py (ajouts)
- `test_export_json_sanitizes_control_chars` : description avec `\x00\x01\x02` -> JSON valide sans ces caracteres.
- `test_export_json_preserves_newlines_tabs` : description avec `\n` et `\t` -> preserves dans le JSON.
- `test_export_json_unicode_safe` : description avec emojis et accents -> JSON valide.
### Livrable
Le retry gere les HTTP 429 avec respect du Retry-After. L'export JSON ne contient plus de caracteres de controle invalides. Tous les tests passent.
---
## Phase 2 : Tests edge cases (#13)
**Goal** : Ajouter une couverture de tests pour les cas limites non couverts par les 88 tests existants.
**Issues Gitea** : fixes #13
### Fichiers
| Action | Fichier | Modifications | Cross-references |
|--------|---------|---------------|------------------|
| Modify | `tests/test_collector.py` | Tests RepoData avec unicode, repo 0 commits (deja couvert partiellement, completer) | `collector.py` |
| Modify | `tests/test_client.py` | Test reponse API malformee (JSON invalide) | `client.py` |
| Modify | `tests/test_display.py` | Tests affichage avec description contenant unicode et caracteres speciaux | `display.py` |
| Modify | `tests/test_exporter.py` | Tests deja ajoutes en phase 1 pour les caracteres de controle, completer si necessaire | `exporter.py` |
### Interfaces
Pas de nouvelle interface -- cette phase n'ajoute que des tests.
### Comportement attendu
Tous les edge cases identifes sont couverts par des tests unitaires :
1. **RepoData Unicode** : un RepoData avec `description="Projet avec des accents : e, a, u et des emojis"` se collecte, s'affiche et s'exporte sans erreur.
2. **Repo 0 commits** : deja partiellement couvert (`test_collect_all_no_commits`), mais verifier que l'affichage et l'export JSON fonctionnent aussi.
3. **Mock HTTP 429** : couvert par la phase 1, mais ajouter un test d'integration dans `test_collector.py` qui simule un 429 pendant la collecte et verifie que le retry est transparent.
4. **Reponse API malformee** : le client recoit du HTML au lieu de JSON (ex: page de maintenance Gitea). Doit lever une exception claire, pas un crash obscur.
5. **Description avec caracteres de controle** : couvert par la phase 1 pour l'export JSON, ajouter un test pour le tableau Rich.
### Tests
#### test_collector.py (ajouts)
- `test_repo_data_unicode_description` : RepoData avec description unicode complete (accents, CJK, emojis).
- `test_collect_all_repo_zero_commits_and_no_release` : repo sans commits ET sans release -> RepoData avec `last_commit_date=None` et `latest_release=None`.
#### test_client.py (ajouts)
- `test_get_paginated_malformed_json` : mock reponse avec `resp.json()` qui leve `json.JSONDecodeError` -> verifie que l'exception remonte proprement.
- `test_get_repos_html_response` : mock reponse HTML (status 200 mais contenu HTML) -> verifie le comportement.
#### test_display.py (ajouts)
- `test_render_dashboard_unicode_description` : repo avec description unicode -> le tableau Rich s'affiche sans crash.
- `test_render_dashboard_control_chars_in_name` : repo avec caracteres de controle dans le nom -> pas de crash.
#### test_exporter.py (ajouts, complement phase 1)
- `test_export_json_empty_description` : description vide -> JSON valide.
- `test_export_json_very_long_description` : description de 10000 caracteres -> JSON valide.
### Livrable
La couverture de tests passe de 88 a ~103 tests. Tous les edge cases identifies dans l'issue #13 sont couverts. Les tests documentent le comportement attendu pour les cas limites.
---
## Phase 3 : Nouvelles fonctionnalites (#14, #15)
**Goal** : Ajouter l'option `--health` pour verifier la connexion Gitea, et la colonne "Description" dans le tableau.
**Issues Gitea** : fixes #14, fixes #15
### Fichiers
| Action | Fichier | Modifications | Cross-references |
|--------|---------|---------------|------------------|
| Modify | `src/gitea_dashboard/client.py` | Ajouter methode `get_version()` qui appelle `GET /api/v1/version` | `cli.py` (consomme pour --health) |
| Modify | `src/gitea_dashboard/cli.py` | Ajouter options `--health` et `--no-desc` dans argparse. Logique --health : appeler `get_version()`, compter les repos, afficher, exit 0 ou 1 | `client.py` (get_version), `display.py` (render_dashboard) |
| Modify | `src/gitea_dashboard/display.py` | Ajouter colonne "Description" au tableau, troncature a 40 chars avec "...", parametre `show_description` dans `render_dashboard()` | `collector.py` (champ description deja present dans RepoData) |
| Modify | `tests/test_client.py` | Tests `get_version()` | `client.py` |
| Modify | `tests/test_cli.py` | Tests --health (succes, echec connexion), tests --no-desc | `cli.py` |
### Interfaces
#### client.py (modifications)
```python
class GiteaClient:
def get_version(self) -> dict:
"""Retourne la version de l'instance Gitea.
Endpoint: GET /api/v1/version
Retourne: {"version": "1.21.0"}
Leve HTTPError si l'appel echoue.
"""
```
**Pourquoi une methode dediee plutot qu'un appel direct dans cli.py** : coherent avec l'architecture (ADR-002) -- toute communication API passe par `client.py`. Le CLI ne connait pas les endpoints.
#### cli.py (modifications)
```python
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
"""Parse les arguments CLI.
Options existantes : --repo, --exclude, --sort, --format
Nouvelles options :
--health : verifie la connexion Gitea et affiche la version.
Mutuellement exclusif avec le dashboard normal.
Exit code 0 si connexion OK, 1 sinon.
--no-desc : masque la colonne Description dans le tableau.
"""
def _run_health_check(client: GiteaClient, console: Console) -> None:
"""Execute le health check et affiche les resultats.
1. Appelle client.get_version() -> affiche "Gitea vX.Y.Z"
2. Appelle client.get_repos() -> affiche "N repos accessibles"
3. Exit code 0 si tout OK
Leve une exception en cas d'echec (geree par le try/except de main).
"""
```
**Pourquoi `--health` est mutuellement exclusif** : l'utilisateur veut soit verifier la connexion, soit afficher le dashboard. Les deux en meme temps n'ont pas de sens. Si `--health` est present, les options `--repo`, `--exclude`, `--sort`, `--format` sont ignorees.
**Pourquoi une fonction `_run_health_check` separee** : eviter de surcharger `main()` avec de la logique conditionnelle. La fonction est interne (prefixe `_`) car elle n'est pas une interface publique.
#### display.py (modifications)
```python
def _truncate(text: str, max_length: int = 40) -> str:
"""Tronque le texte a max_length caracteres avec '...' si necessaire."""
def render_dashboard(
repos: list[RepoData],
console: Console | None = None,
sort_key: str = "name",
show_description: bool = True,
) -> None:
"""Affiche le dashboard. Nouveau parametre show_description.
Si show_description est True, ajoute une colonne "Description"
entre "Repo" et "Issues", tronquee a 40 caracteres.
"""
```
**Pourquoi tronquer a 40 caracteres** : les descriptions peuvent etre longues et casser le tableau Rich. 40 chars est un compromis entre informativite et lisibilite. Le suffixe "..." indique visuellement que le texte est tronque.
**Pourquoi un parametre `show_description` et non un filtre de colonnes generique** : YAGNI. Une seule colonne est optionnelle. Un systeme generique serait over-engineere pour ce cas.
### Comportement attendu
1. Health check reussi :
```
$ gitea-dashboard --health
Gitea v1.21.0
12 repos accessibles
$ echo $?
0
```
2. Health check echoue :
```
$ gitea-dashboard --health
Erreur : connexion refusee. Verifiez l'URL et le serveur Gitea.
$ echo $?
1
```
3. Description dans le tableau :
```
Gitea Dashboard
+------------------+------------------------------------------+--------+------------------+----------------+
| Repo | Description | Issues | Release | Dernier commit |
+------------------+------------------------------------------+--------+------------------+----------------+
| mon-projet | Dashboard CLI pour Gitea | 3 | v1.2.0 (il y a 5j) | il y a 2j |
| long-description | Un tres long texte de description qui... | 0 | --- | il y a 1j |
+------------------+------------------------------------------+--------+------------------+----------------+
```
4. Sans description :
```
$ gitea-dashboard --no-desc
# Tableau identique a v1.2.0 (pas de colonne Description)
```
5. Export JSON : la description est toujours presente dans le JSON (le champ existe deja dans RepoData). `--no-desc` n'affecte que l'affichage tableau.
### Tests
#### test_client.py (ajouts)
- `test_get_version_success` : mock reponse 200 avec `{"version": "1.21.0"}` -> retourne le dict.
- `test_get_version_connection_error` : mock ConnectionError -> leve l'exception.
#### test_cli.py (ajouts)
- `test_parse_args_health` : `--health` -> `Namespace(health=True)`.
- `test_main_health_success` : mock client.get_version et get_repos -> exit 0, affiche version et nombre de repos.
- `test_main_health_connection_error` : mock ConnectionError -> exit 1.
- `test_parse_args_no_desc` : `--no-desc` -> `Namespace(no_desc=True)`.
- `test_main_passes_no_desc_to_render` : verifie que `render_dashboard` est appele avec `show_description=False`.
#### test_display.py (ajouts)
- `test_description_column_displayed` : le tableau contient une colonne "Description".
- `test_description_truncated_at_40` : description de 60 chars -> tronquee a 40 + "...".
- `test_description_short_not_truncated` : description de 20 chars -> affichee telle quelle.
- `test_description_empty` : description vide -> cellule vide (pas de crash).
- `test_no_description_flag` : `show_description=False` -> pas de colonne "Description".
### Livrable
L'option `--health` permet de verifier la connexion Gitea. Le tableau affiche la description des repos, tronquee a 40 chars, masquable avec `--no-desc`. Tous les tests passent.
---
## Architecture des modules (impact v1.3.0)
```
gitea-dashboard v1.3.0
=====================
Terminal Application Gitea API
-------- ----------- ---------
+------------------+
$ gitea-dashboard | cli.py |
--health | - parse args |
--no-desc | - route health |
| - route format |
| - gere erreurs |
+--------+---------+
|
+--------+---------+
| --health? |
+--+----------+----+
| |
oui | | non
v v
get_version() collect_all()
get_repos() |
(count) +-------+-------+
| |
v v
+------------+ +-------------+
| display.py | | exporter.py |
| + Description| | + sanitize |
<-----------------| + troncature | | control ch |---------> stdout (JSON)
Output Rich | + --no-desc | +-------------+
(tableaux) +------------+
+------------------+
| client.py |
| + get_version() |-----> GET /api/v1/version
| + retry HTTP 429 |-----> GET /api/v1/user/repos
| + Retry-After |-----> GET .../releases/latest
+------------------+-----> GET .../milestones
-----> GET .../commits?limit=1
```
| Module | Impact v1.3.0 | Detail |
|--------|--------------|--------|
| `client.py` | Modifie | Retry HTTP 429 + Retry-After, nouvelle methode `get_version()` |
| `collector.py` | Inchange | Pas de modification (RepoData a deja `description`) |
| `display.py` | Modifie | Colonne "Description" avec troncature, parametre `show_description` |
| `exporter.py` | Modifie | Sanitisation des caracteres de controle |
| `cli.py` | Modifie | Options `--health` et `--no-desc`, logique health check |
---
## Decisions architecturales
### ADR-009 : Retry HTTP 429 avec Retry-After dans _get_with_retry (v1.3.0)
**Date** : 2026-03-12
**Statut** : accepte
**Contexte** : Le retry dans `_get_with_retry` ne gere que `requests.Timeout` (exception Python). Un HTTP 429 (rate limiting) retourne une reponse avec un status code, pas une exception Timeout. Le retry ne se declenche donc pas sur rate limiting.
**Decision** : Etendre `_get_with_retry` pour intercepter les reponses HTTP 429. Si le header `Retry-After` est present, utiliser sa valeur comme delai d'attente. Sinon, utiliser le backoff lineaire standard. Apres epuisement des retries, lever `requests.HTTPError`.
**Consequences** :
- La logique de retry reste centralisee dans une seule methode (coherent avec ADR-007)
- Le header Retry-After est un standard HTTP, le respecter evite les retries inutiles
- La boucle de retry gere desormais deux cas : Timeout (exception) et 429 (reponse)
- Pas de changement d'interface publique -- transparent pour les appelants
- Risque : complexite accrue de `_get_with_retry` (2 cas au lieu de 1), mais reste testable
### ADR-010 : Sanitisation des caracteres de controle dans exporter.py (v1.3.0)
**Date** : 2026-03-12
**Statut** : accepte
**Contexte** : L'export JSON peut contenir des caracteres de controle ASCII (0x00-0x1F) provenant des descriptions de repos. Ces caracteres sont invalides dans une chaine JSON selon RFC 8259, et `python3 -m json.tool` les rejette.
**Decision** : Sanitiser les champs texte dans `repos_to_dicts()` avant serialisation. Supprimer les caracteres de controle sauf `\n`, `\r` et `\t` (qui sont echappes par `json.dumps`).
**Consequences** :
- La sanitisation est au point de sortie (exporter), pas au point d'entree (collector)
- Les donnees dans RepoData restent brutes (pas de perte d'information pour le rendu Rich)
- `json.dumps` avec `ensure_ascii=False` gere nativement `\n`, `\r`, `\t` -- seuls les autres caracteres de controle posent probleme
- Approche defensive : meme si les descriptions actuelles n'ont pas de caracteres de controle, le code est protege
### ADR-011 : --health comme commande alternative, pas sous-commande (v1.3.0)
**Date** : 2026-03-12
**Statut** : accepte
**Contexte** : L'option `--health` est un mode alternatif au dashboard. Deux approches : flag optionnel (`--health`) ou sous-commande (`gitea-dashboard health`).
**Decision** : Utiliser un flag optionnel `--health` dans argparse. Pas de sous-commandes.
**Consequences** :
- Coherent avec ADR-004 (argparse simple, pas de framework CLI lourd)
- Un seul niveau d'arguments -- pas de complexite de sous-commandes
- `--health` est mutuellement exclusif avec le mode dashboard (les options --repo, --sort, etc. sont ignorees)
- Si d'autres modes alternatifs apparaissent (ex: `--export-config`), il faudra reconsiderer les sous-commandes
---
## Risques d'audit
| Zone | Risque | Severite estimee |
|------|--------|-----------------|
| `client.py` -- retry 429 | La boucle de retry devient plus complexe (2 types de retry). Risque de regression sur le retry timeout existant | major |
| `client.py` -- Retry-After | Le header Retry-After peut contenir une date HTTP (RFC 7231) au lieu de secondes. Ne gerer que les secondes (entier) est suffisant mais incomplet | minor |
| `exporter.py` -- sanitisation | La regex de sanitisation pourrait supprimer des caracteres Unicode valides si mal ecrite | major |
| `cli.py` -- --health | Si `--health` et `--format json` sont combines, le comportement n'est pas defini. Doit etre documente ou interdit | minor |
| `display.py` -- troncature | La troncature a 40 chars peut couper au milieu d'un caractere multi-byte (unicode) | minor |
| `display.py` -- retrocompatibilite | L'ajout de la colonne "Description" change le rendu par defaut. Les utilisateurs qui parsent la sortie Rich seront affectes | minor |
| `tests` -- couverture | L'issue #13 est une issue de tests sans code de production. Le builder doit ecrire les tests APRES les corrections de #11/#12 | minor |
---
## Issues Gitea rattachees
| Issue | Titre | Phase |
|-------|-------|-------|
| [#11](https://gitea.tsmse.fr/admin/gitea-dashboard/issues/11) | [bug] Le retry ne gere pas le rate limiting (HTTP 429) | Phase 1 |
| [#12](https://gitea.tsmse.fr/admin/gitea-dashboard/issues/12) | [bug] Invalid control character dans le JSON en pipe | Phase 1 |
| [#13](https://gitea.tsmse.fr/admin/gitea-dashboard/issues/13) | [improvement] Ajouter des tests edge cases | Phase 2 |
| [#14](https://gitea.tsmse.fr/admin/gitea-dashboard/issues/14) | [feat] Commande --health pour verifier la connexion Gitea | Phase 3 |
| [#15](https://gitea.tsmse.fr/admin/gitea-dashboard/issues/15) | [feat] Afficher la description des repos dans le tableau | Phase 3 |
---
## Dependances
| Dependance | Type | Version |
|------------|------|---------|
| Python | Runtime | >= 3.10 |
| requests | Librairie | >= 2.31 (inchange) |
| rich | Librairie | >= 13.0 (inchange) |
| pytest | Dev | >= 7.0 (inchange) |
| ruff | Dev | >= 0.4 (inchange) |
| Instance Gitea | Service externe | 192.168.0.106:3000 |
Aucune nouvelle dependance. Tous les ajouts utilisent la stdlib Python (re pour la sanitisation, pas de nouvelle librairie).
---
## Criteres de validation par issue
| Issue | Critere de validation |
|-------|----------------------|
| #11 | `_get_with_retry` retente sur HTTP 429. Le header `Retry-After` est respecte. Test unitaire avec mock 429 (avec et sans Retry-After). Apres epuisement des retries, leve HTTPError. |
| #12 | `gitea-dashboard --format json \| python3 -m json.tool` fonctionne meme si les descriptions contiennent des caracteres de controle. Test avec `\x00`-`\x1f`. |
| #13 | Tests edge cases ajoutes : RepoData unicode, repo 0 commits, mock HTTP 429, reponse API malformee, description avec caracteres de controle. Minimum 10 nouveaux tests. |
| #14 | `gitea-dashboard --health` appelle `GET /api/v1/version`, affiche la version Gitea et le nombre de repos, exit code 0 si OK, 1 sinon. Tests unitaires pour succes et echec. |
| #15 | Le tableau affiche une colonne "Description" tronquee a 40 chars avec "...". `--no-desc` masque la colonne. Tests unitaires pour troncature, description vide, et flag --no-desc. |

View File

@@ -102,6 +102,7 @@ gitea-dashboard/
v1.0.0-plan.md # Plan de version
v1.1.0-plan.md # Plan de version
v1.2.0-plan.md # Plan de version
v1.3.0-plan.md # Plan de version
technical/
ARCHITECTURE.md # Ce fichier
decisions.md # ADR
@@ -154,3 +155,8 @@ Decisions cles pour v1.2.0 :
- **ADR-006** : Ajout du module exporter.py (5 modules)
- **ADR-007** : Retry simple plutot que urllib3.Retry
- **ADR-008** : Tri dans display.py, pas dans collector.py
Decisions cles pour v1.3.0 :
- **ADR-009** : Retry HTTP 429 avec Retry-After dans _get_with_retry
- **ADR-010** : Sanitisation des caracteres de controle dans exporter.py
- **ADR-011** : --health comme commande alternative, pas sous-commande

View File

@@ -123,3 +123,47 @@
- Le tri est teste independamment de la collecte
- L'export JSON peut aussi appliquer le tri (via `_sort_repos` importable depuis display)
- Le critere de tri par defaut ("name") garantit un affichage stable entre les executions
## ADR-009 : Retry HTTP 429 avec Retry-After dans _get_with_retry (v1.3.0)
**Date** : 2026-03-12
**Statut** : accepte
**Contexte** : Le retry dans `_get_with_retry` ne gere que `requests.Timeout`. Un HTTP 429 (rate limiting) retourne une reponse HTTP, pas une exception Timeout. Le retry ne se declenche donc pas sur rate limiting.
**Decision** : Etendre `_get_with_retry` pour intercepter les reponses HTTP 429. Respecter le header `Retry-After` (en secondes) si present, sinon utiliser le backoff lineaire standard. Apres epuisement des retries, lever `requests.HTTPError`.
**Consequences** :
- La logique de retry reste centralisee dans une seule methode (coherent avec ADR-007)
- Le header Retry-After est un standard HTTP (RFC 7231), le respecter evite les retries inutiles
- La boucle gere desormais 2 cas (Timeout + 429), complexite accrue mais testable
- Pas de changement d'interface publique
## ADR-010 : Sanitisation des caracteres de controle dans exporter.py (v1.3.0)
**Date** : 2026-03-12
**Statut** : accepte
**Contexte** : L'export JSON peut contenir des caracteres de controle ASCII (0x00-0x1F) provenant des descriptions de repos. Ces caracteres sont invalides en JSON (RFC 8259) et causent des erreurs avec `python3 -m json.tool`.
**Decision** : Sanitiser les champs texte dans `repos_to_dicts()` avant serialisation. Supprimer les caracteres de controle sauf `\n`, `\r` et `\t` (qui sont echappes nativement par `json.dumps`).
**Consequences** :
- La sanitisation est au point de sortie (exporter), pas dans le collecteur
- Les donnees dans RepoData restent brutes (pas de perte pour le rendu Rich)
- Approche defensive contre les donnees inattendues de l'API Gitea
## ADR-011 : --health comme flag optionnel, pas sous-commande (v1.3.0)
**Date** : 2026-03-12
**Statut** : accepte
**Contexte** : L'option `--health` est un mode alternatif au dashboard. Deux approches : flag optionnel ou sous-commande.
**Decision** : Utiliser un flag `--health` dans argparse. Pas de sous-commandes.
**Consequences** :
- Coherent avec ADR-004 (argparse simple)
- Un seul niveau d'arguments
- `--health` est mutuellement exclusif avec le mode dashboard
- Si d'autres modes alternatifs apparaissent, reconsiderer les sous-commandes