Compare commits

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

+8
View File
@@ -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

+16 -5
View File
@@ -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
View File
@@ -46,10 +46,10 @@
l'en-tête, lui, n'avait qu'un texte. */
var ico = themeBtn.querySelector('[data-theme-ico]');
var lbl = themeBtn.querySelector('[data-theme-label]');
if (ico) ico.textContent = light ? '🌙' : '☀️';
if (ico) ico.textContent = light ? '☾' : '☀';
if (lbl) lbl.textContent = light ? 'Mode sombre' : 'Mode clair';
if (!ico && !lbl) {
themeBtn.textContent = light ? '🌙 Mode sombre' : '☀️ Mode clair';
themeBtn.textContent = light ? '☾ Mode sombre' : '☀ Mode clair';
}
themeBtn.setAttribute('aria-pressed', light ? 'true' : 'false');
}
@@ -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');
+1 -1
View File
@@ -75,7 +75,7 @@
var item = document.createElement('span');
item.className = 'repo-item';
item.setAttribute('role', 'option');
var star = repo.starred ? '⭐ ' : '';
var star = repo.starred ? '★ ' : '';
var date = repo.updated_at ? repo.updated_at.slice(0, 10) : '';
var nameEl = document.createElement('span');
nameEl.className = 'repo-item-name';
+3 -3
View File
@@ -31,7 +31,7 @@
var data = await res.json();
if (!data.ok) throw new Error(data.error || 'échec');
btn.setAttribute('data-starred', want ? '1' : '0');
btn.textContent = want ? '⭐' : '☆';
btn.textContent = want ? '★' : '☆';
btn.title = want ? 'Désépingler (étoile Gitea)' : 'Épingler (étoile Gitea)';
var card = btn.closest('.project-card');
if (card) {
@@ -44,9 +44,9 @@
refreshPinnedVisibility();
}
} catch (err) {
btn.textContent = '⚠️';
btn.textContent = '!';
setTimeout(function () {
btn.textContent = btn.getAttribute('data-starred') === '1' ? '⭐' : '☆';
btn.textContent = btn.getAttribute('data-starred') === '1' ? '★' : '☆';
}, 1600);
} finally {
btn.disabled = false;
+36
View File
@@ -0,0 +1,36 @@
"""Icônes monochromes (#59, suite) : plus d'emoji colorés dans les
gabarits applicatifs (hors barre Fusion partagée, favicons et
contenu utilisateur ), étoile et thème en symboles monochromes.
"""
def test_pas_d_emoji_colores_dans_les_gabarits():
import pathlib
emoji = ["🎯", "🔮", "🩺", "📖", "🏷️", "🧱", "➕", "📝", "💾", "📎", "💬",
"📄", "📲", "📤", "✍️", "⚡", "🔌", "📱", "📈", "💤", "👥", "🔭",
"📜", "🧾", "🧠", "👀", "📦", "🐌", "🧟", "🗝️", "🐙", "🍵", "🟢",
"🔴", "✅", "❌", "⭐", "🐢", "⏱️", "📊", "🗣️", "🛩️", "⚙️", "📰",
"🗄️", "🖥️", "🛠️", "🧭", "🎛️", "⌨️", "🔗", "🌆", "🚧", "🚫", "⏭️",
"🔁", "☀️", "🌙"]
for path in pathlib.Path("app/templates").glob("*.html"):
if path.name in ("_draco_bar.html",):
continue
text = path.read_text(encoding="utf-8")
for line in text.splitlines():
if 'rel="icon"' in line:
continue
for e in emoji:
assert e not in line, (path.name, e, line.strip()[:100])
def test_etoile_et_theme_monochromes(client, fake_gitea):
css = client.get("/static/css/style.css").text
# L'étoile n'est plus jaune : couleur texte, épinglée = texte plein.
# (#fbbf24 reste pour l'âge des cartes sur le wallboard, une
# information sémantique, pas une icône.)
assert '.star-btn[data-starred="1"]' in css
assert "color: var(--text-2);" in css
js = client.get("/static/js/stars.js").text
assert "⭐" not in js
nav = client.get("/static/js/nav.js").text
assert "☀️" not in nav and "🌙" not in nav
+35
View File
@@ -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
+6 -6
View File
@@ -8,7 +8,7 @@ def test_dashboard_shows_next_milestone(client, fake_gitea):
page = client.get("/")
assert page.status_code == 200
# Faux : Sprint 1, échéance 2026-10-20, 1 fermée / 2 au total.
assert "🎯 Sprint 1" in page.text
assert "Sprint 1" in page.text
assert "1/2 terminées" in page.text
assert "échéance" in page.text
@@ -75,12 +75,12 @@ def test_project_page(client, fake_gitea):
)
page = client.get("/project/Projets/demo")
assert page.status_code == 200
assert "🎯 Jalons ouverts" in page.text
assert "Jalons ouverts" in page.text
assert "Sprint 1" in page.text
assert "🩺 Santé du flux" in page.text
assert "🔮 Prévisions" in page.text
assert "🕘 Activité récente" in page.text
assert "📝 Notes du projet" in page.text
assert "Santé du flux" in page.text
assert "Prévisions" in page.text
assert "Activité récente" in page.text
assert "Notes du projet" in page.text
assert 'href="/board/Projets/demo"' in page.text
assert "/wall?repo=Projets/demo" in page.text
# Titre du projet et lien Gitea du faux.
+1 -1
View File
@@ -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):
+1 -1
View File
@@ -81,7 +81,7 @@ def test_forecast_page_and_milestone_banner(client, fake_gitea):
assert "Sprint 1" in r.text
board = client.get("/board/Projets/demo", params={"milestone": "Sprint 1"})
assert board.status_code == 200
assert "🔮" in board.text
assert "milestone-forecast" in board.text
assert "historique pour prévoir" in board.text # empty journal
assert "/sprint/Projets/demo" in board.text
+37
View File
@@ -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