feat(health): santé du flux — goulots et cartes zombies (issue #9)

app/flow.py: séjours par colonne reconstruits du journal,
médianes, colonne la plus lente; zombies = aucune activité
(journal/Gitea) depuis KANBANTE_ZOMBIE_DAYS (30). Pages /health
et /health/{o}/{r} + actions rapides (commenter/backlog/fermer).
This commit is contained in:
Spark
2026-10-11 14:49:38 -04:00
parent 1932d11457
commit 6453b53df4
17 changed files with 321 additions and 1 deletions
+1
View File
@@ -26,6 +26,7 @@
<a href="/digest" class="nav-pill">🧠 Digest</a>
<a href="/forecast" class="nav-pill">🔮 Prévisions</a>
<a href="/rules" class="nav-pill">⚙️ Règles</a>
<a href="/health" class="nav-pill">🩺 Santé</a>
<a href="/wall" class="nav-pill">🖥️ Wallboard</a>
<button type="button" class="nav-pill palette-trigger" title="Palette de commandes (Ctrl+K)">⌨️ Ctrl K</button>
</nav>
+1
View File
@@ -28,6 +28,7 @@
<a href="/digest" class="nav-pill">🧠 Digest</a>
<a href="/forecast" class="nav-pill">🔮 Prévisions</a>
<a href="/rules" class="nav-pill">⚙️ Règles</a>
<a href="/health" class="nav-pill">🩺 Santé</a>
<a href="/wall" class="nav-pill">🖥️ Wallboard</a>
<button type="button" class="nav-pill palette-trigger" title="Palette de commandes (Ctrl+K)">⌨️ Ctrl K</button>
{% if repo_info.html_url %}
+1
View File
@@ -25,6 +25,7 @@
<a href="/digest" class="nav-pill">🧠 Digest</a>
<a href="/forecast" class="nav-pill">🔮 Prévisions</a>
<a href="/rules" class="nav-pill">⚙️ Règles</a>
<a href="/health" class="nav-pill">🩺 Santé</a>
<a href="/wall" class="nav-pill">🖥️ Wallboard</a>
<button type="button" class="nav-pill palette-trigger" title="Palette de commandes (Ctrl+K)">⌨️ Ctrl K</button>
</nav>
+1
View File
@@ -23,6 +23,7 @@
<a href="/digest" class="nav-pill">🧠 Digest</a>
<a href="/forecast" class="nav-pill">🔮 Prévisions</a>
<a href="/rules" class="nav-pill">⚙️ Règles</a>
<a href="/health" class="nav-pill">🩺 Santé</a>
<a href="/wall" class="nav-pill">🖥️ Wallboard</a>
<button type="button" class="nav-pill palette-trigger" title="Palette de commandes (Ctrl+K)">⌨️ Ctrl K</button>
</nav>
+1
View File
@@ -26,6 +26,7 @@
<a href="/digest" class="nav-pill">🧠 Digest</a>
<a href="/forecast" class="nav-pill">🔮 Prévisions</a>
<a href="/rules" class="nav-pill">⚙️ Règles</a>
<a href="/health" class="nav-pill">🩺 Santé</a>
<a href="/wall" class="nav-pill">🖥️ Wallboard</a>
<button type="button" class="nav-pill palette-trigger" title="Palette de commandes (Ctrl+K)">⌨️ Ctrl K</button>
</nav>
+1
View File
@@ -25,6 +25,7 @@
<a href="/digest" class="nav-pill">🧠 Digest</a>
<a href="/forecast" class="nav-pill">🔮 Prévisions</a>
<a href="/rules" class="nav-pill">⚙️ Règles</a>
<a href="/health" class="nav-pill">🩺 Santé</a>
<a href="/wall" class="nav-pill">🖥️ Wallboard</a>
<button type="button" class="nav-pill palette-trigger" title="Palette de commandes (Ctrl+K)">⌨️ Ctrl K</button>
<span class="badge">{{ groups|map(attribute='cards')|map('length')|sum }} cartes en cours</span>
+53
View File
@@ -0,0 +1,53 @@
<!DOCTYPE html>
<html lang="fr" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="csrf-token" content="{{ csrf_token }}">
<title>Santé du flux — Kanbante</title>
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<header class="app-header">
<a href="/" class="logo">📋 Kanbante</a>
<nav class="nav-pills">
<a href="/" class="nav-pill">🏠 Projets</a>
<a href="/focus" class="nav-pill">🎯 Focus</a>
<a href="/today" class="nav-pill">🌅 Aujourd'hui</a>
<a href="/activity" class="nav-pill">📰 Activité</a>
<a href="/forecast" class="nav-pill">🔮 Prévisions</a>
<a href="/rules" class="nav-pill">⚙️ Règles</a>
<a href="/health" class="nav-pill active">🩺 Santé</a>
<button type="button" class="nav-pill palette-trigger" title="Palette de commandes (Ctrl+K)">⌨️ Ctrl K</button>
</nav>
</header>
<main class="focus-main">
<section class="briefing-card">
<h1>🩺 Santé du flux</h1>
<p>Les boards montrent l'état ; cette page montre la <strong>santé</strong> : quelle colonne accumule (temps médian passé dans chaque colonne, d'après le journal) et quelles cartes sont devenues des <strong>zombies</strong> (aucune activité depuis le seuil configuré).</p>
</section>
{% for project in projects %}
<section class="briefing-card forecast-card">
<h2>📂 {{ project.full_name }} <small>({{ project.open_count }} ouverte(s))</small></h2>
{% if project.slowest %}
<p>🐌 Colonne la plus lente : <strong>{{ project.column_labels[project.slowest.column] }}</strong> — médiane {{ project.slowest.median_days }} j ({{ project.slowest.count }} séjours mesurés)</p>
{% else %}
<p>🐌 Pas encore assez de séjours mesurés pour nommer un goulot.</p>
{% endif %}
<p>🧟 Zombies (≥ {{ project.zombie_days }} j sans activité) : <strong>{{ project.zombies|length }}</strong>
— <a href="/health/{{ project.full_name }}">voir le détail et agir →</a></p>
</section>
{% else %}
<div class="briefing-card"><p>Aucun projet actif.</p></div>
{% endfor %}
</main>
<footer>
<span>Kanbante v{{ version }} · l'hygiène du flux, mesurée sur le journal — pas au feeling</span>
</footer>
{% include "_palette.html" %}
<script src="/static/js/palette.js" defer></script>
<script src="/static/js/live.js" defer></script>
</body>
</html>
+82
View File
@@ -0,0 +1,82 @@
<!DOCTYPE html>
<html lang="fr" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="csrf-token" content="{{ csrf_token }}">
<title>Santé — {{ full_name }} — Kanbante</title>
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<header class="app-header">
<a href="/" class="logo">📋 Kanbante</a>
<nav class="nav-pills">
<a href="/board/{{ full_name }}" class="nav-pill">📋 Board</a>
<a href="/health" class="nav-pill active">🩺 Santé</a>
<a href="/forecast" class="nav-pill">🔮 Prévisions</a>
<button type="button" class="nav-pill palette-trigger" title="Palette de commandes (Ctrl+K)">⌨️ Ctrl K</button>
</nav>
</header>
<main class="focus-main">
{% if health is none %}
<section class="briefing-card">
<h1>🩺 {{ full_name }}</h1>
<p>Projet introuvable.</p>
</section>
{% else %}
<section class="briefing-card">
<h1>🩺 {{ full_name }}</h1>
<h2>⏱️ Temps passé par colonne (journal)</h2>
{% if health.dwell %}
<table class="health-table">
<thead><tr><th>Colonne</th><th>Séjours mesurés</th><th>Médiane</th><th>Moyenne</th></tr></thead>
<tbody>
{% for row in health.dwell %}
<tr{% if health.slowest and row.column == health.slowest.column %} class="health-slowest"{% endif %}>
<td>{{ health.column_labels[row.column] }}{% if health.slowest and row.column == health.slowest.column %} 🐌{% endif %}</td>
<td>{{ row.count }}</td>
<td>{{ row.median_days }} j</td>
<td>{{ row.avg_days }} j</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if health.slowest %}
<p>🐌 <strong>Goulot : {{ health.column_labels[health.slowest.column] }}</strong> — c'est là que les cartes dorment le plus longtemps (médiane {{ health.slowest.median_days }} j).</p>
{% endif %}
{% else %}
<p>Pas encore de séjours mesurables dans le journal de ce projet — il faut des déplacements journalisés pour calculer les temps de colonne.</p>
{% endif %}
</section>
<section class="briefing-card">
<h2>🧟 Cartes zombies — aucune activité depuis ≥ {{ health.zombie_days }} jours</h2>
{% if health.zombies %}
<ul class="today-list">
{% for z in health.zombies %}
<li>
<span>#{{ z.number }} — {{ z.title }} <em>({{ z.column_label }} · inactive depuis {{ z.days_inactive }} j)</em></span>
<span class="zombie-actions">
<button type="button" class="btn-sm" data-zombie="comment" data-repo="{{ full_name }}" data-number="{{ z.number }}" data-column="{{ z.column }}">💬 Toujours pertinent ?</button>
<button type="button" class="btn-sm" data-zombie="backlog" data-repo="{{ full_name }}" data-number="{{ z.number }}" data-column="{{ z.column }}">↩️ Backlog</button>
<button type="button" class="btn-sm" data-zombie="close" data-repo="{{ full_name }}" data-number="{{ z.number }}" data-column="{{ z.column }}">✅ Fermer</button>
</span>
</li>
{% endfor %}
</ul>
<p class="notes-hint">« Fermer » déplace la carte en Terminé (l'issue est fermée dans Gitea par la synchro habituelle). « Toujours pertinent ? » poste un commentaire du même texte sur l'issue.</p>
{% else %}
<p>Aucune carte zombie. Le flux est propre. ✨</p>
{% endif %}
</section>
{% endif %}
</main>
<footer>
<span>Kanbante v{{ version }} · seuil zombie : KANBANTE_ZOMBIE_DAYS</span>
</footer>
{% include "_palette.html" %}
<script src="/static/js/palette.js" defer></script>
<script src="/static/js/health.js" defer></script>
</body>
</html>
+1
View File
@@ -24,6 +24,7 @@
<a href="/digest" class="nav-pill">🧠 Digest</a>
<a href="/forecast" class="nav-pill">🔮 Prévisions</a>
<a href="/rules" class="nav-pill">⚙️ Règles</a>
<a href="/health" class="nav-pill">🩺 Santé</a>
<a href="/wall" class="nav-pill">🖥️ Wallboard</a>
<button type="button" class="nav-pill palette-trigger" title="Palette de commandes (Ctrl+K)">⌨️ Ctrl K</button>
</nav>
+1
View File
@@ -24,6 +24,7 @@
<a href="/digest" class="nav-pill">🧠 Digest</a>
<a href="/forecast" class="nav-pill">🔮 Prévisions</a>
<a href="/rules" class="nav-pill">⚙️ Règles</a>
<a href="/health" class="nav-pill">🩺 Santé</a>
<a href="/wall" class="nav-pill">🖥️ Wallboard</a>
<button type="button" class="nav-pill palette-trigger" title="Palette de commandes (Ctrl+K)">⌨️ Ctrl K</button>
</nav>
+1
View File
@@ -17,6 +17,7 @@
<a href="/today" class="nav-pill">🌅 Aujourd'hui</a>
<a href="/forecast" class="nav-pill">🔮 Prévisions</a>
<a href="/rules" class="nav-pill">⚙️ Règles</a>
<a href="/health" class="nav-pill">🩺 Santé</a>
<button type="button" class="nav-pill palette-trigger" title="Palette de commandes (Ctrl+K)">⌨️ Ctrl K</button>
</nav>
</header>
+1
View File
@@ -23,6 +23,7 @@
<a href="/digest" class="nav-pill">🧠 Digest</a>
<a href="/forecast" class="nav-pill">🔮 Prévisions</a>
<a href="/rules" class="nav-pill">⚙️ Règles</a>
<a href="/health" class="nav-pill">🩺 Santé</a>
<a href="/wall" class="nav-pill">🖥️ Wallboard</a>
<button type="button" class="nav-pill palette-trigger" title="Palette de commandes (Ctrl+K)">⌨️ Ctrl K</button>
</nav>
+1
View File
@@ -23,6 +23,7 @@
<a href="/digest" class="nav-pill">🧠 Digest</a>
<a href="/forecast" class="nav-pill">🔮 Prévisions</a>
<a href="/rules" class="nav-pill">⚙️ Règles</a>
<a href="/health" class="nav-pill">🩺 Santé</a>
<a href="/wall" class="nav-pill">🖥️ Wallboard</a>
<button type="button" class="nav-pill palette-trigger" title="Palette de commandes (Ctrl+K)">⌨️ Ctrl K</button>
</nav>