Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88740a78a0 | ||
|
|
69c347f198 | ||
|
|
0acb02c333 | ||
|
|
9b0ad1b39c | ||
|
|
8ade518402 | ||
|
|
13e66742a1 | ||
|
|
2836c3f71d | ||
|
|
84d94f9e24 | ||
|
|
92176f614b | ||
|
|
93c6a1fc88 | ||
|
|
67c2684ad6 | ||
|
|
895ee81d51 | ||
|
|
d0ba39bf01 | ||
|
|
fc1b40cd98 | ||
|
|
5f184f033c | ||
|
|
d17f3e56b3 | ||
|
|
11b1b5f11d | ||
|
|
37fd4de63d | ||
|
|
1b218ce030 | ||
|
|
40f034e86e | ||
|
|
911f07362f | ||
|
|
ee837972be | ||
|
|
4470e6ccbc | ||
|
|
d8cb4664e5 | ||
|
|
f2e702a3d9 | ||
|
|
435954c31b | ||
|
|
7f66775039 | ||
|
|
9586e310ba | ||
|
|
f58339fd77 | ||
|
|
0fc8673cf8 | ||
|
|
a090824db3 | ||
|
|
0393a91ff8 | ||
|
|
50596fd8b2 | ||
|
|
a65e92f6e8 | ||
|
|
6fd8e9fffe | ||
|
|
842268577b | ||
|
|
b3735e4056 | ||
|
|
ce1bbf0e59 | ||
|
|
336c79fd8b | ||
|
|
7a23aff31b | ||
|
|
a2ebefbda1 | ||
|
|
9653f851c8 | ||
|
|
144957ccd3 | ||
|
|
204b9ebbe6 | ||
|
|
05fde96377 | ||
|
|
7856cdb0c2 | ||
|
|
9757f3717a | ||
|
|
0faabf0078 | ||
|
|
a5c351e0b3 | ||
|
|
f033b18e86 | ||
|
|
3ade3174bb | ||
|
|
44d905c25d | ||
|
|
1273924eab | ||
|
|
fdc42332b2 | ||
|
|
304f30847a | ||
|
|
61739b6115 | ||
|
|
4a338fea79 | ||
|
|
659ff36f84 | ||
|
|
86f216be64 | ||
|
|
89f4493713 | ||
|
|
6008f5bb23 | ||
|
|
0fa828b48c | ||
|
|
060a51c32e | ||
|
|
0180c610b8 | ||
|
|
fe41eca899 |
@@ -1,3 +1,28 @@
|
||||
## 0.45.1 — 2026-10-29 — Barre commune : les 5 logos en place dans le menu et la barre
|
||||
|
||||
- **Barre d'apps v0.1.9 vendangée (dracodev-commons)** : `draco-barre.js` et `draco-barre.css` remplacés verbatim par la v0.1.9 (les fichiers vendangés précédents étaient identiques à l'octet près à ceux de commons avant la v0.1.9, aucune adaptation locale n'est perdue) ; fragment `_draco_bar.html` porté à la v0.1.9, le reste de l'intégration Kanbante (URL Jinja `draco_urls` / `agent_url`, `onAction` complet y compris profil/déconnexion, compteurs) est inchangé. Les surcharges Kanbante dans `style.css` ne sont pas touchées.
|
||||
- **Icônes** : les 5 tuiles de la barre — et donc du menu de la marque, qui les clone — sont désormais les logos fournis, tels quels, jamais redessinés : ObsiGate (arbre), Flowdeck (cartes), TikTak (logo officiel SVG), Kanbante (cycle vert), Console Spark (étoile rouge validée). Fichiers servis depuis `static/img/logos/` (copie de `ui/barre-apps/logos/` de commons, PNG 180 px) ; l'entrée « Console Spark » du menu clone l'icône de l'onglet agent. Les glyphes texte ▤/▦/✓/▥/✦ disparaissent des onglets (la marque ✦, le bouton Spark et le rappel d'en-tête gardent leur ✦).
|
||||
- **Cache** : le bump 0.45.0 → 0.45.1 change aussi le `?v=` des CSS/JS de la barre, donc les navigateurs ne garderont pas les anciennes icônes.
|
||||
|
||||
## 0.45.0 — 2026-10-29 — Mobile : tiroir, pied et menu compte
|
||||
|
||||
- **Tiroir mobile** : l'état « replié » mémorisé sur desktop ne rétrécit plus le tiroir (cause : `body.sidebar-collapsed .sidebar { width: 68px; }` gagnait par spécificité sur le 264 px mobile, libellés tronqués par overflow-x). Le tiroir fait 264 px, replié ou non ; le fond ne défile plus derrière lui ; le bouton de repli dans le tiroir le ferme ; un clic sur un lien de navigation le ferme aussi.
|
||||
- **Pied de sidebar visible en mobile** : version, fraîcheur des données, thème et Sortir sont désormais accessibles dans le tiroir (avant : pied masqué, aucun moyen de sortir depuis un téléphone).
|
||||
- **Menu compte draco** : Profil et Déconnexion fonctionnent enfin (ils étaient relayés par draco-barre.js mais jamais pris en charge par le onAction Kanbante — le menu se fermait sans rien faire). En mobile, c'était le seul accès au profil et à la déconnexion.
|
||||
- **Menus draco** : largeur plafonnée à 100vw - 20 px pour ne jamais déborder d'un petit écran.
|
||||
|
||||
## 0.44.0 — 2026-10-29 — Ergonomie : tuile Projets et pied de sidebar
|
||||
|
||||
- **Tuile Projets** : en-tête groupé (gauche : organisation + santé, droite : date + étoile + menu) au lieu de 5 éléments éparpillés ; avancement global labellisé (« Avancement — X/Y terminées ») au lieu d'une barre anonyme ; jalon séparé par un filet, avec étiquette « Prochain jalon » et échéance distincte ; pied en 2 rangées — (1) compte d'issues + bouton Board, (2) répartition par colonne labellisée (« En cours 8 », « Terminé 59 », au lieu de chiffres nus 8 / 59) + prévision ; activité de la semaine en texte secondaire (« Cette semaine : … »).
|
||||
- **Pied de sidebar** : 2 rangées séparées par un filet — (1) profil + bouton Sortir icône seule (⏻, aria-label, sans texte), (2) version + fraîcheur des données en pastille discrète (point vert + « à l'instant », infobulle expliquant les instantanés Gitea). En rail replié, la rangée méta est masquée, les icônes restent centrées.
|
||||
|
||||
## 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
@@ -177,7 +177,7 @@ from urllib.parse import urlparse
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
APP_VERSION = "0.42.0"
|
||||
APP_VERSION = "0.45.1"
|
||||
|
||||
|
||||
def _env_bool(name: str, default: bool) -> bool:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{# Barre d'apps commune dracodev — Fusion ACC-1 (issue kanbante#28).
|
||||
Snippet vendangé depuis Projets/dracodev-commons v0.1.6
|
||||
Snippet vendangé depuis Projets/dracodev-commons v0.1.9
|
||||
(ui/barre-apps/) : structure inchangée ; seule adaptation locale
|
||||
(0.32.0, dev/prod) = les URL des apps et l'adresse du service
|
||||
d'agents sont INJECTÉES par l'hôte depuis app.config
|
||||
@@ -10,7 +10,12 @@
|
||||
« Console Spark » du menu de la marque est conservée.
|
||||
CSS/JS : /static/css/draco-barre.css, /static/js/draco-barre.js.
|
||||
Inclus par _header.html (toutes les pages applicatives ; ni le
|
||||
wallboard ni l'écran de connexion, volontaires). #}
|
||||
wallboard ni l'écran de connexion, volontaires).
|
||||
Icônes logos v0.1.9 (2026-10-29) : les 5 tuiles sont les logos
|
||||
fournis, tels quels, servis depuis /static/img/logos/ (copie de
|
||||
ui/barre-apps/logos/ de dracodev-commons) — jamais de glyphe
|
||||
texte, jamais de redessin ; l'entrée « Console Spark » du menu
|
||||
clone l'icône de l'onglet agent (draco-barre.js). #}
|
||||
<link rel="stylesheet" href="/static/css/draco-barre.css?v={{ version }}">
|
||||
<link rel="stylesheet" href="/static/css/draco-panneau.css?v={{ version }}">
|
||||
<link rel="stylesheet" href="/static/css/draco-alertes.css?v={{ version }}">
|
||||
@@ -23,25 +28,25 @@
|
||||
</a>
|
||||
<nav class="draco-apptabs" aria-label="Applications">
|
||||
<a class="draco-apptab" data-app="obsigate" href="{{ draco_urls.obsigate }}/">
|
||||
<span class="draco-apptab-ico" style="--app:#7a5af5">▤</span>
|
||||
<span class="draco-apptab-ico" style="--app:#7a5af5"><img src="/static/img/logos/obsigate.png" alt="ObsiGate" width="30" height="30"></span>
|
||||
<span class="draco-apptab-txt"><strong>ObsiGate</strong><small>Notes</small></span>
|
||||
</a>
|
||||
<a class="draco-apptab" data-app="flowdeck" href="{{ draco_urls.flowdeck }}/">
|
||||
<span class="draco-apptab-ico" style="--app:#2f6fed">▦</span>
|
||||
<span class="draco-apptab-ico" style="--app:#2f6fed"><img src="/static/img/logos/flowdeck.png" alt="Flowdeck" width="30" height="30"></span>
|
||||
<span class="draco-apptab-txt"><strong>Flowdeck</strong><small>Projets</small></span>
|
||||
</a>
|
||||
<a class="draco-apptab" data-app="tiktak" href="{{ draco_urls.tiktak }}/">
|
||||
<span class="draco-apptab-ico" style="--app:#e8762c">✓</span>
|
||||
<span class="draco-apptab-ico" style="--app:#e8762c"><img src="/static/img/logos/tiktak.svg" alt="TikTak" width="30" height="30"></span>
|
||||
<span class="draco-apptab-txt"><strong>TikTak</strong><small>Tâches</small></span>
|
||||
<span class="draco-badge" data-badge="tiktak" hidden>0</span>
|
||||
</a>
|
||||
<a class="draco-apptab" data-app="kanbante" href="{{ draco_urls.kanbante }}/">
|
||||
<span class="draco-apptab-ico" style="--app:#0d9488">▥</span>
|
||||
<span class="draco-apptab-ico" style="--app:#0d9488"><img src="/static/img/logos/kanbante.png" alt="Kanbante" width="30" height="30"></span>
|
||||
<span class="draco-apptab-txt"><strong>Kanbante</strong><small>Kanban</small></span>
|
||||
<span class="draco-badge" data-badge="kanbante" hidden>0</span>
|
||||
</a>
|
||||
<a class="draco-apptab" data-app="agent" href="{{ agent_url }}/">
|
||||
<span class="draco-apptab-ico" style="--app:#c2437e">✦</span>
|
||||
<span class="draco-apptab-ico" style="--app:#d21f2c"><img src="/static/img/logos/spark.png" alt="Console Spark" width="30" height="30"></span>
|
||||
<span class="draco-apptab-txt"><strong>Console Spark</strong><small>Agent commun</small></span>
|
||||
</a>
|
||||
</nav>
|
||||
@@ -103,6 +108,13 @@
|
||||
};
|
||||
if (act === "domaine") { try { localStorage.setItem("draco_domain", el.dataset.dom); } catch (e) { } return; }
|
||||
if (act === "rail") { var b = document.getElementById("side-collapse"); if (b) b.click(); return; }
|
||||
/* Menu compte (image 2) : ces deux actions étaient relayées
|
||||
par draco-barre.js mais jamais prises en charge ici —
|
||||
le menu se fermait sans rien faire. En mobile, c'est le
|
||||
seul accès au profil et à la déconnexion (le pied de
|
||||
sidebar était masqué). */
|
||||
if (act === "profil") { location.href = "/profile"; return; }
|
||||
if (act === "deconnexion") { location.href = "/logout"; return; }
|
||||
if (act === "palette") { location.href = "/recherche"; return; }
|
||||
if (act === "alertes") { if (window.DRACO_ALERTES) { window.DRACO_ALERTES.open(); } else { location.href = "/activity"; } return; }
|
||||
/* Correctif fusion #70 : action "menu" supprimée avec le bouton trois points (console agent dans le menu de la marque). */
|
||||
|
||||
+39
-35
@@ -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 %}
|
||||
@@ -137,19 +137,23 @@
|
||||
{% endfor %}
|
||||
</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>
|
||||
{% 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>
|
||||
{% 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>
|
||||
{% endif %}
|
||||
{% if auth_enabled %}
|
||||
<a href="/logout" class="side-logout" title="Se déconnecter">⏻<span class="side-label"> Sortir</span></a>
|
||||
{% endif %}
|
||||
<div class="side-foot-user">
|
||||
{% if current_user %}
|
||||
<a href="/profile" class="side-logout side-profile" 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 %}
|
||||
{% 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>
|
||||
{% endif %}
|
||||
{% if auth_enabled %}
|
||||
<a href="/logout" class="side-logout logout-icon" title="Se déconnecter" aria-label="Se déconnecter">⏻</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="side-foot-meta">
|
||||
<span class="side-version side-label">v{{ version }}</span>
|
||||
{% if data_freshness %}
|
||||
<span class="side-fresh" title="Dernière actualisation des données Gitea (instantanés d'arrière-plan) — les données affichées datent de cet instantané"><span class="fresh-dot" aria-hidden="true"></span><span class="side-label">{{ data_freshness }}</span></span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
<div class="side-backdrop" id="side-backdrop" hidden></div>
|
||||
@@ -162,7 +166,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,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
@@ -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>
|
||||
|
||||
@@ -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 }}">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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" %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -91,82 +91,93 @@
|
||||
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 multi_conn %}<span class="conn-badge" title="Connexion {{ repo._conn_slug }}">{% if repo._conn_kind == 'github' %}🐙{% else %}🍵{% 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>
|
||||
<div class="card-header-left">
|
||||
<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' %}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 %}
|
||||
</div>
|
||||
<div class="card-header-actions">
|
||||
<span class="date" title="Dernière activité">{{ 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="/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>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<h3>
|
||||
<a href="/project/{{ repo.owner.login }}/{{ repo.name }}">{{ repo.name }}</a>
|
||||
</h3>
|
||||
<p class="desc">{{ repo.description or '—' }}</p>
|
||||
<p class="desc" title="{{ repo.description or '' }}">{{ 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 class="card-progress">
|
||||
<div class="progress-head"><span>Avancement</span><span>{{ done_n }}/{{ total }} terminées</span></div>
|
||||
<div class="progress" title="{{ done_n }}/{{ total }} terminées">
|
||||
<span style="width: {{ (100 * done_n / total)|round|int }}%"></span>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if repo.next_milestone %}
|
||||
{% set ms = repo.next_milestone %}
|
||||
<div class="ms-block">
|
||||
<div class="ms-head"><span class="ms-kicker">Prochain jalon</span>{% if ms.due_label %}<span class="ms-due{% if ms.late %} late{% endif %}">échéance {{ ms.due_label }}</span>{% endif %}</div>
|
||||
<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 %}
|
||||
<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>
|
||||
</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 %}
|
||||
<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 column_keys %}
|
||||
{% set n = stats[full].get(col, 0) %}
|
||||
{% if n > 0 %}
|
||||
<span class="chip" style="background: color-mix(in srgb, {{ COLUMN_COLORS[col] }} 25%, transparent); color: var(--text)">
|
||||
{{ n }}
|
||||
</span>
|
||||
<div class="foot-main">
|
||||
<span class="foot-count">{{ repo.open_issues_count or 0 }} issues ouvertes</span>
|
||||
<a href="/board/{{ full }}" class="card-board-link btn-sm">Board →</a>
|
||||
</div>
|
||||
<div class="foot-dist">
|
||||
{% if full in stats %}
|
||||
<div class="mini-progress" aria-label="Répartition par colonne">
|
||||
{% for col in column_keys %}
|
||||
{% set n = stats[full].get(col, 0) %}
|
||||
{% if n > 0 %}
|
||||
<span class="chip" title="{{ COLUMN_LABELS[col] }}" style="background: color-mix(in srgb, {{ COLUMN_COLORS[col] }} 25%, transparent); color: var(--text)">
|
||||
{{ COLUMN_LABELS[col] }} {{ n }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% 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 %}
|
||||
</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
|
||||
Cette semaine : {{ repo.activity.closed }} fermée(s) · {{ repo.activity.moved }} déplacement(s)
|
||||
{% 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 +197,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 +230,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>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -94,6 +94,13 @@
|
||||
width: 30px; height: 30px; flex: none; display: grid; place-items: center;
|
||||
border-radius: 9px; background: var(--app); color: #fff; font-size: 15px;
|
||||
}
|
||||
/* Icônes logos (2026-10-29, corrigé le même jour) : l'image du logo
|
||||
* fourni, telle quelle, remplit la tuile 30 px — jamais de glyphe
|
||||
* texte, jamais de redessin. Le fond de la tuile est transparent :
|
||||
* les logos PNG portent déjà leur fond (dégradé), le SVG TikTak
|
||||
* reste sur le fond de la barre. */
|
||||
.draco-apptab-ico { background: transparent; }
|
||||
.draco-apptab-ico img { width: 30px; height: 30px; display: block; border-radius: 9px; object-fit: cover; }
|
||||
.draco-apptab-txt { display: none; flex-direction: column; line-height: 1.15; }
|
||||
.draco-apptab-txt strong { font-size: 14px; font-weight: 700; }
|
||||
.draco-apptab-txt small { font-size: 11.5px; color: var(--ink-2); }
|
||||
|
||||
+85
-15
@@ -315,8 +315,15 @@ a.kpi:hover { background: var(--surface-2); text-decoration: none; }
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.card-header-left, .card-header-actions {
|
||||
display: flex; align-items: center; gap: 8px; min-width: 0;
|
||||
}
|
||||
.card-header-actions { flex-shrink: 0; }
|
||||
.card-header .date { white-space: nowrap; }
|
||||
.card-header .star-btn { margin-left: 0; }
|
||||
.org-tag {
|
||||
font-size: 0.68rem;
|
||||
text-transform: uppercase;
|
||||
@@ -340,20 +347,33 @@ a.kpi:hover { background: var(--surface-2); text-decoration: none; }
|
||||
min-height: 2.4em;
|
||||
}
|
||||
.card-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-2);
|
||||
gap: 10px;
|
||||
display: flex; flex-direction: column; align-items: stretch;
|
||||
font-size: 0.8rem; color: var(--text-2); gap: 10px;
|
||||
border-top: 1px solid var(--border);
|
||||
padding-top: 12px; margin-top: 14px;
|
||||
}
|
||||
.mini-progress { display: flex; gap: 4px; }
|
||||
.foot-main, .foot-dist {
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
gap: 10px; flex-wrap: wrap;
|
||||
}
|
||||
.foot-count { font-weight: 600; color: var(--text); }
|
||||
.card-board-link.btn-sm { text-decoration: none; }
|
||||
.mini-progress { display: flex; gap: 4px; flex-wrap: wrap; }
|
||||
.mini-progress .chip { white-space: nowrap; }
|
||||
.forecast-line { white-space: nowrap; }
|
||||
.chip {
|
||||
padding: 2px 9px;
|
||||
border-radius: var(--radius-full);
|
||||
font-size: 0.7rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
.card-progress { margin-bottom: 12px; }
|
||||
.progress-head {
|
||||
display: flex; justify-content: space-between; align-items: baseline;
|
||||
gap: 10px; margin-bottom: 6px;
|
||||
font-size: 0.78rem; color: var(--text-2);
|
||||
}
|
||||
.progress-head span:last-child { font-weight: 600; color: var(--text); white-space: nowrap; }
|
||||
.progress {
|
||||
height: 6px;
|
||||
border-radius: var(--radius-full);
|
||||
@@ -1387,7 +1407,24 @@ body.nav-closed-admin [data-group="admin"] .side-chevron { transform: rotate(-90
|
||||
.theme-toggle-foot:hover { color: var(--text); }
|
||||
.side-foot {
|
||||
margin-top: auto; padding-top: 12px;
|
||||
display: flex; align-items: center; justify-content: space-between; gap: 8px;
|
||||
display: flex; flex-direction: column; align-items: stretch; gap: 8px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
.side-foot-user { display: flex; align-items: center; gap: 8px; }
|
||||
.side-foot-user .side-profile { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
||||
.side-logout.logout-icon {
|
||||
width: 36px; height: 36px; flex-shrink: 0;
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
border: 1px solid var(--border); border-radius: var(--radius-sm);
|
||||
}
|
||||
.side-logout.logout-icon:hover { border-color: var(--accent, #0d9488); }
|
||||
.side-foot-meta {
|
||||
display: flex; justify-content: space-between; align-items: center; gap: 8px;
|
||||
}
|
||||
.fresh-dot {
|
||||
width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0;
|
||||
background: var(--accent-2); display: inline-block; margin-right: 6px;
|
||||
vertical-align: 1px;
|
||||
}
|
||||
.side-collapse {
|
||||
/* Depuis la 0.22.0 : bouton icône seule dans l'en-tête de la
|
||||
@@ -1403,7 +1440,7 @@ body.nav-closed-admin [data-group="admin"] .side-chevron { transform: rotate(-90
|
||||
color: var(--text); border-color: var(--accent, #0d9488);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.side-fresh { color: var(--text-3); font-size: 0.76rem; white-space: nowrap; }
|
||||
.side-fresh { color: var(--text-3); font-size: 0.76rem; white-space: nowrap; display: inline-flex; align-items: center; }
|
||||
.side-logout { color: var(--text-3); font-size: 0.85rem; white-space: nowrap; }
|
||||
.side-logout:hover { color: var(--text); text-decoration: none; }
|
||||
.side-collapse-ico { display: inline-block; transition: transform 0.18s ease; }
|
||||
@@ -1412,7 +1449,9 @@ body.sidebar-collapsed .side-collapse-ico { transform: rotate(180deg); }
|
||||
body.sidebar-collapsed .side-label { display: none; }
|
||||
body.sidebar-collapsed .side-link { justify-content: center; padding-left: 0; padding-right: 0; }
|
||||
body.sidebar-collapsed .side-logo { justify-content: center; padding-left: 0; padding-right: 0; }
|
||||
body.sidebar-collapsed .side-foot { justify-content: center; }
|
||||
body.sidebar-collapsed .side-foot { align-items: center; }
|
||||
body.sidebar-collapsed .side-foot-meta { display: none; }
|
||||
body.sidebar-collapsed .side-foot-user { justify-content: center; }
|
||||
.side-backdrop {
|
||||
position: fixed; inset: 0; z-index: 190;
|
||||
background: rgba(4, 6, 10, 0.55);
|
||||
@@ -1423,12 +1462,30 @@ body.sidebar-collapsed .side-foot { justify-content: center; }
|
||||
body.with-sidebar, body.with-sidebar.sidebar-collapsed { padding-left: 0; }
|
||||
.sidebar { width: 264px; transform: translateX(-105%); box-shadow: none; }
|
||||
body.sidebar-open .sidebar { transform: none; box-shadow: 18px 0 44px rgba(0,0,0,.5); }
|
||||
/* Tiroir mobile : l'état « replié » mémorisé sur desktop ne doit
|
||||
JAMAIS rétrécir le tiroir. Avant, `body.sidebar-collapsed
|
||||
.sidebar { width: 68px; }` (spécificité supérieure à `.sidebar`)
|
||||
gagnait en mobile : libellés tronqués par overflow-x hidden,
|
||||
exactement la capture image 1. Le tiroir fait 264 px, replié
|
||||
ou non, et le fond ne défile plus derrière lui. */
|
||||
body.sidebar-collapsed .sidebar,
|
||||
body.sidebar-open.sidebar-collapsed .sidebar { width: 264px; }
|
||||
body.sidebar-open { overflow: hidden; }
|
||||
body.sidebar-collapsed .side-label { display: initial; }
|
||||
body.sidebar-collapsed .side-link { justify-content: flex-start; padding-left: 10px; padding-right: 10px; }
|
||||
body.sidebar-collapsed .side-logo { justify-content: flex-start; padding-left: 8px; padding-right: 8px; }
|
||||
body.sidebar-collapsed .side-head { flex-direction: row; padding: 4px 6px 12px; }
|
||||
body.sidebar-collapsed .side-search { margin-left: auto; }
|
||||
.side-foot { display: none; }
|
||||
/* Pied visible dans le tiroir (avant : display:none en mobile —
|
||||
aucun moyen de voir la version, la fraîcheur, le thème ni de
|
||||
sortir depuis un téléphone). */
|
||||
.side-foot { display: flex; }
|
||||
body.sidebar-collapsed .side-foot { align-items: stretch; }
|
||||
body.sidebar-collapsed .side-foot-meta { display: flex; }
|
||||
body.sidebar-collapsed .side-foot-user { justify-content: flex-start; }
|
||||
.topbar-search { display: inline-flex; }
|
||||
/* Menus draco (vendangés) : jamais plus larges que l'écran. */
|
||||
header.draco-bar .draco-compte-menu, header.draco-bar .draco-apps-menu { max-width: calc(100vw - 20px); }
|
||||
}
|
||||
|
||||
/* ── Dashboard vague 2 (0.17.0, #17 #18 #20) + page Projet (#25) ── */
|
||||
@@ -1443,12 +1500,25 @@ 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; }
|
||||
.ms-block { margin: 10px 0 2px; }
|
||||
.ms-block {
|
||||
margin: 14px 0 2px; padding-top: 12px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
.ms-head {
|
||||
display: flex; justify-content: space-between; align-items: baseline;
|
||||
gap: 10px; margin-bottom: 6px;
|
||||
}
|
||||
.ms-kicker {
|
||||
font-size: 0.68rem; text-transform: uppercase; letter-spacing: 0.6px;
|
||||
color: var(--text-3); font-weight: 700;
|
||||
}
|
||||
.ms-block .ms-line { margin-bottom: 2px; }
|
||||
.ms-line { display: flex; justify-content: space-between; align-items: baseline; gap: 10px; }
|
||||
.ms-title { font-weight: 600; font-size: 0.92rem; }
|
||||
.ms-due { font-size: 0.8rem; color: var(--text-2); white-space: nowrap; }
|
||||
@@ -1484,7 +1554,7 @@ body.sidebar-collapsed .side-foot { justify-content: center; }
|
||||
.proj-notes-body { white-space: pre-wrap; font-size: 0.93rem; background: var(--surface-2); border-radius: 10px; padding: 12px 14px; }
|
||||
|
||||
/* ── Dashboard vague 2 suite (0.18.0, #19 #21 #22 #23) ── */
|
||||
.activity-line { margin-top: 8px; font-size: 0.82rem; color: var(--text-2); }
|
||||
.activity-line { margin-top: 10px; font-size: 0.78rem; color: var(--text-3); }
|
||||
/* Barre d'outils unique du dashboard (0.20.0) : onglets
|
||||
segmentés, filtre, tri, regroupement et vue dans UN cadre. */
|
||||
.dash-toolbar {
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 240">
|
||||
<!-- TikTak — deux arcs (les deux temps), coche-flèche. Bleu #0076fb, orange #fc750c.
|
||||
Géométrie mesurée sur le visuel validé par Bruno (variante B). -->
|
||||
<path d="M 38.2 101.1 A 84 84 0 0 1 201.8 101.1" fill="none" stroke="#0076fb" stroke-width="28" stroke-linecap="round"/>
|
||||
<path d="M 201.5 140.3 A 84 84 0 0 1 38.5 140.3" fill="none" stroke="#fc750c" stroke-width="28" stroke-linecap="round"/>
|
||||
<polyline points="80,129 106,146 163,84" fill="none" stroke="#0076fb" stroke-width="24" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 128 80 L 163 84 L 167 122" fill="none" stroke="#0076fb" stroke-width="22" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 760 B |
@@ -369,7 +369,10 @@
|
||||
* de l'agent commun (URL agent résolue — explicite > -dev >
|
||||
* défaut prod ; aucun hôte spark-dev inventé). data-app="agent"
|
||||
* l'intègre à refreshMenuHrefs/applyUrls/setCurrent comme les
|
||||
* autres items ; l'icône ✦ reprend la couleur agent du CSS. */
|
||||
* autres items ; son icône CLONE celle de l'onglet agent
|
||||
* (2026-10-29, logos barre-apps, corrigé le même jour) : le logo
|
||||
* fourni, tel quel, jamais redessiné — le CSS --agent (rose) des
|
||||
* autres éléments de la barre n'est pas changé ici. */
|
||||
var sep = document.createElement("div");
|
||||
sep.className = "draco-menu-sep";
|
||||
sep.setAttribute("aria-hidden", "true");
|
||||
@@ -379,10 +382,18 @@
|
||||
sparkItem.href = resolvedUrls.agent || "#";
|
||||
sparkItem.dataset.app = "agent";
|
||||
sparkItem.setAttribute("role", "menuitem");
|
||||
var sparkIco = document.createElement("span");
|
||||
sparkIco.className = "draco-apptab-ico";
|
||||
sparkIco.setAttribute("style", "--app:#c2437e");
|
||||
sparkIco.textContent = "✦";
|
||||
var sparkIco;
|
||||
var agentTab = document.querySelector('.draco-apptab[data-app="agent"]');
|
||||
var agentIco = agentTab ? agentTab.querySelector(".draco-apptab-ico") : null;
|
||||
if (agentIco) {
|
||||
sparkIco = agentIco.cloneNode(true);
|
||||
} else {
|
||||
sparkIco = document.createElement("span");
|
||||
sparkIco.className = "draco-apptab-ico";
|
||||
sparkIco.setAttribute("style", "--app:#d21f2c");
|
||||
sparkIco.setAttribute("data-icon", "agent");
|
||||
sparkIco.textContent = "✦";
|
||||
}
|
||||
sparkItem.appendChild(sparkIco);
|
||||
var sparkTxt = document.createElement("span");
|
||||
sparkTxt.className = "draco-apps-item-txt";
|
||||
|
||||
+12
-2
@@ -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');
|
||||
}
|
||||
@@ -63,8 +63,18 @@
|
||||
});
|
||||
}
|
||||
|
||||
/* En mobile, le bouton de repli dans le tiroir FERME le tiroir :
|
||||
replier un tiroir de 264 px n'a pas de sens, et avant ce
|
||||
correctif il basculait l'état desktop (tiroir 68 px). */
|
||||
var sideLinks = document.querySelectorAll('.side-link');
|
||||
sideLinks.forEach(function (link) {
|
||||
link.addEventListener('click', function () {
|
||||
if (window.innerWidth <= 900) closeDrawer();
|
||||
});
|
||||
});
|
||||
if (collapseBtn) {
|
||||
collapseBtn.addEventListener('click', function () {
|
||||
if (window.innerWidth <= 900) { closeDrawer(); return; }
|
||||
var collapsed = document.body.classList.toggle('sidebar-collapsed');
|
||||
try {
|
||||
localStorage.setItem('kanbante-sidebar', collapsed ? 'collapsed' : 'open');
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,35 @@
|
||||
"""Mobile : tiroir, pied et menu compte.
|
||||
|
||||
Bugs signalés sur captures (29 octobre 2026) :
|
||||
- tiroir rétréci à 68 px en mobile quand l'état « replié » desktop
|
||||
était mémorisé (spécificité de `body.sidebar-collapsed .sidebar`) ;
|
||||
- pied de sidebar masqué en mobile (`display: none`) ;
|
||||
- Profil / Déconnexion du menu compte draco sans effet (onAction
|
||||
ne les prenait pas en charge).
|
||||
"""
|
||||
|
||||
|
||||
def test_tiroir_mobile_largeur_fixe():
|
||||
css = open("static/css/style.css", encoding="utf-8").read()
|
||||
assert "body.sidebar-collapsed .sidebar,\n body.sidebar-open.sidebar-collapsed .sidebar { width: 264px; }" in css
|
||||
assert "body.sidebar-open { overflow: hidden; }" in css
|
||||
|
||||
|
||||
def test_pied_visible_en_mobile():
|
||||
css = open("static/css/style.css", encoding="utf-8").read()
|
||||
assert ".side-foot { display: none; }" not in css
|
||||
assert "body.sidebar-collapsed .side-foot-meta { display: flex; }" in css
|
||||
|
||||
|
||||
def test_menu_compte_draco_fonctionnel():
|
||||
tpl = open("app/templates/_draco_bar.html", encoding="utf-8").read()
|
||||
assert 'act === "profil"' in tpl
|
||||
assert 'location.href = "/profile"' in tpl
|
||||
assert 'act === "deconnexion"' in tpl
|
||||
assert 'location.href = "/logout"' in tpl
|
||||
|
||||
|
||||
def test_tiroir_ferme_en_mobile():
|
||||
js = open("static/js/nav.js", encoding="utf-8").read()
|
||||
assert "window.innerWidth <= 900" in js
|
||||
assert "querySelectorAll('.side-link')" in js
|
||||
@@ -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.
|
||||
|
||||
@@ -19,7 +19,7 @@ def test_activity_line_recent_week(client, fake_gitea):
|
||||
from_column="backlog", to_column="todo",
|
||||
created_at=_ts(0))
|
||||
page = client.get("/")
|
||||
assert "2 fermée(s) · 1 déplacement(s) cette semaine" in page.text
|
||||
assert "Cette semaine : 2 fermée(s) · 1 déplacement(s)" in page.text
|
||||
|
||||
|
||||
def test_activity_line_idle_project(client, fake_gitea):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
"""Tuile Projets et pied de sidebar : ergonomie refondue.
|
||||
|
||||
Tuile : en-tête groupé, avancement labellisé, jalon étiqueté,
|
||||
pied en 2 rangées avec répartition labellisée par colonne.
|
||||
Pied : Sortir icône seule, fraîcheur en pastille discrète.
|
||||
"""
|
||||
|
||||
|
||||
def test_tuile_structure(client, fake_gitea):
|
||||
page = client.get("/")
|
||||
assert 'class="card-header-left"' in page.text
|
||||
assert 'class="card-header-actions"' in page.text
|
||||
assert 'class="foot-main"' in page.text
|
||||
assert 'class="foot-dist"' in page.text
|
||||
# Répartition, avancement et jalon sont conditionnels (stats /
|
||||
# jalon) : on les vérifie dans le gabarit, pas dans la page de
|
||||
# test sans données.
|
||||
tpl = open("app/templates/dashboard.html", encoding="utf-8").read()
|
||||
assert 'aria-label="Répartition par colonne"' in tpl
|
||||
assert 'class="progress-head"' in tpl
|
||||
assert "Avancement" in tpl
|
||||
assert 'class="ms-kicker">Prochain jalon' in tpl
|
||||
assert "COLUMN_LABELS[col]" in tpl
|
||||
|
||||
|
||||
def test_pied_sidebar_structure():
|
||||
tpl = open("app/templates/_header.html", encoding="utf-8").read()
|
||||
assert 'class="side-foot-user"' in tpl
|
||||
assert 'class="side-foot-meta"' in tpl
|
||||
assert 'class="side-logout logout-icon"' in tpl
|
||||
assert 'aria-label="Se déconnecter"' in tpl
|
||||
assert " Sortir</span>" not in tpl
|
||||
assert 'class="fresh-dot"' in tpl
|
||||
css = open("static/css/style.css", encoding="utf-8").read()
|
||||
assert ".side-foot-meta" in css
|
||||
assert ".fresh-dot" in css
|
||||
assert ".logout-icon" in css
|
||||
Reference in New Issue
Block a user