- tablesort.js : parseFloat lisait l'année des dates ISO (2026) et mettait toutes les lignes à égalité — comparaison numérique seulement pour les valeurs entièrement numériques. - dashboard.js : pinned-grid rejoint les grilles triées et regroupées (son exclusion rendait les deux outils invisibles là où l'œil se pose) ; dates comparées via Date.parse. - data-first-dir=desc sur Dernière activité / Écart / Zombies.
272 lines
16 KiB
HTML
272 lines
16 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="fr" data-theme="dark">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Kanbante — Dashboard</title>
|
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>📋</text></svg>">
|
|
<link rel="stylesheet" href="/static/css/style.css">
|
|
<script defer src="/static/vendor/alpine.min.js"></script>
|
|
</head>
|
|
<body>
|
|
{% include "_header.html" %}
|
|
|
|
<!-- En-tête de page (refonte 0.20.0) : titre sobre, bande KPI
|
|
unifiée, puis UNE barre d'outils (dans <main>) qui regroupe
|
|
onglets, filtre, tri et vue — fini les rangées éparses. -->
|
|
<section class="page-head">
|
|
<div class="page-head-row">
|
|
<div class="page-title">
|
|
<h1>Projets</h1>
|
|
<p class="page-sub">Vue d'ensemble de vos dépôts Gitea — Gitea reste la source de vérité.</p>
|
|
</div>
|
|
<div class="kpi-strip" aria-label="Indicateurs des 30 derniers jours">
|
|
<div class="kpi"><span class="kpi-value">{{ active_repos|length }}</span><span class="kpi-label">Projets actifs</span></div>
|
|
<a class="kpi" href="/activity"><span class="kpi-value">{{ metrics.closed }}</span><span class="kpi-label">Issues fermées · 30 j</span></a>
|
|
<a class="kpi" href="/activity"><span class="kpi-value">{{ metrics.moved }}</span><span class="kpi-label">Déplacements · 30 j</span></a>
|
|
<div class="kpi"><span class="kpi-value">{% if metrics.avg_cycle_days is not none %}{{ metrics.avg_cycle_days }}<span class="kpi-unit"> j</span>{% else %}—{% endif %}</span><span class="kpi-label">Temps de cycle moyen</span></div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<script>
|
|
// État du dashboard (#21/#22) : la vue grille/liste survit au
|
|
// rechargement (localStorage), lue avant qu'Alpine démarre.
|
|
function dashState() {
|
|
var view = 'grid';
|
|
try { view = localStorage.getItem('kanbante-dash-view') || 'grid'; } catch (e) { /* défaut */ }
|
|
return {
|
|
tab: 'active', search: '', view: view,
|
|
setView: function (v) {
|
|
this.view = v;
|
|
try { localStorage.setItem('kanbante-dash-view', v); } catch (e) { /* non-fatal */ }
|
|
}
|
|
};
|
|
}
|
|
</script>
|
|
<main x-data="dashState()">
|
|
<!-- Barre d'outils unique (0.20.0) : onglets segmentés,
|
|
filtre, tri/regroupement (#21) et vue (#22) alignés.
|
|
Onglets : "actif" = fenêtre glissante ; "archivé" =
|
|
le vrai drapeau Gitea (repo.archived). Le filtre lit
|
|
des data-* échappés par Jinja, jamais des littéraux JS. -->
|
|
<div class="dash-toolbar">
|
|
<div class="seg-tabs" role="tablist" aria-label="État des projets">
|
|
<button type="button" role="tab" :class="{ active: tab === 'active' }" @click="tab = 'active'">Actifs <span class="seg-count">{{ active_repos|length }}</span></button>
|
|
<button type="button" role="tab" :class="{ active: tab === 'inactive' }" @click="tab = 'inactive'">Inactifs <span class="seg-count">{{ inactive_repos|length }}</span></button>
|
|
<button type="button" role="tab" :class="{ active: tab === 'archived' }" @click="tab = 'archived'">Archivés <span class="seg-count">{{ archived_repos|length }}</span></button>
|
|
</div>
|
|
<div class="search-bar toolbar-search">
|
|
<input type="text" id="project-search" placeholder="Filtrer les projets..." x-model="search" autocomplete="off">
|
|
<kbd class="kbd">/</kbd>
|
|
</div>
|
|
<label class="dash-tool">Tri
|
|
<select id="dash-sort">
|
|
<option value="default">Par défaut</option>
|
|
<option value="updated">Activité Gitea récente</option>
|
|
<option value="activity">Activité kanban récente</option>
|
|
<option value="open">Issues ouvertes ↓</option>
|
|
<option value="name">Nom A → Z</option>
|
|
</select>
|
|
</label>
|
|
<label class="dash-tool dash-tool-check">
|
|
<input type="checkbox" id="dash-group"> Grouper par propriétaire
|
|
</label>
|
|
<div class="dash-view-toggle" role="group" aria-label="Mode d'affichage">
|
|
<button type="button" :class="{ active: view === 'grid' }" @click="setView('grid')">▦ Grille</button>
|
|
<button type="button" :class="{ active: view === 'list' }" @click="setView('list')">☰ Liste</button>
|
|
</div>
|
|
</div>
|
|
|
|
{% macro project_card(repo, archived=False) %}
|
|
{% set full = repo.full_name %}
|
|
<div class="project-card{% if archived %} archived{% endif %}"
|
|
data-full="{{ full }}" data-home="{{ repo.home_tab or '' }}"
|
|
data-name="{{ repo.name|lower }}" data-owner="{{ repo.owner.login }}"
|
|
data-updated="{{ repo.updated_at }}" data-open="{{ repo.open_issues_count or 0 }}"
|
|
data-activity="{{ repo.activity.last_at if repo.activity else '' }}"
|
|
data-search="{{ (repo.name ~ ' ' ~ repo.full_name ~ ' ' ~ (repo.description or ''))|lower }}"
|
|
x-show="search === '' || $el.dataset.search.includes(search.toLowerCase())">
|
|
<div class="card-header">
|
|
<span class="org-tag">{{ repo.owner.login }}</span>
|
|
{% if repo.health_dot %}
|
|
<a href="/health/{{ full }}" class="health-dot {{ repo.health_dot.level }}" title="Santé du flux — {{ repo.health_dot.tooltip }}" aria-label="Santé du flux : {{ repo.health_dot.tooltip }}"></a>
|
|
{% endif %}
|
|
<span class="date">{{ repo.updated_at[:10] }}</span>
|
|
{% if not archived %}
|
|
<button type="button" class="star-btn" data-full="{{ full }}" data-starred="{{ 1 if repo.starred else 0 }}" title="{{ 'Désépingler (étoile Gitea)' if repo.starred else 'Épingler (étoile Gitea)' }}" aria-label="Épingler {{ repo.name }}">{{ '⭐' if repo.starred else '☆' }}</button>
|
|
<div class="card-menu-wrap">
|
|
<button type="button" class="card-menu-btn" data-menu-btn aria-haspopup="true" aria-expanded="false" title="Actions rapides pour {{ repo.name }}" aria-label="Actions rapides pour {{ repo.name }}">⋯</button>
|
|
<div class="card-menu" hidden>
|
|
<a href="/board/{{ full }}">📋 Board</a>
|
|
<a href="/capture?repo={{ full|urlencode }}">⚡ Capturer ici</a>
|
|
<a href="/triage/{{ full }}">📥 Triage du backlog</a>
|
|
<a href="/health/{{ full }}">🩺 Santé</a>
|
|
<a href="/sprint/{{ full }}">📖 Récit de sprint</a>
|
|
{% if repo.html_url %}
|
|
<a href="{{ repo.html_url }}" target="_blank" rel="noopener">🔗 Gitea ↗</a>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
<h3>
|
|
<a href="/project/{{ repo.owner.login }}/{{ repo.name }}">{{ repo.name }}</a>
|
|
</h3>
|
|
<p class="desc">{{ repo.description or '—' }}</p>
|
|
{% if full in stats %}
|
|
{% set total = stats[full].values()|sum %}
|
|
{% set done_n = stats[full].get('done', 0) %}
|
|
{% if total > 0 %}
|
|
<div class="progress" title="{{ done_n }}/{{ total }} terminées">
|
|
<span style="width: {{ (100 * done_n / total)|round|int }}%"></span>
|
|
</div>
|
|
{% endif %}
|
|
{% endif %}
|
|
{% if repo.next_milestone %}
|
|
{% set ms = repo.next_milestone %}
|
|
<div class="ms-block">
|
|
<div class="ms-line">
|
|
<a href="/forecast#ms-{{ repo.owner.login }}-{{ repo.name }}" class="ms-title" title="Voir ce jalon dans le tableau des prévisions">🎯 {{ ms.title }}</a>
|
|
{% if ms.due_label %}<span class="ms-due{% if ms.late %} late{% endif %}">{{ '⚠️ ' if ms.late }}échéance {{ ms.due_label }}</span>{% endif %}
|
|
</div>
|
|
{% if ms.total %}
|
|
<div class="progress ms-progress"><span style="width: {{ ms.pct }}%"></span></div>
|
|
<div class="ms-meta">{{ ms.closed }}/{{ ms.total }} terminées{% if ms.eta_label %} · 🔮 {{ ms.eta_label }}{% if ms.eta_late %} — ⚠️ après l'échéance{% endif %}{% endif %}</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
<div class="card-footer">
|
|
<span>{{ repo.open_issues_count or 0 }} issues ouvertes</span>
|
|
<a href="/board/{{ full }}" class="card-board-link">📋 Board</a>
|
|
{% if repo.forecast_label %}
|
|
<span class="forecast-line" title="Prévision d'après le débit réel du journal (fermetures par semaine)">🔮 {{ repo.forecast_label }}</span>
|
|
{% endif %}
|
|
{% if full in stats %}
|
|
<div class="mini-progress">
|
|
{% for col in ['backlog','todo','in_progress','qa','review','done'] %}
|
|
{% set n = stats[full].get(col, 0) %}
|
|
{% if n > 0 %}
|
|
<span class="chip" style="background: {{ COLUMN_COLORS[col] }}20; color: {{ COLUMN_COLORS[col] }}">
|
|
{{ n }}
|
|
</span>
|
|
{% endif %}
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% if repo.activity %}
|
|
<div class="activity-line">
|
|
{% if repo.activity.closed or repo.activity.moved %}
|
|
📈 {{ repo.activity.closed }} fermée(s) · {{ repo.activity.moved }} déplacement(s) cette semaine
|
|
{% elif repo.activity.idle_days is not none and repo.activity.idle_days > 7 %}
|
|
💤 aucune activité kanban depuis {{ repo.activity.idle_days }} j
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endmacro %}
|
|
|
|
{% macro project_row(repo, archived=False) %}
|
|
{% set full = repo.full_name %}
|
|
{% set st = stats.get(full, {}) %}
|
|
<tr data-name="{{ repo.name|lower }}" data-owner="{{ repo.owner.login|lower }}"
|
|
data-open="{{ repo.open_issues_count or 0 }}"
|
|
data-inflight="{{ (st.get('in_progress', 0) + st.get('qa', 0) + st.get('review', 0)) }}"
|
|
data-updated="{{ repo.updated_at }}"
|
|
data-search="{{ (repo.name ~ ' ' ~ repo.full_name ~ ' ' ~ (repo.description or ''))|lower }}"
|
|
x-show="search === '' || $el.dataset.search.includes(search.toLowerCase())">
|
|
<td><a href="/project/{{ full }}"><strong>{{ repo.name }}</strong></a></td>
|
|
<td>{{ repo.owner.login }}</td>
|
|
<td>{{ repo.open_issues_count or 0 }}</td>
|
|
<td>{{ st.get('in_progress', 0) + st.get('qa', 0) + st.get('review', 0) }}</td>
|
|
<td>{% if repo.next_milestone %}🎯 {{ repo.next_milestone.title }}{% if repo.next_milestone.late %} ⚠️{% endif %}{% else %}—{% endif %}</td>
|
|
<td>{% if repo.health_dot %}<a href="/health/{{ full }}" class="health-dot {{ repo.health_dot.level }}" title="{{ repo.health_dot.tooltip }}" aria-label="Santé : {{ repo.health_dot.tooltip }}"></a>{% else %}—{% endif %}</td>
|
|
<td>{{ repo.updated_at[:10] }}</td>
|
|
</tr>
|
|
{% endmacro %}
|
|
|
|
{% macro project_table(repos, tab_name, archived=False) %}
|
|
<div x-show="tab === '{{ tab_name }}'">
|
|
<table class="proj-table">
|
|
<thead>
|
|
<tr>
|
|
<th data-sort="name">Projet</th>
|
|
<th data-sort="owner">Propriétaire</th>
|
|
<th data-sort="open">Ouvertes</th>
|
|
<th data-sort="inflight">En vol</th>
|
|
<th>Prochain jalon</th>
|
|
<th>Santé</th>
|
|
<th data-sort="updated" data-first-dir="desc">Dernière activité</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for repo in repos %}{{ project_row(repo, archived) }}{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
{% if not repos %}
|
|
<div class="empty-state">Aucun projet dans cette vue.</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endmacro %}
|
|
|
|
<div x-show="view === 'grid'">
|
|
<!-- Épinglés (#20) : les dépôts étoilés dans Gitea, hors onglets. -->
|
|
<section id="pinned-section"{% if not pinned_repos %} hidden{% endif %}>
|
|
<h3 class="pinned-title">⭐ Épinglés</h3>
|
|
<div class="project-grid" id="pinned-grid">
|
|
{% for repo in pinned_repos %}{{ project_card(repo) }}{% endfor %}
|
|
</div>
|
|
</section>
|
|
|
|
<div x-show="tab === 'active'" class="project-grid" id="grid-active">
|
|
{% for repo in active_listed %}{{ project_card(repo) }}{% endfor %}
|
|
{% if not active_listed and not pinned_repos %}
|
|
<div class="empty-state">🌱 Aucun projet actif en ce moment. Les projets bougent dès qu'une issue ou un commit arrive dans Gitea.</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div x-show="tab === 'inactive'" class="project-grid" id="grid-inactive">
|
|
{% for repo in inactive_listed %}{{ project_card(repo) }}{% endfor %}
|
|
{% if not inactive_listed %}
|
|
<div class="empty-state">✨ Rien d'endormi : tous vos projets ont eu de l'activité récemment.</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div x-show="tab === 'archived'" class="project-grid" id="grid-archived">
|
|
{% for repo in archived_repos %}{{ project_card(repo, archived=True) }}{% endfor %}
|
|
{% if not archived_repos %}
|
|
<div class="empty-state">🗄️ Aucun projet archivé. L'archivage se fait dans Gitea (Réglages du dépôt).</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Vue liste dense (#22) : mêmes onglets, mêmes données. -->
|
|
<div x-show="view === 'list'">
|
|
{{ project_table(active_repos, 'active') }}
|
|
{{ project_table(inactive_repos, 'inactive') }}
|
|
{{ project_table(archived_repos, 'archived', archived=True) }}
|
|
</div>
|
|
</main>
|
|
|
|
<footer>
|
|
Kanbante v{{ version }} · {{ active_repos|length + inactive_repos|length + archived_repos|length }} projets sur Gitea · actifs = mis à jour depuis {{ active_window_days }} jours
|
|
</footer>
|
|
<script src="/static/js/live.js" defer></script>
|
|
<script>window.KANBANTE_CSRF = "{{ csrf_token }}";</script>
|
|
<script src="/static/js/stars.js" defer></script>
|
|
<script src="/static/js/dashboard.js" defer></script>
|
|
<script src="/static/js/tablesort.js" defer></script>
|
|
<script>
|
|
document.addEventListener('keydown', function (e) {
|
|
if (e.key === '/' && !/INPUT|TEXTAREA/.test(document.activeElement?.tagName || '')) {
|
|
e.preventDefault();
|
|
document.getElementById('project-search')?.focus();
|
|
}
|
|
});
|
|
</script>
|
|
{% include "_palette.html" %}
|
|
<script src="/static/js/palette.js" defer></script>
|
|
</body>
|
|
</html>
|