Compare commits

..
39 Commits
Author SHA1 Message Date
bruno 7f66775039 Merge pull request 'Icônes monochromes sur tout le site : emoji colorés retirés (#59, suite)' (#70) from icones-monochromes-59 into main
CI / test (push) Successful in 3m58s
2026-10-29 08:27:48 -04:00
bruno 9586e310ba Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 3s
CI / test (pull_request) Failing after 2s
2026-10-29 08:22:00 -04:00
bruno f58339fd77 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 4s
2026-10-29 08:21:57 -04:00
bruno 0fc8673cf8 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 3s
2026-10-29 08:21:54 -04:00
bruno a090824db3 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 2s
2026-10-29 08:21:51 -04:00
bruno 0393a91ff8 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 3s
2026-10-29 08:21:48 -04:00
bruno 50596fd8b2 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 3s
2026-10-29 08:21:45 -04:00
bruno a65e92f6e8 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 1s
2026-10-29 08:21:41 -04:00
bruno 6fd8e9fffe Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 1s
2026-10-29 08:21:38 -04:00
bruno 842268577b Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 2s
2026-10-29 08:21:35 -04:00
bruno b3735e4056 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 0s
2026-10-29 08:21:31 -04:00
bruno ce1bbf0e59 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 2s
2026-10-29 08:21:28 -04:00
bruno 336c79fd8b Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 2s
2026-10-29 08:21:26 -04:00
bruno 7a23aff31b Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 2s
2026-10-29 08:21:23 -04:00
bruno a2ebefbda1 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 2s
2026-10-29 08:21:21 -04:00
bruno 9653f851c8 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 1s
2026-10-29 08:21:17 -04:00
bruno 144957ccd3 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 1s
2026-10-29 08:21:13 -04:00
bruno 204b9ebbe6 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 1s
2026-10-29 08:21:10 -04:00
bruno 05fde96377 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 0s
2026-10-29 08:21:06 -04:00
bruno 7856cdb0c2 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 3s
2026-10-29 08:21:02 -04:00
bruno 9757f3717a Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 3s
2026-10-29 08:20:59 -04:00
bruno 0faabf0078 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 2s
2026-10-29 08:20:56 -04:00
bruno a5c351e0b3 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 3s
2026-10-29 08:20:53 -04:00
bruno f033b18e86 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 1s
2026-10-29 08:20:50 -04:00
bruno 3ade3174bb Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 0s
2026-10-29 08:20:47 -04:00
bruno 44d905c25d Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 1s
2026-10-29 08:20:44 -04:00
bruno 1273924eab Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 1s
2026-10-29 08:20:42 -04:00
bruno fdc42332b2 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 1s
2026-10-29 08:20:40 -04:00
bruno 304f30847a Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 1s
2026-10-29 08:20:38 -04:00
bruno 61739b6115 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 0s
2026-10-29 08:20:35 -04:00
bruno 4a338fea79 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 0s
2026-10-29 08:20:32 -04:00
bruno 659ff36f84 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 1s
2026-10-29 08:20:28 -04:00
bruno 86f216be64 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Successful in 3m58s
2026-10-29 08:20:25 -04:00
bruno 89f4493713 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 3s
2026-10-29 08:20:23 -04:00
bruno 6008f5bb23 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 1s
2026-10-29 08:20:20 -04:00
bruno 0fa828b48c Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 1s
2026-10-29 08:20:19 -04:00
bruno 060a51c32e Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 1s
2026-10-29 08:20:16 -04:00
bruno 0180c610b8 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Successful in 4m8s
2026-10-29 08:20:14 -04:00
bruno fe41eca899 Merge pull request 'Lot UX complet : jetons, boutons, icônes, accessibilité, cartes, thème clair (#57-68)' (#69) from ux-57-68 into main
CI / test (push) Failing after 1s
2026-10-28 23:41:02 -04:00
37 changed files with 232 additions and 188 deletions
+7
View File
@@ -1,3 +1,10 @@
## 0.43.0 — 2026-10-29 — Icônes monochromes sur tout le site (#59, suite)
- **Icônes colorées retirées de toutes les pages** : emoji décoratifs devant titres, boutons et liens supprimés (le texte porte le sens) ; icônes fonctionnelles passées en trait/monochrome — étoile ★/☆ (couleur texte, plus jaune), recherche en SVG, épingler déjà en SVG, thème ☀/☾ en symboles texte.
- **Statuts** : 🟢/🔴/✅/❌ remplacés par le mot (Actif, Désactivé…) ou par ●/✓ monochromes pour les états d'issue ; les pastilles de couleur sémantique (chip-ok, santé, colonnes) sont conservées, ce sont des informations, pas des décorations.
- **Forges** : 🐙/🍵 remplacés par les mots GitHub/Gitea.
- **Hors portée assumé** : barre Fusion partagée non touchée (ses icônes colorées sont l'identité des autres apps — ObsiGate, Flowdeck, TikTak, Spark) ; favicons et contenu utilisateur (titres/labels Gitea) inchangés ; symboles monochromes déjà en place conservés (⋯, ▾, ☰, ↗, ❮❯, ⏻).
## 0.42.0 — 2026-10-28 — Lot UX complet (#57 à #68)
- **#57 Jetons** : échelle fermée documentée (typo xs/sm/base/lg/xl, rayons sm/md/base/full, ombres, espacements) ; rayons pilule passés au jeton `--radius-full`.
+1 -1
View File
@@ -177,7 +177,7 @@ from urllib.parse import urlparse
BASE_DIR = Path(__file__).resolve().parent.parent
APP_VERSION = "0.42.0"
APP_VERSION = "0.43.0"
def _env_bool(name: str, default: bool) -> bool:
+25 -25
View File
@@ -21,38 +21,38 @@
Le wallboard (TV) n'inclut pas ce partiel, volontairement. #}
{% set nav_groups = [
("principal", "Principal", [
("home", "/", "🏠", "Projets"),
("focus", "/focus", "🎯", "Focus"),
("today", "/today", "🌅", "Aujourd'hui"),
("my", "/my", "👤", "Mes issues"),
("home", "/", "", "Projets"),
("focus", "/focus", "", "Focus"),
("today", "/today", "", "Aujourd'hui"),
("my", "/my", "", "Mes issues"),
]),
("rituels", "Rituels", [
("briefing", "/briefing", "📊", "Briefing"),
("standup", "/standup", "🗣️", "Standup"),
("digest", "/digest", "🧠", "Digest"),
("briefing", "/briefing", "", "Briefing"),
("standup", "/standup", "", "Standup"),
("digest", "/digest", "", "Digest"),
]),
("pilotage", "Pilotage", [
("inflight", "/inflight", "🛩️", "En vol"),
("forecast", "/forecast", "🔮", "Prévisions"),
("health", "/health", "🩺", "Santé"),
("rules", "/rules", "⚙️", "Règles"),
("activity", "/activity", "📰", "Activité"),
("inflight", "/inflight", "", "En vol"),
("forecast", "/forecast", "", "Prévisions"),
("health", "/health", "", "Santé"),
("rules", "/rules", "", "Règles"),
("activity", "/activity", "", "Activité"),
]),
("outils", "Outils", [
("search", "/search", "", "Recherche"),
("capture", "/capture", "⚡", "Capture"),
("archives", "/archives", "🗄️", "Archives"),
("wall", "/wall", "🖥️", "Wallboard"),
("capture", "/capture", "", "Capture"),
("archives", "/archives", "", "Archives"),
("wall", "/wall", "", "Wallboard"),
]),
] %}
{% if is_admin %}
{% set nav_groups = nav_groups + [
("admin", "Administration", [
("admin", "/admin", "🛠️", "Vue d'ensemble"),
("admin_connections", "/admin/connections", "🔌", "Connexions"),
("admin_workspaces", "/admin/workspaces", "🧭", "Espaces"),
("admin_columns", "/admin/columns", "🧱", "Colonnes"),
("admin_settings", "/admin/settings", "🎛️", "Paramètres"),
("admin", "/admin", "", "Vue d'ensemble"),
("admin_connections", "/admin/connections", "", "Connexions"),
("admin_workspaces", "/admin/workspaces", "", "Espaces"),
("admin_columns", "/admin/columns", "", "Colonnes"),
("admin_settings", "/admin/settings", "", "Paramètres"),
]),
] %}
{% endif %}
@@ -127,7 +127,7 @@
{% endfor %}
{% if gkey == 'admin' %}
<button type="button" class="side-link theme-toggle-link" id="theme-toggle" title="Basculer entre thème sombre et clair" aria-label="Basculer entre thème sombre et clair">
<span class="side-ico" data-theme-ico>☀️</span>
<span class="side-ico" data-theme-ico>☀</span>
<span class="side-label" data-theme-label>Mode clair</span>
</button>
{% endif %}
@@ -138,14 +138,14 @@
</nav>
<div class="side-foot">
{% if data_freshness %}
<span class="side-fresh side-label" title="Dernière actualisation des données Gitea (instantanés d'arrière-plan)">🕐 {{ data_freshness }}</span>
<span class="side-fresh side-label" title="Dernière actualisation des données Gitea (instantanés d'arrière-plan)">{{ data_freshness }}</span>
{% endif %}
{% if current_user %}
<a href="/profile" class="side-logout" title="Mon profil">👤<span class="side-label"> {{ current_user.display_name or current_user.username }}</span></a>
<a href="/profile" class="side-logout" title="Mon profil"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true" style="width:18px;height:18px;display:inline-block;vertical-align:-3px"><circle cx="12" cy="8" r="3.5"/><path d="M4 20c0-3.5 3.5-5.5 8-5.5s8 2 8 5.5"/></svg><span class="side-label"> {{ current_user.display_name or current_user.username }}</span></a>
{% endif %}
<span class="side-version side-label">v{{ version }}</span>
{% if not is_admin %}
<button type="button" class="theme-toggle-foot" id="theme-toggle" title="Basculer entre thème sombre et clair" aria-label="Basculer entre thème sombre et clair"><span data-theme-ico>☀️</span></button>
<button type="button" class="theme-toggle-foot" id="theme-toggle" title="Basculer entre thème sombre et clair" aria-label="Basculer entre thème sombre et clair"><span data-theme-ico>☀</span></button>
{% endif %}
{% if auth_enabled %}
<a href="/logout" class="side-logout" title="Se déconnecter">⏻<span class="side-label"> Sortir</span></a>
@@ -162,7 +162,7 @@
{% endif %}
<div class="header-actions">
{% if repo_info is defined and repo_info and repo_info.html_url %}
<a href="{{ repo_info.html_url }}" target="_blank" rel="noopener" class="nav-pill" title="Ouvrir ce dépôt dans Gitea">🔗 Gitea ↗</a>
<a href="{{ repo_info.html_url }}" target="_blank" rel="noopener" class="nav-pill" title="Ouvrir ce dépôt dans Gitea">Gitea ↗</a>
{% endif %}
</div>
<script src="/static/js/nav.js?v={{ version }}" defer></script>
+1 -1
View File
@@ -1,7 +1,7 @@
<div class="palette-overlay" id="palette-overlay" hidden>
<div class="palette" role="dialog" aria-modal="true" aria-label="Palette de commandes">
<div class="palette-input-row">
<span class="palette-icon">⌨️</span>
<span class="palette-icon"></span>
<input id="palette-input" type="text" autocomplete="off" spellcheck="false"
placeholder="Chercher un projet, une issue, une action…">
<kbd>Échap</kbd>
+10 -10
View File
@@ -15,11 +15,11 @@
<h1>Administration</h1>
<p class="page-sub">Utilisateurs, vue d'ensemble et journal d'administration.</p>
</div>
{% if msg %}<p class="chip-ok">✅ {{ msg }}</p>{% endif %}
{% if msg %}<p class="chip-ok">{{ msg }}</p>{% endif %}
{% if err %}<p class="chip-warn">{{ err }}</p>{% endif %}
<section class="briefing-card">
<h2>👥 Utilisateurs ({{ users|length }})</h2>
<h2>Utilisateurs ({{ users|length }})</h2>
<table class="proj-table">
<thead>
<tr><th>Utilisateur</th><th>Rôle</th><th>État</th><th>Draco</th><th>Dernière connexion</th><th>Actions</th></tr>
@@ -37,8 +37,8 @@
</select>
</form>
</td>
<td>{% if u.active %}🟢 Actif{% else %}🔴 Désactivé{% endif %}</td>
<td>{% if u.draco_sub %}🔗 lié{% else %}—{% endif %}</td>
<td>{% if u.active %}Actif{% else %}Désactivé{% endif %}</td>
<td>{% if u.draco_sub %}lié{% else %}—{% endif %}</td>
<td>{{ u.last_login_at or '—' }}</td>
<td class="admin-actions">
<form method="post" action="/admin/users/{{ u.id }}/toggle" class="inline-form">
@@ -56,7 +56,7 @@
</tbody>
</table>
<h3>➕ Nouvel utilisateur</h3>
<h3>Nouvel utilisateur</h3>
<form method="post" action="/admin/users" class="admin-form">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<label>Nom d'utilisateur <input type="text" name="username" required autocomplete="off"></label>
@@ -74,17 +74,17 @@
</section>
<section class="briefing-card">
<h2>🔭 Vue d'ensemble</h2>
<h2>Vue d'ensemble</h2>
<p>
<a href="/admin/connections">🔌 Connexions ({{ connections|length }})</a> ·
<a href="/admin/workspaces">🧭 Espaces ({{ workspaces|length }})</a> ·
<a href="/admin/settings">🎛️ Paramètres</a> ·
<a href="/admin/connections">Connexions ({{ connections|length }})</a> ·
<a href="/admin/workspaces">Espaces ({{ workspaces|length }})</a> ·
<a href="/admin/settings">Paramètres</a> ·
<a href="/archives">Archives</a>
</p>
</section>
<section class="briefing-card">
<h2>📜 Journal d'administration</h2>
<h2>Journal d'administration</h2>
{% if audit %}
<table class="proj-table">
<thead><tr><th>Quand</th><th>Qui</th><th>Action</th><th>Détails</th></tr></thead>
+4 -4
View File
@@ -15,7 +15,7 @@
<h1>Colonnes du board</h1>
<p class="page-sub">Espace : <strong>{{ workspace.name }}</strong> (change d'espace par le sélecteur en haut pour régler un autre board). Les colonnes sont propres à chaque espace : nom, couleur, ordre et <strong>mapping étiquette ↔ colonne</strong>. La <strong>clé</strong> d'une colonne ne change jamais (les cartes y sont rattachées) ; le <strong>nom affiché</strong>, lui, est libre.</p>
</div>
{% if msg %}<p class="chip-ok">✅ {{ msg }}</p>{% endif %}
{% if msg %}<p class="chip-ok">{{ msg }}</p>{% endif %}
{% if err %}<p class="chip-warn">{{ err }}</p>{% endif %}
<section class="briefing-card">
@@ -75,10 +75,10 @@
{% endfor %}
<section class="briefing-card">
<h2>➕ Nouvelle colonne</h2>
<h2>Nouvelle colonne</h2>
<form method="post" action="/admin/columns" class="admin-form">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<label>Nom affiché <input type="text" name="name" required placeholder="🛠️ En développement"></label>
<label>Nom affiché <input type="text" name="name" required placeholder="En développement"></label>
<label>Couleur <input type="color" name="color" value="#7c5cfc"></label>
<label>Étiquette mappée <input type="text" name="label" placeholder="(défaut : kanban:<clé>)"></label>
<label>Alias (virgules) <input type="text" name="aliases" placeholder=""></label>
@@ -87,7 +87,7 @@
</section>
<section class="briefing-card">
<h2>↩️ Réinitialiser</h2>
<h2>Réinitialiser</h2>
<p class="notes-hint">Revenir aux six colonnes historiques de cet espace. Les cartes placées dans des colonnes personnalisées retomberont au Backlog à l'affichage.</p>
<form method="post" action="/admin/columns/reset" class="inline-form" onsubmit="return confirm('Réinitialiser les colonnes de cet espace aux valeurs par défaut ?');">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
+3 -3
View File
@@ -15,12 +15,12 @@
<h1>Connexions aux forges</h1>
<p class="page-sub">Chaque connexion est un accès Gitea ou GitHub. Les jetons sont <strong>chiffrés au repos</strong> et ne sont <strong>jamais réaffichés</strong> — pour en changer un, saisis la nouvelle valeur.</p>
</div>
{% if msg %}<p class="chip-ok">✅ {{ msg }}</p>{% endif %}
{% if msg %}<p class="chip-ok">{{ msg }}</p>{% endif %}
{% if err %}<p class="chip-warn">{{ err }}</p>{% endif %}
{% for c in connections %}
<section class="briefing-card">
<h2>{% if c.kind == 'github' %}🐙{% else %}🍵{% endif %} {{ c.name }}
<h2>{% if c.kind == 'github' %}GitHub{% else %}Gitea{% endif %} {{ c.name }}
<span class="muted">({{ c.slug }})</span>
{% if c.owner_user_id %}<span class="chip-warn">personnelle</span>{% endif %}
{% if not c.enabled %}<span class="chip-warn">désactivée</span>{% endif %}
@@ -56,7 +56,7 @@
{% endfor %}
<section class="briefing-card">
<h2>➕ Nouvelle connexion</h2>
<h2>Nouvelle connexion</h2>
<form method="post" action="/admin/connections" class="admin-form">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<label>Type
+1 -1
View File
@@ -15,7 +15,7 @@
<h1>Paramètres</h1>
<p class="page-sub"><strong>L'environnement amorce, le panneau gouverne.</strong> Une valeur saisie ici remplace celle de l'environnement (sans redémarrage pour la plupart) ; un champ vidé revient à la valeur d'environnement. Les secrets et les réglages d'amorçage (base de données, clé maître, session, draco) restent en environnement, volontairement.</p>
</div>
{% if msg %}<p class="chip-ok">✅ {{ msg }}</p>{% endif %}
{% if msg %}<p class="chip-ok">{{ msg }}</p>{% endif %}
{% if err %}<p class="chip-warn">{{ err }}</p>{% endif %}
<section class="briefing-card">
+6 -6
View File
@@ -15,7 +15,7 @@
<h1>Espaces de travail</h1>
<p class="page-sub">Un espace regroupe des connexions (forges) et des membres. Le board d'un projet appartient à l'espace : deux espaces peuvent ranger le même projet différemment. L'état de board, les notes et les épingles du jour sont propres à chaque espace.</p>
</div>
{% if msg %}<p class="chip-ok">✅ {{ msg }}</p>{% endif %}
{% if msg %}<p class="chip-ok">{{ msg }}</p>{% endif %}
{% if err %}<p class="chip-warn">{{ err }}</p>{% endif %}
{% for d in details %}
@@ -30,19 +30,19 @@
<button type="submit" class="btn-save btn-inline">Renommer</button>
</form>
<h3>🔌 Connexions de l'espace</h3>
<h3>Connexions de l'espace</h3>
<form method="post" action="/admin/workspaces/{{ ws.id }}/connections" class="admin-form">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
{% for c in all_connections %}
<label>
<input type="checkbox" name="connection_id" value="{{ c.id }}"{% if c.id in d.conn_ids %} checked{% endif %}>
{% if c.kind == 'github' %}🐙{% else %}🍵{% endif %} {{ c.name }} <span class="muted">({{ c.slug }})</span>
{% if c.kind == 'github' %}GitHub{% else %}Gitea{% endif %} {{ c.name }} <span class="muted">({{ c.slug }})</span>
</label>
{% endfor %}
<button type="submit" class="btn-save btn-inline">Enregistrer</button>
</form>
<h3>👥 Membres</h3>
<h3>Membres</h3>
{% if d.members %}
<table class="proj-table">
<thead><tr><th>Utilisateur</th><th>Rôle dans l'espace</th><th></th></tr></thead>
@@ -81,7 +81,7 @@
<button type="submit" class="btn-save btn-inline">Ajouter / mettre à jour</button>
</form>
<h3>📦 Sélection de projets</h3>
<h3>Sélection de projets</h3>
<p class="notes-hint">Vide = <strong>tous</strong> les projets des connexions de l'espace. Sinon, une ligne par projet au format <code>slug-connexion:proprietaire/depot</code> (ex. <code>github:bruno/mon-projet</code>).</p>
<form method="post" action="/admin/workspaces/{{ ws.id }}/projects">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
@@ -101,7 +101,7 @@
{% if is_global_admin %}
<section class="briefing-card">
<h2>➕ Nouvel espace</h2>
<h2>Nouvel espace</h2>
<form method="post" action="/admin/workspaces" class="admin-form">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<label>Nom <input type="text" name="name" required placeholder="Open source"></label>
+4 -4
View File
@@ -15,7 +15,7 @@
<h1>Archives</h1>
<p class="page-sub">Espace : <strong>{{ workspace.name if workspace else '—' }}</strong>. Deux sortes d'archives : <strong>locale</strong> (le projet disparaît des vues de cet espace, la forge n'est pas touchée, réversible en un clic) et <strong>sur la forge</strong> (propriété du dépôt, lecture seule là-bas). Les archives sortent des rituels et des instantanés, mais restent cherchables.</p>
</div>
{% if msg %}<p class="chip-ok">✅ {{ msg }}</p>{% endif %}
{% if msg %}<p class="chip-ok">{{ msg }}</p>{% endif %}
{% if err %}<p class="chip-warn">{{ err }}</p>{% endif %}
{% macro actions(row, kind) %}
@@ -74,7 +74,7 @@
{% macro rowline(row, kind) %}
<tr>
<td>
{% if row.conn_kind == 'github' %}🐙{% else %}🍵{% endif %}
{% if row.conn_kind == 'github' %}GitHub{% else %}Gitea{% endif %}
<strong>{{ row.full_name }}</strong>
{% if row.conn_slug %}<span class="conn-badge">{{ row.conn_slug }}</span>{% endif %}
{% if row.missing %}<span class="chip-warn">introuvable sur la forge</span>{% endif %}
@@ -86,7 +86,7 @@
{% endmacro %}
<section class="briefing-card">
<h2>💤 À archiver ? (inactifs depuis {{ suggest_days }} jours et plus)</h2>
<h2>À archiver ? (inactifs depuis {{ suggest_days }} jours et plus)</h2>
{% if groups.suggested %}
<form method="post" action="/archives/local-bulk">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
@@ -96,7 +96,7 @@
{% for row in groups.suggested %}
<tr>
<td><input type="checkbox" name="item" value="{{ row.conn_id }}:{{ row.full_name }}" checked></td>
<td>{% if row.conn_kind == 'github' %}🐙{% else %}🍵{% endif %} <strong>{{ row.full_name }}</strong> {% if row.conn_slug %}<span class="conn-badge">{{ row.conn_slug }}</span>{% endif %}</td>
<td>{% if row.conn_kind == 'github' %}GitHub{% else %}Gitea{% endif %} <strong>{{ row.full_name }}</strong> {% if row.conn_slug %}<span class="conn-badge">{{ row.conn_slug }}</span>{% endif %}</td>
<td>{{ row.updated_at }} <span class="muted">(il y a {{ row.idle_days }} j)</span></td>
<td>{% if row.open_issues %}{{ row.open_issues }} ouverte(s){% else %}0{% endif %}</td>
<td class="admin-actions">{{ actions(row, 'suggested') }}</td>
+17 -17
View File
@@ -16,7 +16,7 @@
{% if milestones %}
<div class="milestone-bar">
<span class="milestone-label">🎯 Milestone</span>
<span class="milestone-label">Milestone</span>
<a href="/board/{{ owner }}/{{ repo }}" class="milestone-chip{% if not milestone_param %} active{% endif %}">Toutes</a>
{% for m in milestones %}
<a href="/board/{{ owner }}/{{ repo }}?milestone={{ m.title|urlencode }}"
@@ -27,15 +27,15 @@
{% if milestone_progress %}
<div class="milestone-banner">
<div class="milestone-info">
<strong>🎯 {{ milestone_progress.title }}</strong>
<strong>{{ milestone_progress.title }}</strong>
{% if milestone_progress.due_on %}
<span class="milestone-due">📅 échéance {{ milestone_progress.due_on[:10] }}</span>
<span class="milestone-due">échéance {{ milestone_progress.due_on[:10] }}</span>
{% endif %}
<span class="milestone-count">{{ milestone_progress.closed }}/{{ milestone_progress.total }} issues terminées</span>
{% if milestone_forecast %}
<span class="milestone-forecast">🔮 {{ milestone_forecast }}</span>
<span class="milestone-forecast">{{ milestone_forecast }}</span>
{% endif %}
<a class="milestone-sprint" href="/sprint/{{ full_name }}?milestone={{ milestone_param|urlencode }}">📖 Récit de sprint</a>
<a class="milestone-sprint" href="/sprint/{{ full_name }}?milestone={{ milestone_param|urlencode }}">Récit de sprint</a>
</div>
<div class="progress progress-lg"><span style="width: {{ milestone_progress.pct }}%"></span></div>
<span class="milestone-pct">{{ milestone_progress.pct }}%</span>
@@ -51,12 +51,12 @@
<option value="{{ label.name }}">{{ label.name }}</option>
{% endfor %}
</select>
<button type="button" id="labels-btn" class="btn-sm">🏷️ Gérer les étiquettes</button>
<button type="button" id="labels-btn" class="btn-sm">Gérer les étiquettes</button>
<a href="/triage/{{ full_name }}" class="btn-sm" style="text-decoration:none">Triage du backlog</a>
{% if is_admin or workspace_role == 'admin' %}<a href="/admin/columns" class="btn-sm" style="text-decoration:none" title="Personnaliser les colonnes de cet espace">🧱 Colonnes</a>{% endif %}
{% if is_admin or workspace_role == 'admin' %}<a href="/admin/columns" class="btn-sm" style="text-decoration:none" title="Personnaliser les colonnes de cet espace">Colonnes</a>{% endif %}
<form id="create-issue-form" class="composer">
<div class="composer-head">
<span class="composer-title">➕ Nouvelle issue</span>
<span class="composer-title">Nouvelle issue</span>
<span class="composer-target">arrive dans
<strong>{{ column_labels.backlog }}</strong>
</span>
@@ -102,7 +102,7 @@
<aside class="notes-panel" id="notes-panel">
<div class="notes-resize" id="notes-resize" title="Glisser pour ajuster la largeur"></div>
<div class="notes-head">
<h3>📝 Notes du projet</h3>
<h3>Notes du projet</h3>
<button type="button" id="notes-toggle" class="btn-sm notes-toggle"
title="Replier / déplier le panneau">⤢</button>
</div>
@@ -114,7 +114,7 @@
placeholder="Notes, idées, liens...">{{ notes }}</textarea>
<div class="notes-foot">
<span id="notes-status" class="notes-status"></span>
<button type="submit" class="btn-save">💾 Sauvegarder</button>
<button type="submit" class="btn-save">Sauvegarder</button>
</div>
</form>
</div>
@@ -125,7 +125,7 @@
<div id="label-modal" class="modal" hidden>
<div class="modal-box">
<div class="detail-head">
<h2>🏷️ Étiquettes de {{ full_name }}</h2>
<h2>Étiquettes de {{ full_name }}</h2>
<button id="label-modal-close" class="btn-sm" type="button">✕</button>
</div>
<p class="notes-hint">Ces étiquettes vivent dans Gitea : les créer ou les supprimer ici les modifie pour tout le dépôt.</p>
@@ -161,29 +161,29 @@
<div id="detail-code" class="detail-code"></div>
<div id="detail-body" class="detail-body"></div>
<div class="detail-actions">
<button id="detail-edit-btn" class="btn-sm" type="button">✏️ Modifier le titre et la description</button>
<button id="detail-edit-btn" class="btn-sm" type="button">Modifier le titre et la description</button>
</div>
<form id="detail-edit-form" hidden>
<input type="text" id="detail-title-input" maxlength="255" placeholder="Titre">
<textarea id="detail-body-input" rows="10" maxlength="100000"
placeholder="Description (markdown)..."></textarea>
<div class="detail-actions">
<button type="submit" class="btn-save btn-inline">💾 Enregistrer</button>
<button type="submit" class="btn-save btn-inline">Enregistrer</button>
<button type="button" id="detail-edit-cancel" class="btn-sm">Annuler</button>
</div>
</form>
<h3 class="detail-comments-title">📎 Pièces jointes</h3>
<h3 class="detail-comments-title">Pièces jointes</h3>
<div id="detail-assets"></div>
<form id="asset-form" class="asset-form">
<input type="file" id="asset-file">
<button type="submit" class="btn-sm">📎 Joindre le fichier</button>
<button type="submit" class="btn-sm">Joindre le fichier</button>
</form>
<h3 class="detail-comments-title">💬 Commentaires</h3>
<h3 class="detail-comments-title">Commentaires</h3>
<div id="detail-comments"></div>
<form id="comment-form">
<textarea id="comment-body" rows="3" maxlength="10000"
placeholder="Écrire un commentaire (markdown)..."></textarea>
<button type="submit" class="btn-save">💬 Commenter dans Gitea</button>
<button type="submit" class="btn-save">Commenter dans Gitea</button>
</form>
<p class="detail-foot"><a id="detail-gitea-link" href="#" target="_blank">Ouvrir dans Gitea ↗</a></p>
</aside>
+4 -4
View File
@@ -22,17 +22,17 @@
</div>
<section class="briefing-card">
<h2>📄 Contenu du briefing</h2>
<h2>Contenu du briefing</h2>
<pre class="briefing-text">{{ text }}</pre>
</section>
<section class="briefing-card">
<h2>📲 Envoi ntfy</h2>
<h2>Envoi ntfy</h2>
{% if ntfy_configured %}
<p class="notes-hint">ntfy est configuré. {% if briefing_time %}Envoi automatique chaque jour à <strong>{{ briefing_time }}</strong> (heure du serveur).{% else %}Aucune heure d'envoi automatique (<code>KANBANTE_BRIEFING_TIME</code> non défini).{% endif %}{% if last_sent %} Dernier envoi : {{ last_sent }}.{% endif %}</p>
<form id="briefing-send-form">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<button type="submit" class="btn-save">📤 Envoyer maintenant via ntfy</button>
<button type="submit" class="btn-save">Envoyer maintenant via ntfy</button>
</form>
<p id="briefing-status" class="notes-hint"></p>
{% else %}
@@ -52,7 +52,7 @@
status.textContent = 'Envoi en cours...';
const r = await fetch('/api/briefing/send', { method: 'POST', body: new FormData(e.target) });
const data = await r.json().catch(() => ({}));
status.textContent = data.ok ? '✅ Briefing envoyé sur ntfy.' : '❌ ' + (data.error || ('HTTP ' + r.status));
status.textContent = data.ok ? 'Briefing envoyé sur ntfy.' : '' + (data.error || ('HTTP ' + r.status));
});
</script>
{% include "_palette.html" %}
+6 -6
View File
@@ -18,7 +18,7 @@
{% if created %}
<div class="briefing-card">
<h2>✅ Capturé !</h2>
<h2>Capturé !</h2>
<p>Issue <strong>#{{ created.number }}</strong> créée dans
<strong>{{ created.repo_full_name }}</strong> (colonne {{ created.column }}) :
{% if created.html_url %}<a href="{{ created.html_url }}" target="_blank">{{ created.title }}</a>{% else %}{{ created.title }}{% endif %}
@@ -27,13 +27,13 @@
{% endif %}
{% if error %}
<div class="briefing-card">
<h2>❌ Capture impossible</h2>
<h2>Capture impossible</h2>
<p>{{ error }}</p>
</div>
{% endif %}
<div class="briefing-card">
<h2>✍️ Saisie rapide</h2>
<h2>Saisie rapide</h2>
<form method="post" action="/capture" class="capture-form">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<label>Dépôt
@@ -56,12 +56,12 @@
<option value="todo">À faire</option>
</select>
</label>
<button type="submit" class="btn-save">⚡ Capturer</button>
<button type="submit" class="btn-save">Capturer</button>
</form>
</div>
<div class="briefing-card">
<h2>🔌 Depuis un script ou un raccourci</h2>
<h2>Depuis un script ou un raccourci</h2>
{% if api_enabled %}
<p>L'endpoint <code>POST /api/capture</code> est <strong>actif</strong> (jeton de capture configuré). Exemples :</p>
<pre class="capture-pre">curl -X POST https://votre-kanbante/api/capture \
@@ -75,7 +75,7 @@
</div>
<div class="briefing-card">
<h2>📱 Depuis le téléphone (ntfy)</h2>
<h2>Depuis le téléphone (ntfy)</h2>
{% if ntfy_topic %}
<p>Écoute active sur le topic <code>{{ ntfy_topic }}</code> : chaque message publié devient une issue dans <code>{{ capture_repo or "— (KANBANTE_CAPTURE_REPO manquant)" }}</code>. La première ligne fait le titre. Publie depuis l'application ntfy ou :</p>
<pre class="capture-pre">curl -d "Idée : relire le rapport\n\nDétails…" {{ ntfy_url }}/{{ ntfy_topic }}</pre>
+14 -14
View File
@@ -92,25 +92,25 @@
x-show="search === '' || $el.dataset.search.includes(search.toLowerCase())">
<div class="card-header">
<span class="org-tag">{{ repo.owner.login }}</span>
{% if multi_conn %}<span class="conn-badge" title="Connexion {{ repo._conn_slug }}">{% if repo._conn_kind == 'github' %}🐙{% else %}🍵{% endif %} {{ repo._conn_slug }}</span>{% endif %}
{% if multi_conn %}<span class="conn-badge" title="Connexion {{ repo._conn_slug }}">{% if repo._conn_kind == 'github' %}GitHub{% else %}Gitea{% endif %} {{ repo._conn_slug }}</span>{% endif %}
{% 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>
<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="/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>
<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>
<a href="{{ repo.html_url }}" target="_blank" rel="noopener">Gitea ↗</a>
<div class="card-menu-sep" role="separator"></div>
<button type="button" class="card-menu-item" data-copy-url="{{ repo.html_url }}" title="Copier l'URL Gitea de {{ repo.name }}" aria-label="Copier l'URL du projet {{ repo.name }}">🔗 Copier l'URL du projet</button>
<button type="button" class="card-menu-item" data-copy-url="{{ repo.html_url }}" title="Copier l'URL Gitea de {{ repo.name }}" aria-label="Copier l'URL du projet {{ repo.name }}">Copier l'URL du projet</button>
{% endif %}
</div>
</div>
@@ -133,12 +133,12 @@
{% 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>
<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>
<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 %}
@@ -146,7 +146,7 @@
<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>
<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">
@@ -164,9 +164,9 @@
{% 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
{{ 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
aucune activité kanban depuis {{ repo.activity.idle_days }} j
{% endif %}
</div>
{% endif %}
@@ -186,7 +186,7 @@
<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.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>
@@ -219,7 +219,7 @@
<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>
<h3 class="pinned-title">Épinglés</h3>
<div class="project-grid" id="pinned-grid">
{% for repo in pinned_repos %}{{ project_card(repo) }}{% endfor %}
</div>
+8 -8
View File
@@ -15,8 +15,8 @@
<h1>Digest hebdo → ObsiGate</h1>
<p>Chaque semaine, Kanbante compose le résumé de l'activité (livré, en cours, chiffres du journal) en une note markdown.
L'envoi vers ObsiGate est une écriture dans un autre système : <strong>il n'a jamais lieu sans ton approbation ici</strong>.</p>
{% if last_result %}<p>🧾 Dernier envoi : <code>{{ last_result }}</code></p>{% endif %}
{% if digest_time %}<p>🕐 Composition automatique : jour {{ digest_weekday }} à {{ digest_time }} (notification ntfy, envoi manuel ensuite).</p>{% endif %}
{% if last_result %}<p>Dernier envoi : <code>{{ last_result }}</code></p>{% endif %}
{% if digest_time %}<p>Composition automatique : jour {{ digest_weekday }} à {{ digest_time }} (notification ntfy, envoi manuel ensuite).</p>{% endif %}
</section>
{% if banner %}
@@ -25,7 +25,7 @@
{% if tools is not none %}
<div class="briefing-card">
<h2>🛠️ Outils du serveur MCP ObsiGate</h2>
<h2>Outils du serveur MCP ObsiGate</h2>
{% if tools %}
<ul>
{% for tool in tools %}<li><code>{{ tool }}</code></li>{% endfor %}
@@ -38,29 +38,29 @@
{% endif %}
<div class="briefing-card">
<h2>📤 Livraison</h2>
<h2>Livraison</h2>
<p>
ObsiGate (MCP) : {% if obsigate_configured %}<strong>configuré</strong> — <code>{{ obsigate_url }}</code>, outil <code>{{ obsigate_tool or "(à définir)" }}</code>{% else %}non configuré (<code>KANBANTE_OBSIGATE_MCP_URL</code>){% endif %}<br>
Webhook générique : {% if webhook_configured %}<strong>configuré</strong>{% else %}non configuré (<code>KANBANTE_DIGEST_WEBHOOK_URL</code>){% endif %}
</p>
<form method="post" action="/digest/probe" style="display:inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<button type="submit" class="btn-save">🔌 Tester la connexion MCP</button>
<button type="submit" class="btn-save">Tester la connexion MCP</button>
</form>
<form method="post" action="/digest/send" style="display:inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<input type="hidden" name="target" value="obsigate">
<button type="submit" class="btn-save">🧠 Envoyer vers ObsiGate</button>
<button type="submit" class="btn-save">Envoyer vers ObsiGate</button>
</form>
<form method="post" action="/digest/send" style="display:inline">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<input type="hidden" name="target" value="webhook">
<button type="submit" class="btn-save">🔗 Envoyer au webhook</button>
<button type="submit" class="btn-save">Envoyer au webhook</button>
</form>
</div>
<div class="briefing-card">
<h2>👀 Aperçu du digest de la semaine</h2>
<h2>Aperçu du digest de la semaine</h2>
{% if pending %}
<pre class="capture-pre">{{ pending }}</pre>
{% else %}
+5 -5
View File
@@ -15,9 +15,9 @@
<h1>Prévisions — « on finit quand ? »</h1>
<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>
<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 %}
{% 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">
@@ -58,7 +58,7 @@
{% 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>
<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 %}
@@ -75,7 +75,7 @@
{% endif %}
<section class="briefing-card">
<h2>📈 Débit par projet</h2>
<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>
@@ -93,7 +93,7 @@
{% else %}
<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>
{% else %}
+2 -2
View File
@@ -23,8 +23,8 @@
<th data-sort="severity">Santé</th>
<th data-sort="project">Projet</th>
<th data-sort="open">Ouvertes</th>
<th data-sort="zombies" data-first-dir="desc">🧟 Zombies</th>
<th data-sort="slowest">🐌 Goulot</th>
<th data-sort="zombies" data-first-dir="desc">Zombies</th>
<th data-sort="slowest">Goulot</th>
<th></th>
</tr>
</thead>
+8 -8
View File
@@ -13,7 +13,7 @@
<main class="focus-main">
{% if project_names %}
<div class="health-switch-bar">
<label for="health-switch">🩺 Projet :</label>
<label for="health-switch">Projet :</label>
<select id="health-switch">
{% for name in project_names %}
<option value="{{ name }}"{% if name == full_name %} selected{% endif %}>{{ name }}</option>
@@ -31,14 +31,14 @@
{% else %}
<section class="briefing-card">
<h1>{{ full_name }}</h1>
<h2>⏱️ Temps passé par colonne (journal)</h2>
<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>{{ 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>
@@ -47,7 +47,7 @@
</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>
<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>
@@ -55,16 +55,16 @@
</section>
<section class="briefing-card">
<h2>🧟 Cartes zombies — aucune activité depuis ≥ {{ health.zombie_days }} jours</h2>
<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>
<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 %}
+1 -1
View File
@@ -15,7 +15,7 @@
regroupé par colonne, les cartes les plus anciennes d'abord.</p>
<div class="inflight-summary">
<span class="inflight-total">🛩️ {{ total }} carte{{ 's' if total != 1 }} en vol</span>
<span class="inflight-total">{{ total }} carte{{ 's' if total != 1 }} en vol</span>
{% for group in groups %}
<span class="col-chip" style="background: color-mix(in srgb, {{ column_colors[group.column] }} 25%, transparent); color: var(--text)">
{{ column_labels[group.column] }} · {{ group.cards|length }}{% if wip_globals.get(group.column) %} / limite globale {{ wip_globals[group.column] }}{% endif %}
+1 -1
View File
@@ -30,7 +30,7 @@
</label>
<button type="submit" class="login-submit">Se connecter</button>
</form>
<a class="login-sso" href="{{ sso_authorize_url }}">🧭 Se connecter avec ObsiGate (SSO)</a>
<a class="login-sso" href="{{ sso_authorize_url }}">Se connecter avec ObsiGate (SSO)</a>
<p class="login-foot">Accès restreint — session de {{ 7 }} jours, déconnexion depuis le menu latéral.</p>
</section>
</main>
+2 -2
View File
@@ -20,7 +20,7 @@
<div class="my-filters" id="my-filters">
<div class="my-filters-row">
<div class="my-search-wrap">
<span class="my-search-icon" aria-hidden="true">🔍</span>
<span class="my-search-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true" style="width:18px;height:18px;display:block"><circle cx="11" cy="11" r="7"/><path d="M20 20l-4-4"/></svg></span>
<input type="search" id="my-search" autocomplete="off"
placeholder="Rechercher un titre, un projet, un #numéro, une étiquette… ( / )">
</div>
@@ -77,7 +77,7 @@
data-updated="{{ issue.updated_at or '' }}"
data-title="{{ issue.title }}"
data-search="#{{ issue.number }} {{ issue.title }} {{ group.full_name }} {{ (issue.labels or [])|map(attribute='name')|join(' ') }}">
<span class="issue-state">🟢</span>
<span class="issue-state" aria-label="Ouverte">●</span>
<a class="issue-main" href="/board/{{ group.full_name }}">
<span class="card-id">#{{ issue.number }}</span> {{ issue.title }}
</a>
+6 -6
View File
@@ -15,7 +15,7 @@
<h1>Mon profil</h1>
<p class="page-sub">{{ profile_user.username }} · rôle : {{ profile_user.role }}</p>
</div>
{% if msg %}<p class="chip-ok">✅ {{ msg }}</p>{% endif %}
{% if msg %}<p class="chip-ok">{{ msg }}</p>{% endif %}
{% if err %}<p class="chip-warn">{{ err }}</p>{% endif %}
{% if profile_user.must_change_password %}<p class="chip-warn">Ton mot de passe est temporaire : change-le ci-dessous.</p>{% endif %}
@@ -47,11 +47,11 @@
</section>
<section class="briefing-card">
<h2>🗝️ Jetons d'API (MCP)</h2>
<h2>Jetons d'API (MCP)</h2>
<p class="muted">Pour les consommateurs machine — le service d'agents, en premier lieu. Un jeton (préfixe <code>kbt_</code>) authentifie uniquement <code>/mcp</code> et <code>/api/</code>, jamais l'interface web. Il se présente en en-tête <code>Authorization: Bearer kbt_…</code>. Le texte clair n'est affiché qu'à la création ; ensuite, seule son empreinte est conservée.</p>
{% if profile_user.id %}
{% if nouveau_jeton %}
<p>✅ Nouveau jeton — <strong>copie-le maintenant</strong> :</p>
<p>Nouveau jeton — <strong>copie-le maintenant</strong> :</p>
<p><code style="font-size:15px">{{ nouveau_jeton }}</code></p>
{% endif %}
{% if api_tokens %}
@@ -84,7 +84,7 @@
</section>
<section class="briefing-card">
<h2>✦ Liaison draco (SSO)</h2>
<h2>Liaison draco (SSO)</h2>
{% if profile_user.draco_sub %}
<p>Compte lié à l'identité draco <code>{{ profile_user.draco_sub }}</code> — la connexion via ObsiGate ouvre directement cette session.</p>
<form method="post" action="/profile/draco/unlink" class="inline-form">
@@ -97,11 +97,11 @@
</section>
<section class="briefing-card">
<h2>🔌 Mes connexions personnelles</h2>
<h2>Mes connexions personnelles</h2>
<p class="muted">Visibles de toi seul. Pour les utiliser, ajoute-les à un de tes espaces (ou demande à un admin d'espace).</p>
{% for c in my_connections %}
<p>
{% if c.kind == 'github' %}🐙{% else %}🍵{% endif %} <strong>{{ c.name }}</strong>
{% if c.kind == 'github' %}GitHub{% else %}Gitea{% endif %} <strong>{{ c.name }}</strong>
<span class="muted">({{ c.slug }}) · {{ c.base_url }} · compte : {{ c.account_login or 'à tester' }} · jeton : {{ c.token_mask }}</span>
<form method="post" action="/profile/connections/{{ c.id }}/test" class="inline-form">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
+19 -19
View File
@@ -27,19 +27,19 @@
<div class="proj-actions">
<a href="/board/{{ full_name }}" class="nav-pill proj-primary">Ouvrir le board</a>
<a href="/capture" class="nav-pill">⚡ Capture</a>
<a href="/capture" class="nav-pill">Capture</a>
<a href="/triage/{{ full_name }}" class="nav-pill">Triage</a>
<a href="/health/{{ full_name }}" class="nav-pill">🩺 Santé</a>
<a href="/forecast" class="nav-pill">🔮 Prévisions</a>
<a href="/wall?repo={{ full_name }}" class="nav-pill">🖥️ Wallboard</a>
<a href="/health/{{ full_name }}" class="nav-pill">Santé</a>
<a href="/forecast" class="nav-pill">Prévisions</a>
<a href="/wall?repo={{ full_name }}" class="nav-pill">Wallboard</a>
{% if repo_info.html_url %}
<a href="{{ repo_info.html_url }}" target="_blank" rel="noopener" class="nav-pill">🔗 Gitea ↗</a>
<a href="{{ repo_info.html_url }}" target="_blank" rel="noopener" class="nav-pill">Gitea ↗</a>
{% endif %}
</div>
<div class="proj-grid">
<section class="proj-panel">
<h2>🎯 Jalons ouverts</h2>
<h2>Jalons ouverts</h2>
{% if milestones %}
{% for ms in milestones %}
<div class="proj-ms">
@@ -49,19 +49,19 @@
</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 }}{% endif %}</div>
<div class="ms-meta">{{ ms.closed }}/{{ ms.total }} terminées{% if ms.eta_label %} · {{ ms.eta_label }}{% endif %}</div>
{% else %}
<div class="ms-meta">Aucune issue dans ce jalon pour le moment.</div>
{% endif %}
{% if ms.id %}
<details class="ms-edit">
<summary>✏️ Modifier / fermer</summary>
<summary>Modifier / fermer</summary>
<form class="ms-form" data-ms-form data-action="/api/repos/{{ full_name }}/milestones/{{ ms.id }}/update">
<label>Titre <input type="text" name="title" value="{{ ms.title }}" maxlength="255" required></label>
<label>Échéance <input type="date" name="due_on" value="{{ ms.due_iso }}">
<span class="ms-hint">vide = retirer l'échéance</span></label>
<div class="ms-form-actions">
<button type="submit" class="btn-save btn-inline">💾 Enregistrer</button>
<button type="submit" class="btn-save btn-inline">Enregistrer</button>
</div>
</form>
<form class="ms-form" data-ms-form data-action="/api/repos/{{ full_name }}/milestones/{{ ms.id }}/close"
@@ -76,29 +76,29 @@
<p class="proj-empty">Aucun jalon ouvert pour le moment.</p>
{% endif %}
<details class="ms-edit ms-new">
<summary>➕ Nouveau jalon</summary>
<summary>Nouveau jalon</summary>
<form class="ms-form" data-ms-form data-action="/api/repos/{{ full_name }}/milestones">
<label>Titre <input type="text" name="title" maxlength="255" required placeholder="Ex. : Version 2.0"></label>
<label>Échéance <input type="date" name="due_on"></label>
<label>Description <textarea name="description" rows="2" maxlength="2000" placeholder="Optionnelle"></textarea></label>
<div class="ms-form-actions">
<button type="submit" class="btn-save btn-inline">🎯 Créer le jalon</button>
<button type="submit" class="btn-save btn-inline">Créer le jalon</button>
</div>
</form>
</details>
</section>
<section class="proj-panel">
<h2>🩺 Santé du flux</h2>
<h2>Santé du flux</h2>
{% if health %}
<ul class="proj-list">
<li><strong>{{ health.open_count }}</strong> cartes ouvertes</li>
{% if health.slowest %}
<li>🐢 Goulot : <strong>{{ COLUMN_LABELS[health.slowest.column] }}</strong> — séjour médian {{ health.slowest.median_days }} j</li>
<li>Goulot : <strong>{{ COLUMN_LABELS[health.slowest.column] }}</strong> — séjour médian {{ health.slowest.median_days }} j</li>
{% else %}
<li>🐢 Pas encore de goulot mesurable (historique trop court).</li>
<li>Pas encore de goulot mesurable (historique trop court).</li>
{% endif %}
<li>🧟 Zombies (≥ {{ health.zombie_days }} j sans activité) : <strong>{{ health.zombies|length }}</strong></li>
<li>Zombies (≥ {{ health.zombie_days }} j sans activité) : <strong>{{ health.zombies|length }}</strong></li>
</ul>
{% if health.zombies %}
<ul class="proj-list proj-zombies">
@@ -114,7 +114,7 @@
</section>
<section class="proj-panel">
<h2>🔮 Prévisions</h2>
<h2>Prévisions</h2>
{% if throughput %}
<ul class="proj-list">
<li>Débit : <strong>{{ throughput.avg }}</strong> fermetures / semaine en moyenne (min {{ throughput.min }} · max {{ throughput.max }}, 4 dernières semaines complètes)</li>
@@ -127,7 +127,7 @@
</section>
<section class="proj-panel">
<h2>🕘 Activité récente</h2>
<h2>Activité récente</h2>
{% if events %}
<ul class="proj-list">
{% for e in events %}
@@ -141,11 +141,11 @@
</section>
<section class="proj-panel proj-notes">
<h2>📝 Notes du projet</h2>
<h2>Notes du projet</h2>
{% if notes %}
<div class="proj-notes-body">{{ notes }}</div>
{% else %}
<p class="proj-empty">Aucune note. Les notes s'écrivent depuis le panneau « 🗒️ Notes » du board et vivent avec le projet.</p>
<p class="proj-empty">Aucune note. Les notes s'écrivent depuis le panneau « Notes » du board et vivent avec le projet.</p>
{% endif %}
<p><a href="/board/{{ full_name }}">Ouvrir le board pour éditer →</a></p>
</section>
+5 -5
View File
@@ -27,13 +27,13 @@
{% for err in errors %}
<p class="chip-warn">{{ err }}</p>
{% endfor %}
<p><button type="button" class="btn-save btn-inline" id="rules-run">▶️ Évaluer maintenant</button>
<p><button type="button" class="btn-save btn-inline" id="rules-run">Évaluer maintenant</button>
<span id="rules-run-result" class="notes-hint"></span></p>
</section>
{% if rules %}
<section class="briefing-card">
<h2>📜 Règles actives ({{ rules|length }})</h2>
<h2>Règles actives ({{ rules|length }})</h2>
<ul class="forecast-milestones">
{% for rule in rules %}
<li><strong>{{ rule.name }}</strong> — quand
@@ -46,7 +46,7 @@
{% if last_run %}
<section class="briefing-card">
<h2>🕐 Dernière évaluation — {{ last_run.at }}</h2>
<h2>Dernière évaluation — {{ last_run.at }}</h2>
{% if last_run.actions %}
<ul class="forecast-milestones">
{% for action in last_run.actions %}
@@ -75,10 +75,10 @@
const r = await fetch('/api/rules/run', { method: 'POST', body: data });
const body = await r.json();
if (!r.ok || !body.ok) throw new Error((body && body.error) || ('HTTP ' + r.status));
out.textContent = '✔ ' + body.summary.actions.length + ' action(s) — rechargement…';
out.textContent = '' + body.summary.actions.length + ' action(s) — rechargement…';
setTimeout(() => location.reload(), 700);
} catch (e) {
out.textContent = '❌ ' + e.message;
out.textContent = '' + e.message;
}
});
</script>
+1 -1
View File
@@ -30,7 +30,7 @@
{% for row in group.rows %}
{% set issue = row.issue %}
<div class="issue-row">
<span class="issue-state">{{ '✅' if issue.state != 'open' else '🟢' }}</span>
<span class="issue-state">{{ '✓' if issue.state != 'open' else '●' }}</span>
<a class="issue-main" href="/board/{{ group.full_name }}">
<span class="card-id">#{{ issue.number }}</span> {{ issue.title }}
</a>
+3 -3
View File
@@ -19,7 +19,7 @@
<p>
<button type="button" class="btn-save btn-inline" id="sprint-copy">Copier le markdown</button>
<button type="button" class="btn-save btn-inline" id="sprint-download">Télécharger (.md)</button>
<a class="btn-save btn-inline" style="text-decoration:none" href="/board/{{ full_name }}?milestone={{ milestone_param|urlencode }}">🎯 Voir le board du milestone</a>
<a class="btn-save btn-inline" style="text-decoration:none" href="/board/{{ full_name }}?milestone={{ milestone_param|urlencode }}">Voir le board du milestone</a>
</p>
<p class="notes-hint">Le texte est archivable tel quel dans ObsiGate — c'est le markdown ci-dessous, rendu pour lecture.</p>
</section>
@@ -31,7 +31,7 @@
document.getElementById('sprint-copy').addEventListener('click', async () => {
try {
await navigator.clipboard.writeText(SPRINT_MD);
document.getElementById('sprint-copy').textContent = '✅ Copié !';
document.getElementById('sprint-copy').textContent = 'Copié !';
} catch (e) { alert('Copie impossible : ' + e); }
});
document.getElementById('sprint-download').addEventListener('click', () => {
@@ -49,7 +49,7 @@
<p>Choisis un milestone pour générer son récit :</p>
<ul>
{% for m in milestones %}
<li><a href="/sprint/{{ full_name }}?milestone={{ m.title|urlencode }}">🎯 {{ m.title }}</a> — {{ m.closed_issues or 0 }} fermée(s), {{ m.open_issues or 0 }} ouverte(s)</li>
<li><a href="/sprint/{{ full_name }}?milestone={{ m.title|urlencode }}">{{ m.title }}</a> — {{ m.closed_issues or 0 }} fermée(s), {{ m.open_issues or 0 }} ouverte(s)</li>
{% else %}
<li>Aucun milestone dans ce projet.</li>
{% endfor %}
+2 -2
View File
@@ -17,7 +17,7 @@
<p>
<button type="button" class="btn-save btn-inline" id="standup-copy">Copier le standup</button>
{% if include_in_briefing %}
<span class="chip-ok">Inclus dans le briefing ntfy du matin ✔</span>
<span class="chip-ok">Inclus dans le briefing ntfy du matin </span>
{% else %}
<span class="notes-hint">Astuce : <code>KANBANTE_BRIEFING_INCLUDE_STANDUP=1</code> l'ajoute au briefing ntfy du matin.</span>
{% endif %}
@@ -37,7 +37,7 @@
const text = document.getElementById('standup-text').textContent;
try {
await navigator.clipboard.writeText(text);
document.getElementById('standup-copy').textContent = '✅ Copié !';
document.getElementById('standup-copy').textContent = 'Copié !';
} catch (e) { alert('Copie impossible : ' + e); }
});
</script>
+3 -3
View File
@@ -38,7 +38,7 @@
{% if recap %}
<section class="briefing-card">
<h2>🌆 Ça a bougé aujourd'hui</h2>
<h2>Ça a bougé aujourd'hui</h2>
<ul>
{% for event in recap %}
<li>{{ event.created_at[11:16] }} — {{ event.repo_full_name }} #{{ event.issue_number }} : {{ event.event_type }}{% if event.to_column %} → {{ column_labels.get(event.to_column, event.to_column) }}{% endif %}</li>
@@ -49,7 +49,7 @@
{% if carry_over %}
<section class="briefing-card">
<h2>↩️ À reprendre d'hier</h2>
<h2>À reprendre d'hier</h2>
<ul class="today-list">
{% for pick in carry_over %}
<li>
@@ -63,7 +63,7 @@
{% if candidates and picks|length < 3 %}
<section class="briefing-card">
<h2>🎯 Candidates (encours actuel)</h2>
<h2>Candidates (encours actuel)</h2>
<ul class="today-list">
{% for card in candidates %}
<li>
+10 -10
View File
@@ -36,7 +36,7 @@
<ul class="triage-summary" id="triage-summary"></ul>
<p>
<a class="btn-save btn-inline" href="/board/{{ full_name }}">Retour au board</a>
<a class="btn-save btn-inline" href="/triage/{{ full_name }}">🔁 Nouvelle session</a>
<a class="btn-save btn-inline" href="/triage/{{ full_name }}">Nouvelle session</a>
</p>
</section>
@@ -44,20 +44,20 @@
<section class="triage-keys" id="triage-keys">
<button type="button" data-act="move" data-col="todo"><kbd>1</kbd> À faire</button>
<button type="button" data-act="move" data-col="in_progress"><kbd>2</kbd> 🚧 En cours</button>
<button type="button" data-act="move" data-col="in_progress"><kbd>2</kbd> En cours</button>
<button type="button" data-act="move" data-col="qa"><kbd>3</kbd> QA</button>
<button type="button" data-act="move" data-col="review"><kbd>4</kbd> 👀 Révision</button>
<button type="button" data-act="move" data-col="done"><kbd>5</kbd> ✅ Terminé</button>
<button type="button" data-act="labels"><kbd>L</kbd> 🏷️ Étiquettes</button>
<button type="button" data-act="assign"><kbd>A</kbd> 👤 M'assigner</button>
<button type="button" data-act="close"><kbd>X</kbd> 🚫 Fermer</button>
<button type="button" data-act="next"><kbd>Entrée</kbd> ⏭️ Suivante</button>
<button type="button" data-act="undo"><kbd>Z</kbd> ↩️ Annuler</button>
<button type="button" data-act="move" data-col="review"><kbd>4</kbd> Révision</button>
<button type="button" data-act="move" data-col="done"><kbd>5</kbd> Terminé</button>
<button type="button" data-act="labels"><kbd>L</kbd> Étiquettes</button>
<button type="button" data-act="assign"><kbd>A</kbd> M'assigner</button>
<button type="button" data-act="close"><kbd>X</kbd> Fermer</button>
<button type="button" data-act="next"><kbd>Entrée</kbd> Suivante</button>
<button type="button" data-act="undo"><kbd>Z</kbd> Annuler</button>
</section>
<div class="triage-labels" id="triage-labels" hidden>
<div class="triage-labels-head">
<strong>🏷️ Étiquettes de la carte</strong>
<strong>Étiquettes de la carte</strong>
<button type="button" class="btn-sm" id="triage-labels-close">✕ (Échap)</button>
</div>
<div class="triage-labels-list" id="triage-labels-list">Chargement…</div>
+2 -2
View File
@@ -55,7 +55,7 @@
{% endif %}
<footer class="wall-foot">
<span>✅ Fermées aujourd'hui : <strong>{{ slide.closed_today }}</strong></span>
<span>Fermées aujourd'hui : <strong>{{ slide.closed_today }}</strong></span>
<span>Ouvertes : <strong>{{ slide.open_total }}</strong></span>
<span class="wall-live">● EN DIRECT</span>
</footer>
@@ -67,7 +67,7 @@
<button type="button" class="wall-nav-btn" id="wall-prev" title="Projet précédent (←)">❮</button>
<div class="wall-dots" id="wall-dots"></div>
<button type="button" class="wall-nav-btn" id="wall-next" title="Projet suivant (→)">❯</button>
<div class="wall-paused" id="wall-paused" hidden>⏸️ Rotation en pause — Espace pour reprendre</div>
<div class="wall-paused" id="wall-paused" hidden>Rotation en pause — Espace pour reprendre</div>
</nav>
{% endif %}
{% else %}
+2 -1
View File
@@ -1443,8 +1443,9 @@ body.sidebar-collapsed .side-foot { justify-content: center; }
.star-btn {
background: transparent; border: none; cursor: pointer;
font-size: 1rem; line-height: 1; padding: 2px 4px; margin-left: auto;
color: #fbbf24;
color: var(--text-2);
}
.star-btn[data-starred="1"] { color: var(--text); }
.star-btn:hover { transform: scale(1.2); }
.star-btn:disabled { opacity: 0.5; }
.card-board-link { font-weight: 600; }
+2 -2
View File
@@ -46,10 +46,10 @@
l'en-tête, lui, n'avait qu'un texte. */
var ico = themeBtn.querySelector('[data-theme-ico]');
var lbl = themeBtn.querySelector('[data-theme-label]');
if (ico) ico.textContent = light ? '🌙' : '☀️';
if (ico) ico.textContent = light ? '☾' : '☀';
if (lbl) lbl.textContent = light ? 'Mode sombre' : 'Mode clair';
if (!ico && !lbl) {
themeBtn.textContent = light ? '🌙 Mode sombre' : '☀️ Mode clair';
themeBtn.textContent = light ? '☾ Mode sombre' : '☀ Mode clair';
}
themeBtn.setAttribute('aria-pressed', light ? 'true' : 'false');
}
+1 -1
View File
@@ -75,7 +75,7 @@
var item = document.createElement('span');
item.className = 'repo-item';
item.setAttribute('role', 'option');
var star = repo.starred ? '⭐ ' : '';
var star = repo.starred ? '★ ' : '';
var date = repo.updated_at ? repo.updated_at.slice(0, 10) : '';
var nameEl = document.createElement('span');
nameEl.className = 'repo-item-name';
+3 -3
View File
@@ -31,7 +31,7 @@
var data = await res.json();
if (!data.ok) throw new Error(data.error || 'échec');
btn.setAttribute('data-starred', want ? '1' : '0');
btn.textContent = want ? '⭐' : '☆';
btn.textContent = want ? '★' : '☆';
btn.title = want ? 'Désépingler (étoile Gitea)' : 'Épingler (étoile Gitea)';
var card = btn.closest('.project-card');
if (card) {
@@ -44,9 +44,9 @@
refreshPinnedVisibility();
}
} catch (err) {
btn.textContent = '⚠️';
btn.textContent = '!';
setTimeout(function () {
btn.textContent = btn.getAttribute('data-starred') === '1' ? '⭐' : '☆';
btn.textContent = btn.getAttribute('data-starred') === '1' ? '★' : '☆';
}, 1600);
} finally {
btn.disabled = false;
+36
View File
@@ -0,0 +1,36 @@
"""Icônes monochromes (#59, suite) : plus d'emoji colorés dans les
gabarits applicatifs (hors barre Fusion partagée, favicons et
contenu utilisateur ), étoile et thème en symboles monochromes.
"""
def test_pas_d_emoji_colores_dans_les_gabarits():
import pathlib
emoji = ["🎯", "🔮", "🩺", "📖", "🏷️", "🧱", "➕", "📝", "💾", "📎", "💬",
"📄", "📲", "📤", "✍️", "⚡", "🔌", "📱", "📈", "💤", "👥", "🔭",
"📜", "🧾", "🧠", "👀", "📦", "🐌", "🧟", "🗝️", "🐙", "🍵", "🟢",
"🔴", "✅", "❌", "⭐", "🐢", "⏱️", "📊", "🗣️", "🛩️", "⚙️", "📰",
"🗄️", "🖥️", "🛠️", "🧭", "🎛️", "⌨️", "🔗", "🌆", "🚧", "🚫", "⏭️",
"🔁", "☀️", "🌙"]
for path in pathlib.Path("app/templates").glob("*.html"):
if path.name in ("_draco_bar.html",):
continue
text = path.read_text(encoding="utf-8")
for line in text.splitlines():
if 'rel="icon"' in line:
continue
for e in emoji:
assert e not in line, (path.name, e, line.strip()[:100])
def test_etoile_et_theme_monochromes(client, fake_gitea):
css = client.get("/static/css/style.css").text
# L'étoile n'est plus jaune : couleur texte, épinglée = texte plein.
# (#fbbf24 reste pour l'âge des cartes sur le wallboard, une
# information sémantique, pas une icône.)
assert '.star-btn[data-starred="1"]' in css
assert "color: var(--text-2);" in css
js = client.get("/static/js/stars.js").text
assert "⭐" not in js
nav = client.get("/static/js/nav.js").text
assert "☀️" not in nav and "🌙" not in nav
+6 -6
View File
@@ -8,7 +8,7 @@ def test_dashboard_shows_next_milestone(client, fake_gitea):
page = client.get("/")
assert page.status_code == 200
# Faux : Sprint 1, échéance 2026-10-20, 1 fermée / 2 au total.
assert "🎯 Sprint 1" in page.text
assert "Sprint 1" in page.text
assert "1/2 terminées" in page.text
assert "échéance" in page.text
@@ -75,12 +75,12 @@ def test_project_page(client, fake_gitea):
)
page = client.get("/project/Projets/demo")
assert page.status_code == 200
assert "🎯 Jalons ouverts" in page.text
assert "Jalons ouverts" in page.text
assert "Sprint 1" in page.text
assert "🩺 Santé du flux" in page.text
assert "🔮 Prévisions" in page.text
assert "🕘 Activité récente" in page.text
assert "📝 Notes du projet" in page.text
assert "Santé du flux" in page.text
assert "Prévisions" in page.text
assert "Activité récente" in page.text
assert "Notes du projet" in page.text
assert 'href="/board/Projets/demo"' in page.text
assert "/wall?repo=Projets/demo" in page.text
# Titre du projet et lien Gitea du faux.
+1 -1
View File
@@ -81,7 +81,7 @@ def test_forecast_page_and_milestone_banner(client, fake_gitea):
assert "Sprint 1" in r.text
board = client.get("/board/Projets/demo", params={"milestone": "Sprint 1"})
assert board.status_code == 200
assert "🔮" in board.text
assert "milestone-forecast" in board.text
assert "historique pour prévoir" in board.text # empty journal
assert "/sprint/Projets/demo" in board.text