feat(pilotage): Prévisions en tableau des jalons, Santé en tableau de sévérité (chantier C)

- /forecast : une ligne par jalon, tous projets, triée par
  risque (écart décroissant), filtrable, ancres ms-<owner>-<repo>
  pointées par le bloc jalon des cartes du dashboard ; débit
  par projet conservé en tableau compact.
- /health : tableau trié par sévérité (pastille, zombies,
  goulot) ; page détail : sélecteur de projet (health.js).
- Tri des tableaux mutualisé dans static/js/tablesort.js
  (dashboard, forecast, health).
This commit is contained in:
Spark
2026-10-12 13:53:59 -04:00
parent 3e3187d597
commit 79122c20d0
8 changed files with 209 additions and 62 deletions
+78 -18
View File
@@ -13,13 +13,72 @@
<main class="focus-main">
<section class="briefing-card">
<h1>🔮 Prévisions — « on finit quand ? »</h1>
<p>Réponse honnête, calculée depuis le journal : fermetures réelles par semaine (8 dernières), débit moyen/min/max sur les 4 dernières semaines complètes, et fourchette plutôt que faux chiffre précis. Sans historique de fermetures, aucune prévision n'est affichée.</p>
<p>Le tableau de bord des jalons, <strong>tous projets confondus</strong>, trié par risque : l'écart entre la date estimée (au débit réel du journal) et l'échéance. Réponse honnête — fourchettes plutôt que faux chiffres précis, et aucune prévision sans historique de fermetures.</p>
<p class="forecast-summary">
<span class="chip chip-total">🎯 {{ summary.total }} jalon(s) ouvert(s)</span>
{% if summary.late %}<span class="chip chip-late">⚠️ {{ summary.late }} en retard à ce rythme</span>{% endif %}
{% if summary.ontime %}<span class="chip chip-ok">✅ {{ summary.ontime }} dans les temps</span>{% endif %}
</p>
<div class="search-bar forecast-filter">
<input type="text" id="ms-filter" placeholder="🔍 Filtrer par projet ou jalon..." autocomplete="off">
</div>
</section>
{% for project in projects %}
<section class="briefing-card forecast-card">
<h2>📂 {{ project.full_name }}</h2>
{% if rows %}
<table class="proj-table pilot-table">
<thead>
<tr>
<th data-sort="project">Projet</th>
<th data-sort="title">Jalon</th>
<th data-sort="due">Échéance</th>
<th>Progression</th>
<th>ETA</th>
<th data-sort="gap">Écart</th>
<th data-sort="throughput">Débit / sem.</th>
</tr>
</thead>
<tbody>
{% for row in rows %}
<tr{% if row.anchor %} id="ms-{{ row.owner }}-{{ row.repo }}"{% endif %}
data-project="{{ row.project|lower }}" data-title="{{ row.title|lower }}"
data-due="{{ row.due_sort }}" data-gap="{{ row.gap_days if row.gap_days is not none else '' }}"
data-throughput="{{ row.throughput if row.throughput is not none else '' }}"
data-search="{{ (row.project ~ ' ' ~ row.title)|lower }}">
<td><a href="/project/{{ row.project }}">{{ row.project }}</a></td>
<td><a href="/board/{{ row.project }}?milestone={{ row.title|urlencode }}"><strong>{{ row.title }}</strong></a></td>
<td>{{ row.due_label }}</td>
<td>
{% if row.total %}
<div class="progress ms-progress pilot-progress"><span style="width: {{ row.pct }}%"></span></div>
<span class="ms-meta">{{ row.closed }}/{{ row.total }}</span>
{% else %}—{% endif %}
</td>
<td>{{ row.eta or '—' }}</td>
<td>
{% if row.verdict == 'late' %}
<span class="verdict verdict-late">⚠️ +{{ row.gap_days }} j</span>
{% elif row.verdict == 'ontime' %}
<span class="verdict verdict-ok">✅ dans les temps{% if row.gap_days is not none and row.gap_days < 0 %} ({{ -row.gap_days }} j d'avance){% endif %}</span>
{% elif row.verdict == 'nodue' %}
<span class="verdict">sans échéance</span>
{% else %}
<span class="verdict">pas d'ETA</span>
{% endif %}
</td>
<td>{{ row.throughput if row.throughput is not none else '—' }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<section class="briefing-card"><p>Aucun jalon ouvert dans les projets actifs. Crée des jalons dans Gitea pour piloter par échéances.</p></section>
{% endif %}
<section class="briefing-card">
<h2>📈 Débit par projet</h2>
{% for project in projects %}
<div class="forecast-flex">
<div class="forecast-proj-name">📂 <a href="/project/{{ project.full_name }}">{{ project.full_name }}</a></div>
<svg class="sparkline" viewBox="0 0 168 48" role="img" aria-label="Fermetures par semaine">
{% for bar in project.bars %}
{% set px = (bar.h * 0.42)|round|int %}
@@ -30,25 +89,17 @@
</svg>
<div class="forecast-nums">
{% if project.stats %}
<p>Débit : <strong>{{ project.stats.avg }} / semaine</strong> en moyenne (min {{ project.stats.min }}, max {{ project.stats.max }})</p>
<p>Débit : <strong>{{ project.stats.avg }} / semaine</strong> (min {{ project.stats.min }}, max {{ project.stats.max }})</p>
{% else %}
<p>Débit : <strong>pas assez d'historique</strong> — aucune fermeture récente dans le journal.</p>
<p>Débit : <strong>pas assez d'historique</strong>.</p>
{% endif %}
<p>Restant ouvert : <strong>{{ project.remaining }}</strong>
{% if project.eta_label %} · 🔮 backlog {{ project.eta_label }}{% endif %}</p>
<p>Restant ouvert : <strong>{{ project.remaining }}</strong>{% if project.eta_label %} · 🔮 backlog {{ project.eta_label }}{% endif %}</p>
</div>
</div>
{% if project.milestones %}
<ul class="forecast-milestones">
{% for ms in project.milestones %}
<li>🎯 <strong>{{ ms.title }}</strong> — {{ ms.remaining }} restante(s){% if ms.due_on %} · échéance {{ ms.due_on }}{% endif %}{% if ms.eta %} · 🔮 {{ ms.eta }}{% endif %}{% if ms.verdict %} · {{ ms.verdict }}{% endif %}</li>
{% endfor %}
</ul>
{% endif %}
{% else %}
<p>Aucun projet actif.</p>
{% endfor %}
</section>
{% else %}
<div class="briefing-card"><p>Aucun projet actif.</p></div>
{% endfor %}
</main>
<footer>
<span>Kanbante v{{ version }} · prévisions = journal des fermetures, pas boule de cristal</span>
@@ -56,5 +107,14 @@
{% include "_palette.html" %}
<script src="/static/js/palette.js" defer></script>
<script src="/static/js/live.js" defer></script>
<script src="/static/js/tablesort.js" defer></script>
<script>
document.getElementById('ms-filter')?.addEventListener('input', function (e) {
var q = e.target.value.toLowerCase();
document.querySelectorAll('.pilot-table tbody tr').forEach(function (row) {
row.style.display = !q || (row.dataset.search || '').includes(q) ? '' : 'none';
});
});
</script>
</body>
</html>