Tout ce qui est en vol, tous projets actifs confondus — + regroupé par colonne, les cartes les plus anciennes d'abord.
+ +🎉 Rien en vol pour le moment — aucun encours, aucune révision en attente.
+ {% endif %} + + {% for group in groups %} +diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f84ceb..4bbe012 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,33 @@ # Changelog +## 0.34.0 — 2026-10-26 + +### Ajouté — vue « En vol partout » /inflight (issue #24) + +Dernière carte du backlog de la feuille de route « Gestion +de projets », première du classement big picture (pilotage +quotidien des agents avant gestion de projet). + +- **Page /inflight** : tout l'encours (colonnes en vol de + l'espace — En cours, QA, Révision par défaut) **tous + projets actifs confondus, regroupé par colonne** — là où + /focus groupe par projet et /my par assignation. +- Chaque carte : projet (lien board), titre, étiquettes, + assigné, et **âge dans la colonne** (badge ⏳, teinté + selon les seuils d'ancienneté) — les groupes sont **triés + par âge décroissant** : les plus vieilles cartes, le vrai + signal, en premier. +- En-tête : total en vol + compteurs par colonne ; + **limite WIP globale** affichée quand des projets en + définissent (somme par colonne). +- Entrées : menu latéral **Pilotage → 🛩️ En vol** et + palette Ctrl+K. Rafraîchissement doux par le SSE + `/api/stream` comme les autres vues (fermer une issue la + retire au rechargement). +- Réutilise le pipeline du Focus (`_collect_focus_groups`, + `_card_ages`) : aucun appel forge supplémentaire. + + ## 0.33.0 — 2026-10-26 ### Ajouté — filtrage professionnel sur « Mes issues » (/my) diff --git a/app/config.py b/app/config.py index 2508b7a..0626c88 100644 --- a/app/config.py +++ b/app/config.py @@ -177,7 +177,7 @@ from urllib.parse import urlparse BASE_DIR = Path(__file__).resolve().parent.parent -APP_VERSION = "0.33.0" +APP_VERSION = "0.34.0" def _env_bool(name: str, default: bool) -> bool: diff --git a/app/main.py b/app/main.py index 9deeacc..87b60d3 100644 --- a/app/main.py +++ b/app/main.py @@ -2118,6 +2118,7 @@ _NAV_BY_TEMPLATE = { "capture.html": "capture", "digest.html": "digest", "forecast.html": "forecast", + "inflight.html": "inflight", "rules.html": "rules", "health.html": "health", "health_project.html": "health", @@ -4030,6 +4031,88 @@ async def focus(request: Request): ) +async def _collect_inflight(ws_repos, ws_id: int) -> dict: + """The work in flight, grouped BY COLUMN (issue #24). + + Reuses the Focus pipeline (per-project groups of cards in + the workspace's in-flight columns), then pivots: one group + per column, cards sorted by age in column, oldest first — + the oldest cards are the signal. Ages come from the same + _card_ages pipeline as the board. + """ + per_repo = await _collect_focus_groups(ws_repos, ws_id) + columns: dict[str, list[dict]] = { + key: [] for key in bcols.focus_keys(ws_id) + } + for group in per_repo: + full_name = group["full_name"] + cid = group.get("conn_id", registry.default_id) + open_issues = [c["issue"] for c in group["cards"]] + board = get_board_state( + full_name, workspace_id=ws_id, connection_id=cid + ) + ages = _card_ages( + full_name, open_issues, board, + workspace_id=ws_id, connection_id=cid, + ) + for card in group["cards"]: + age = ages.get(card["issue"]["number"]) or {} + columns.setdefault(card["column"], []).append( + { + "issue": card["issue"], + "repo": full_name, + "column": card["column"], + "age_days": age.get("days", 0), + "age_since": age.get("since", ""), + } + ) + groups = [] + total = 0 + for key in bcols.keys(ws_id): + cards = columns.get(key) + if not cards: + continue + cards.sort(key=lambda c: (-c["age_days"], c["repo"], c["issue"]["number"])) + groups.append({"column": key, "cards": cards}) + total += len(cards) + for key, cards in columns.items(): + if key not in {g["column"] for g in groups} and cards: + cards.sort(key=lambda c: (-c["age_days"], c["repo"], c["issue"]["number"])) + groups.append({"column": key, "cards": cards}) + total += len(cards) + # Limites WIP globales lisibles : somme, par colonne, des + # limites définies dans les projets (0.33.0 : lecture seule). + settings = get_settings() + wip_globals: dict[str, int] = {} + for _repo, limits in (settings.wip_limits or {}).items(): + for col, limit in (limits or {}).items(): + try: + wip_globals[col] = wip_globals.get(col, 0) + int(limit) + except (TypeError, ValueError): + continue + wip_globals = {c: v for c, v in wip_globals.items() if v > 0} + return {"groups": groups, "total": total, "wip_globals": wip_globals} + + +@app.get("/inflight", response_class=HTMLResponse) +async def inflight(request: Request): + """Everything in flight, all projects, grouped by column (#24).""" + workspace, _role = _current_workspace(request) + ws_id = workspace["id"] if workspace else 1 + ws_repos, _ws = await _workspace_repos(request) + data = await _collect_inflight(ws_repos, ws_id) + return render( + request, + "inflight.html", + groups=data["groups"], + total=data["total"], + wip_globals=data["wip_globals"], + column_labels=bcols.names(ws_id), + column_colors=bcols.colors(ws_id), + aging_days=get_settings().aging_days, + ) + + def _group_issue_results(issues: list[dict]) -> list[dict]: """Group search results by repo, annotated with the board column.""" by_repo: dict[str, list[dict]] = {} diff --git a/app/templates/_header.html b/app/templates/_header.html index ac96a8e..77b64c9 100644 --- a/app/templates/_header.html +++ b/app/templates/_header.html @@ -20,6 +20,7 @@ ("digest", "/digest", "🧠", "Digest"), ]), ("pilotage", "Pilotage", [ + ("inflight", "/inflight", "🛩️", "En vol"), ("forecast", "/forecast", "🔮", "Prévisions"), ("health", "/health", "🩺", "Santé"), ("rules", "/rules", "⚙️", "Règles"), diff --git a/app/templates/inflight.html b/app/templates/inflight.html new file mode 100644 index 0000000..af63974 --- /dev/null +++ b/app/templates/inflight.html @@ -0,0 +1,81 @@ + + +
+ + +Tout ce qui est en vol, tous projets actifs confondus — + regroupé par colonne, les cartes les plus anciennes d'abord.
+ +🎉 Rien en vol pour le moment — aucun encours, aucune révision en attente.
+ {% endif %} + + {% for group in groups %} +