Compare commits

..
112 Commits
Author SHA1 Message Date
bruno 895ee81d51 Merge pull request 'Mobile : tiroir 264 px, pied visible, menu compte fonctionnel' (#72) from mobile-72 into main
CI / test (push) Failing after 1s
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
bruno 95b0b219e8 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-28 23:40:44 -04:00
bruno 14488f662c Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 1s
2026-10-28 23:40:42 -04:00
bruno ec0174f9d4 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 0s
2026-10-28 23:40:41 -04:00
bruno 687c968f3e Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 0s
2026-10-28 23:40:39 -04:00
bruno c8fdf61cf3 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 0s
2026-10-28 23:40:37 -04:00
bruno 12e6db7fe2 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 1s
2026-10-28 23:40:35 -04:00
bruno f25862d838 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 0s
2026-10-28 23:40:33 -04:00
bruno dc1f00f24a Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 0s
2026-10-28 23:40:31 -04:00
bruno 1b8b83c005 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 0s
2026-10-28 23:40:29 -04:00
bruno 94900ccd53 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 1s
2026-10-28 23:40:27 -04:00
bruno 8719d0d6f8 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 1s
2026-10-28 23:40:26 -04:00
bruno 1bc0696d6d Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 0s
2026-10-28 23:40:24 -04:00
bruno 505a1e1542 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 1s
2026-10-28 23:40:22 -04:00
bruno 037f0e0b98 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 1s
2026-10-28 23:40:20 -04:00
bruno 4190ee825d Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 1s
2026-10-28 23:40:18 -04:00
bruno 1eed495342 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 0s
2026-10-28 23:40:16 -04:00
bruno e7ceb14d2b Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 1s
2026-10-28 23:40:14 -04:00
bruno f976d22d4b Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 0s
2026-10-28 23:40:12 -04:00
bruno a0b461f468 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 0s
2026-10-28 23:40:10 -04:00
bruno c2052959d1 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 1s
2026-10-28 23:40:09 -04:00
bruno e23dd499a9 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 0s
2026-10-28 23:40:07 -04:00
bruno c091189381 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 0s
2026-10-28 23:40:05 -04:00
bruno e49f498ca0 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 1s
2026-10-28 23:40:03 -04:00
bruno 9eb236ba63 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 1s
2026-10-28 23:40:01 -04:00
bruno e3a89710d7 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 1s
2026-10-28 23:39:59 -04:00
bruno 546e6fb81b Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 1s
2026-10-28 23:39:57 -04:00
bruno bcd9c2face Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 0s
2026-10-28 23:39:55 -04:00
bruno b4f3cce0df Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 0s
2026-10-28 23:39:53 -04:00
bruno 0ce5de46f3 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 0s
2026-10-28 23:39:51 -04:00
bruno cb056ee456 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 0s
2026-10-28 23:39:49 -04:00
bruno a01b6a484a Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 0s
2026-10-28 23:39:47 -04:00
bruno 2735905c7e Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 1s
2026-10-28 23:39:45 -04:00
bruno 8596ad5708 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 0s
2026-10-28 23:39:43 -04:00
bruno 642c7bbb2d Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 0s
2026-10-28 23:39:41 -04:00
bruno 7a3081c408 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 0s
2026-10-28 23:39:39 -04:00
bruno 21414de936 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 1s
2026-10-28 23:39:37 -04:00
bruno bb3da870a0 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Successful in 4m28s
2026-10-28 23:39:35 -04:00
bruno 7cf2a3fc25 Merge pull request 'Identité Kanbante : sidebar charbon, accent vert, icônes ligne, bouton thème en Administration (#54, #55)' (#56) from identite-verte-54-55 into main
CI / test (push) Successful in 4m0s
2026-10-28 23:13:34 -04:00
bruno 6602a070be 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-28 23:12:56 -04:00
bruno 9e1ff33698 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 0s
2026-10-28 23:12:54 -04:00
bruno 47dd9c06a1 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 0s
2026-10-28 23:12:52 -04:00
bruno 90b9189169 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 1s
2026-10-28 23:12:50 -04:00
bruno 91ddfcd3b3 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 0s
2026-10-28 23:12:48 -04:00
bruno 2c10040046 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 0s
2026-10-28 23:12:46 -04:00
bruno 32fbe1d137 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 0s
2026-10-28 23:12:44 -04:00
bruno 4d1757e3d2 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 4m8s
2026-10-28 23:12:41 -04:00
bruno 9dc62885b0 Merge pull request 'Navigation : accordéon Administration + bouton Dark/Light (#51, #52)' (#53) from nav-theme-51-52 into main
CI / test (push) Successful in 3m42s
2026-10-28 23:00:16 -04:00
bruno cc007dfa04 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-28 22:59:13 -04:00
bruno f0ca8b9ec6 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Successful in 4m24s
2026-10-28 22:59:11 -04:00
bruno 6a938cf778 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 0s
2026-10-28 22:59:09 -04:00
bruno c60694bf23 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 0s
2026-10-28 22:59:07 -04:00
bruno a08fa53bab Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 0s
2026-10-28 22:59:05 -04:00
bruno 1c81259e0d Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 0s
2026-10-28 22:59:03 -04:00
bruno 5749b1c718 Merge pull request 'Thème du site : look agent-dev / barre Fusion, fin du bleu/mauve (#49)' (#50) from theme-agent-49 into main
CI / test (push) Successful in 4m50s
2026-10-28 22:48:45 -04:00
bruno c35aafd2e2 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) Successful in 4m32s
2026-10-28 22:47:12 -04:00
bruno 219c614216 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 1s
2026-10-28 22:47:09 -04:00
bruno 0d6499ed1a Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 0s
2026-10-28 22:47:07 -04:00
bruno a09ae26d78 Page Projets : copie URL dans le menu ⋯ + refonte thème barre Fusion/Spark (#44, #45)
CI / test (push) Failing after 1s
2026-10-28 22:47:04 -04:00
52 changed files with 1109 additions and 520 deletions
+51
View File
@@ -1,3 +1,54 @@
## 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`.
- **#58 Boutons** : système primary uni (fini les dégradés), secondary, danger, icon 36px, états unifiés ; `.btn-save` et `.proj-primary` alignés sur le primary.
- **#59 Icônes** : emoji d'interface retirés des gabarits (cartes, tableaux, états vides, boutons, titres) ; épingler en SVG trait, recherche en SVG ; favicons et contenu utilisateur (titres/labels Gitea) inchangés.
- **#60 Accessibilité** : jeton `--accent-text` distinct (`#14b8a6` sombre, `#0f766e` clair, 5,47:1 sur blanc) pour tous les textes d'accent ; focus visible unifié.
- **#61 Typographie** : pile système assumée (Inter déclarée mais jamais chargée, pas de CDN tiers pour une app auto-hébergée) ; hiérarchie H1/H2 via `.page-title`.
- **#62 Pages** : Aujourd'hui et Focus reprennent l'en-tête `.page-head` / `.page-title` commun ; titre Aujourd'hui ramené à l'échelle xl.
- **#63 Navigation** : recherche en double supprimée de l'en-tête (palette Ctrl+K + recherche sidebar restent).
- **#64 Mobile** : cibles 44px sous 640px, en-tête et KPI compacts.
- **#65 États** : `.empty-state` / `.focus-empty` unifiés, classe `.skeleton` (pulsation, respect de `prefers-reduced-motion`).
- **#66 Formulaires** : classes `.field-error` / `.field-error-msg` (bordure rouge + message, jamais la couleur seule).
- **#67 Cartes** : titre clampé à 2 lignes, code discret, labels/chips en texte thème sur fond teinté `color-mix` (contraste garanti quelle que soit la couleur Gitea).
- **#68 Thème clair** : connexion et wallboard suivent le thème mémorisé (init avant rendu, comme `_header`) et ne restent plus sombres par défaut ; variantes claires ajoutées.
## 0.41.0 — 2026-10-28 — Identité Kanbante : sidebar charbon, accent vert, icônes ligne, bouton thème en Administration (#54, #55)
- **Sidebar (#54)** : fond charbon draco (`rgba(26, 28, 26, 0.96)`) à la place du bleu-noir `rgba(13, 16, 24)` oublié par le re-thème 0.39.0 — la barre latérale a désormais la même teinte que le reste du site, en sombre comme en clair (clair déjà blanc).
- **Accent vert (#54)** : le rose Spark cède la place au vert Kanbante — `--accent: #0d9488`, second ton `#14b8a6` (clair : `#0f766e`), anneaux, focus, dégradés et replis associés. La barre Fusion partagée n'est pas touchée (Spark rose, marques des autres apps).
- **Icônes (#54)** : les emoji de la barre latérale sont remplacés par des SVG fins en trait (stroke currentColor), style rail Spark, pour chaque entrée de navigation ; le logo Kanbante devient des colonnes kanban blanches sur fond vert sarcelle.
- **Bouton Dark/Light (#55)** : déplacé de l'en-tête applicatif vers la section Administration de la barre latérale (icône seule en rail replié, libellé explicite sinon). Comportement inchangé (bascule + choix mémorisé). Conséquence : seuls les administrateurs le voient, la section Administration leur étant réservée.
## 0.40.0 — 2026-10-28 — Navigation : section Administration + bouton Dark/Light (#51, #52)
- **Accordéon (#51)** : la section Administration ne fermait jamais — le JS posait `nav-closed-admin`, mais `style.css` ne définissait les règles de fermeture que pour les 4 autres sections. Les règles admin sont ajoutées (fermeture et rotation du chevron) : l'accordéon redevient exclusif.
- **Bouton Dark/Light (#52)** : bouton « ☀️ Mode clair / 🌙 Mode sombre » dans l'en-tête applicatif, qui bascule `data-theme` sur `<html>` et mémorise le choix (localStorage, appliqué avant le rendu par l'amorce de `_header.html`). Thème clair sur les jetons draco (fond #f2f2f0, surfaces blanches/grises, accent rose conservé, second ton assombri pour les liens), barre Fusion et panneau Spark qui suivent. Défaut inchangé : sombre. `nav.js` est désormais versionné (`?v=`), comme dashboard.js depuis la 0.38.1.
## 0.39.0 — 2026-10-28 — Thème du site : look agent-dev / barre Fusion (#49)
- **Re-thème complet** : `style.css` passe sur les jetons draco-barre (Console v7.2) — fond #101211, surfaces #1a1c1a / #242624 / #20221f, bordure #2e302c, textes #eceee9 / #a6aba2 / #8b9089, accent rose Spark #c2437e (second ton #e078a8). Fond plat, plus de halos ni de dégradés violet→bleu, y compris les valeurs de repli et l'écran de connexion.
- **Inchangé** : couleurs sémantiques (vert/jaune/rouge) et couleurs de colonnes choisies par l'utilisateur. Pas de mode clair : l'app force le sombre, comme la console de référence.
## 0.38.1 — 2026-10-28 — Page Projets : le correctif 0.38.0 atteint enfin le navigateur (#47)
- **Cache-busting (#47)** : `dashboard.html` charge désormais `style.css?v={{ version }}` et `dashboard.js?v={{ version }}`, comme les autres pages (admin, board, project) et la barre Fusion. Sans ça, le navigateur gardait l'ancien CSS (menu transparent, bouton de copie non stylé) et l'ancien JS (aucun gestionnaire de copie) alors que le HTML était déjà neuf — c'est pourquoi la 0.38.0 semblait ne rien changer, bouton présent mais inerte. Le code 0.38.0 lui-même (jetons `--menu-bg` opaques, gestionnaire `data-copy-url`) était correct dans le dépôt.
+1 -1
View File
@@ -177,7 +177,7 @@ from urllib.parse import urlparse
BASE_DIR = Path(__file__).resolve().parent.parent
APP_VERSION = "0.38.1"
APP_VERSION = "0.45.0"
def _env_bool(name: str, default: bool) -> bool:
+4 -4
View File
@@ -13,14 +13,14 @@
{% if issue.labels %}
<div class="card-labels">
{% for label in issue.labels %}
<span class="label-tag" style="background: #{{ label.color }}30; color: #{{ label.color }}">
<span class="label-tag" style="background: color-mix(in srgb, #{{ label.color }} 25%, transparent); color: var(--text)">
{{ label.name }}
</span>
{% endfor %}
</div>
{% endif %}
{% if issue._code %}
<div class="card-code">⑂{% if issue._code.branches %} 🌿 {{ issue._code.branches }}{% endif %}{% if issue._code.commits %} · {{ issue._code.commits }} commit{{ 's' if issue._code.commits > 1 }}{% endif %}{% if issue._code.pr %} · PR #{{ issue._code.pr.number }} {{ issue._code.pr.label }}{% endif %}</div>
<div class="card-code">⑂{% if issue._code.branches %} {{ issue._code.branches }}{% endif %}{% if issue._code.commits %} · {{ issue._code.commits }} commit{{ 's' if issue._code.commits > 1 }}{% endif %}{% if issue._code.pr %} · PR #{{ issue._code.pr.number }} {{ issue._code.pr.label }}{% endif %}</div>
{% endif %}
<div class="card-meta">
{% if issue.assignee %}
@@ -30,8 +30,8 @@
{% set age = ages.get(issue.number) %}
{% if age and age.days >= aging_days %}
<span class="age-badge{% if age.days >= aging_days * 2 %} stale{% endif %}"
title="Dans cette colonne depuis le {{ age.since[:10] }}">⏳ {{ age.days }} j</span>
title="Dans cette colonne depuis le {{ age.since[:10] }}">{{ age.days }} j</span>
{% endif %}
<button type="button" class="pin-btn" title="Épingler au Focus du jour (/today)">📌</button>
<button type="button" class="pin-btn btn-icon" title="Épingler au Focus du jour (/today)" aria-label="Épingler au Focus du jour"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 4h6l-1 8 3 3H7l3-3z"/><path d="M12 15v5"/><path d="M9 20h6"/></svg></button>
</div>
</div>
+7
View File
@@ -103,6 +103,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). */
+64 -37
View File
@@ -1,3 +1,15 @@
<script>
/* Thème (#52) : appliqué avant le rendu de la barre, pour
éviter le flash sombre quand l'utilisateur a choisi le
clair. Défaut = sombre (les gabarits portent
data-theme="dark") ; seul un choix mémorisé bascule. */
try {
var _t = localStorage.getItem('kanbante-theme');
if (_t === 'light' || _t === 'dark') {
document.documentElement.setAttribute('data-theme', _t);
}
} catch (e) { /* mode privé : le thème ne survit qu'à la page */ }
</script>
{% include "_draco_bar.html" %}
{# ── Cadre applicatif partagé (issue #16, refonte 0.16.0) ─────────
Barre latérale unique pour TOUTES les pages applicatives :
@@ -9,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"),
("search", "/search", "", "Recherche"),
("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 %}
@@ -78,10 +90,10 @@
<aside class="sidebar" id="sidebar" aria-label="Navigation principale">
<div class="side-head">
<a href="/" class="side-logo" aria-label="Kanbante — accueil">
<span class="logo-mark">📋</span>
<span class="logo-mark"><svg viewBox="0 0 24 24" fill="#fff" aria-hidden="true"><rect x="3" y="4" width="5" height="16" rx="1"/><rect x="9.5" y="4" width="5" height="10" rx="1"/><rect x="16" y="4" width="5" height="13" rx="1"/></svg></span>
<span class="side-label gradient-text">Kanbante</span>
</a>
<button type="button" class="side-search palette-trigger" title="Recherche & commandes (Ctrl+K)" aria-label="Recherche et commandes (Ctrl+K)">🔍</button>
<button type="button" class="side-search palette-trigger" title="Recherche & commandes (Ctrl+K)" aria-label="Recherche et commandes (Ctrl+K)"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="M20 20l-4-4"/></svg></button>
<button type="button" class="side-collapse" id="side-collapse" title="Replier ou déplier le menu" aria-label="Replier ou déplier le menu"><span class="side-collapse-ico">«</span></button>
</div>
{% if current_workspace and user_workspaces and user_workspaces|length > 1 %}
@@ -95,6 +107,9 @@
</select>
</form>
{% endif %}
{# Icônes de navigation (#54) : SVG fins en trait, style rail
Spark — monochromes (currentColor), pas d'emoji. #}
{% macro nav_icon(key) %}<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">{% if key == 'home' %}<path d="M3 11l9-8 9 8"/><path d="M5 10v11h14V10"/><path d="M10 21v-6h4v6"/>{% elif key == 'focus' %}<circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="5"/><circle cx="12" cy="12" r="1" fill="currentColor"/>{% elif key == 'today' %}<path d="M7 18a5 5 0 0 1 10 0"/><path d="M3 18h18"/><path d="M12 9V6"/><path d="M5.5 11l1.5 1.5"/><path d="M18.5 11l-1.5 1.5"/>{% elif key == 'my' %}<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"/>{% elif key == 'briefing' %}<path d="M4 20V11"/><path d="M10 20V4"/><path d="M16 20v-8"/><path d="M3 20h18"/>{% elif key == 'standup' %}<path d="M4 5h16v11h-7l-4 4v-4H4z"/>{% elif key == 'digest' %}<path d="M6 3h12v18H6z"/><path d="M9 8h6"/><path d="M9 12h6"/><path d="M9 16h4"/>{% elif key == 'inflight' %}<path d="M3 12l19-9-7 18-4-6z"/><path d="M11 15L22 3"/>{% elif key == 'forecast' %}<path d="M3 18l5-6 4 3 9-9"/><path d="M16 6h5v5"/>{% elif key == 'health' %}<path d="M12 20C7 16 3 13 3 9a4.5 4.5 0 0 1 8-2.5A4.5 4.5 0 0 1 21 9c0 4-4 7-9 11z"/>{% elif key == 'rules' %}<path d="M6 3h12v18H6z"/><path d="M9 12l2 2 4-4"/>{% elif key == 'activity' %}<path d="M4 4h16v16H4z"/><path d="M7 8h10"/><path d="M7 12h10"/><path d="M7 16h6"/>{% elif key == 'search' %}<circle cx="11" cy="11" r="7"/><path d="M20 20l-4-4"/>{% elif key == 'capture' %}<path d="M13 2L4 14h6l-1 8 10-13h-6z"/>{% elif key == 'archives' %}<path d="M3 8h18"/><path d="M5 8v12h14V8"/><path d="M9 8v5h6V8"/>{% elif key == 'wall' %}<path d="M3 5h18v11H3z"/><path d="M9 21h6"/><path d="M12 16v5"/>{% elif key == 'admin' %}<path d="M4 4h7v7H4z"/><path d="M13 4h7v7h-7z"/><path d="M4 13h7v7H4z"/><path d="M13 13h7v7h-7z"/>{% elif key == 'admin_connections' %}<path d="M9 3v5"/><path d="M15 3v5"/><path d="M7 8h10v5a5 5 0 0 1-10 0z"/><path d="M12 18v3"/>{% elif key == 'admin_workspaces' %}<circle cx="12" cy="12" r="9"/><path d="M15 9l-2 4-4 2 2-4z"/>{% elif key == 'admin_columns' %}<path d="M4 4h4v16H4z"/><path d="M10 4h4v16h-4z"/><path d="M16 4h4v16h-4z"/>{% elif key == 'admin_settings' %}<circle cx="12" cy="12" r="3"/><path d="M12 2v3"/><path d="M12 19v3"/><path d="M2 12h3"/><path d="M19 12h3"/><path d="M4.5 4.5l2 2"/><path d="M17.5 17.5l2 2"/><path d="M19.5 4.5l-2 2"/><path d="M6.5 17.5l-2 2"/>{% else %}<circle cx="12" cy="12" r="9"/>{% endif %}</svg>{% endmacro %}
<nav class="side-nav">
{% for gkey, group, links in nav_groups %}
<div class="side-section" data-group="{{ gkey }}">
@@ -106,26 +121,39 @@
<div class="side-acc-inner">
{% for key, href, icon, label in links %}
<a href="{{ href }}" class="side-link{% if active_nav == key %} active{% endif %}" title="{{ label }}">
<span class="side-ico">{{ icon }}</span>
<span class="side-ico">{{ nav_icon(key) }}</span>
<span class="side-label">{{ label }}</span>
</a>
{% 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-label" data-theme-label>Mode clair</span>
</button>
{% endif %}
</div>
</div>
</div>
{% 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 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>
@@ -138,9 +166,8 @@
{% 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 %}
<button type="button" class="nav-pill palette-trigger topbar-search" title="Recherche et commandes (Ctrl+K)">🔍</button>
</div>
<script src="/static/js/nav.js" defer></script>
<script src="/static/js/nav.js?v={{ version }}" defer></script>
</header>
+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>
+13 -13
View File
@@ -12,14 +12,14 @@
<main class="focus-main">
<div class="page-head">
<h1>🛠️ Administration</h1>
<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 err %}<p class="chip-warn">⚠️ {{ err }}</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="/archives">🗄️ Archives</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>
+8 -8
View File
@@ -12,11 +12,11 @@
<main class="focus-main">
<div class="page-head">
<h1>🧱 Colonnes du board</h1>
<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 err %}<p class="chip-warn">⚠️ {{ err }}</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>Comment marche le mapping</h2>
@@ -52,12 +52,12 @@
<form method="post" action="/admin/columns/{{ c.key }}/move" class="inline-form">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<input type="hidden" name="direction" value="up">
<button type="submit" class="btn-mini" title="Monter">⬆️</button>
<button type="submit" class="btn-mini" title="Monter">↑</button>
</form>
<form method="post" action="/admin/columns/{{ c.key }}/move" class="inline-form">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<input type="hidden" name="direction" value="down">
<button type="submit" class="btn-mini" title="Descendre">⬇️</button>
<button type="submit" class="btn-mini" title="Descendre">↓</button>
</form>
{% if c.kind == 'flow' %}
<form method="post" action="/admin/columns/{{ c.key }}/delete" class="inline-form" onsubmit="return confirm('Supprimer cette colonne ? Ses cartes iront dans la colonne choisie.');">
@@ -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 }}">
+6 -6
View File
@@ -12,15 +12,15 @@
<main class="focus-main">
<div class="page-head">
<h1>🔌 Connexions aux forges</h1>
<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 err %}<p class="chip-warn">⚠️ {{ err }}</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 %}
@@ -41,7 +41,7 @@
<p class="admin-actions">
<form method="post" action="/admin/connections/{{ c.id }}/test" class="inline-form">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<button type="submit" class="btn-mini">🧪 Tester la connexion</button>
<button type="submit" class="btn-mini">Tester la connexion</button>
</form>
<form method="post" action="/admin/connections/{{ c.id }}/toggle" class="inline-form">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
@@ -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
+3 -3
View File
@@ -12,11 +12,11 @@
<main class="focus-main">
<div class="page-head">
<h1>🎛️ Paramètres</h1>
<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 err %}<p class="chip-warn">⚠️ {{ err }}</p>{% endif %}
{% if msg %}<p class="chip-ok">{{ msg }}</p>{% endif %}
{% if err %}<p class="chip-warn">{{ err }}</p>{% endif %}
<section class="briefing-card">
<form method="post" action="/admin/settings">
+8 -8
View File
@@ -12,11 +12,11 @@
<main class="focus-main">
<div class="page-head">
<h1>🧭 Espaces de travail</h1>
<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 err %}<p class="chip-warn">⚠️ {{ err }}</p>{% endif %}
{% if msg %}<p class="chip-ok">{{ msg }}</p>{% endif %}
{% if err %}<p class="chip-warn">{{ err }}</p>{% endif %}
{% for d in details %}
{% set ws = d.ws %}
@@ -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>
+16 -16
View File
@@ -12,11 +12,11 @@
<main class="focus-main">
<div class="page-head">
<h1>🗄️ Archives</h1>
<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 err %}<p class="chip-warn">⚠️ {{ err }}</p>{% endif %}
{% if msg %}<p class="chip-ok">{{ msg }}</p>{% endif %}
{% if err %}<p class="chip-warn">{{ err }}</p>{% endif %}
{% macro actions(row, kind) %}
{% if can_write %}
@@ -26,7 +26,7 @@
<input type="hidden" name="conn_id" value="{{ row.conn_id }}">
<input type="hidden" name="repo_full_name" value="{{ row.full_name }}">
<input type="hidden" name="archived" value="1">
<button type="submit" class="btn-mini">📥 Archiver ici</button>
<button type="submit" class="btn-mini">Archiver ici</button>
</form>
{% endif %}
{% if kind == 'local' %}
@@ -35,7 +35,7 @@
<input type="hidden" name="conn_id" value="{{ row.conn_id }}">
<input type="hidden" name="repo_full_name" value="{{ row.full_name }}">
<input type="hidden" name="archived" value="0">
<button type="submit" class="btn-mini">♻️ Restaurer</button>
<button type="submit" class="btn-mini">Restaurer</button>
</form>
<form method="post" action="/archives/purge" class="inline-form" onsubmit="return confirm('Purger l\'état local (board, notes, épingles) de ce projet dans cet espace ? La forge n\'est pas touchée.');">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
@@ -50,7 +50,7 @@
<input type="hidden" name="conn_id" value="{{ row.conn_id }}">
<input type="hidden" name="repo_full_name" value="{{ row.full_name }}">
<input type="hidden" name="archived" value="1">
<button type="submit" class="btn-mini">🔒 Archiver sur la forge</button>
<button type="submit" class="btn-mini">Archiver sur la forge</button>
</form>
{% endif %}
{% if kind == 'forge' %}
@@ -59,14 +59,14 @@
<input type="hidden" name="conn_id" value="{{ row.conn_id }}">
<input type="hidden" name="repo_full_name" value="{{ row.full_name }}">
<input type="hidden" name="archived" value="0">
<button type="submit" class="btn-mini">🔓 Désarchiver sur la forge</button>
<button type="submit" class="btn-mini">Désarchiver sur la forge</button>
</form>
{% endif %}
<form method="post" action="/archives/export" class="inline-form">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<input type="hidden" name="conn_id" value="{{ row.conn_id }}">
<input type="hidden" name="repo_full_name" value="{{ row.full_name }}">
<button type="submit" class="btn-mini">⬇️ Récap (.md)</button>
<button type="submit" class="btn-mini">Récap (.md)</button>
</form>
{% endif %}
{% endmacro %}
@@ -74,19 +74,19 @@
{% 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 %}
</td>
<td>{{ row.updated_at or '—' }}{% if row.idle_days is not none %} <span class="muted">(il y a {{ row.idle_days }} j)</span>{% endif %}</td>
<td>{% if row.open_issues %}⚠️ {{ row.open_issues }} ouverte(s){% else %}0{% endif %}</td>
<td>{% if row.open_issues %}{{ row.open_issues }} ouverte(s){% else %}0{% endif %}</td>
<td class="admin-actions">{{ actions(row, kind) }}</td>
</tr>
{% 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,15 +96,15 @@
{% 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>{% if row.open_issues %}{{ row.open_issues }} ouverte(s){% else %}0{% endif %}</td>
<td class="admin-actions">{{ actions(row, 'suggested') }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<p><button type="submit" class="btn-save btn-inline">📥 Archiver localement la sélection</button></p>
<p><button type="submit" class="btn-save btn-inline">Archiver localement la sélection</button></p>
</form>
{% else %}
<p class="muted">Rien à proposer : aucun projet inactif au-delà du seuil (réglable : <code>archive_suggest_days</code> dans Administration → Paramètres).</p>
@@ -112,7 +112,7 @@
</section>
<section class="briefing-card">
<h2>📥 Archives locales de cet espace ({{ groups.local|length }})</h2>
<h2>Archives locales de cet espace ({{ groups.local|length }})</h2>
{% if groups.local %}
<table class="proj-table">
<thead><tr><th>Projet</th><th>Dernière activité</th><th>Issues ouvertes</th><th>Actions</th></tr></thead>
@@ -124,7 +124,7 @@
</section>
<section class="briefing-card">
<h2>🔒 Archivés sur la forge ({{ groups.forge|length }})</h2>
<h2>Archivés sur la forge ({{ groups.forge|length }})</h2>
{% if groups.forge %}
<table class="proj-table">
<thead><tr><th>Projet</th><th>Dernière activité</th><th>Issues ouvertes</th><th>Actions</th></tr></thead>
+19 -19
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>
@@ -44,19 +44,19 @@
<!-- Toolbar: text + label filters, and issue creation (Gitea-backed) -->
<div class="board-toolbar">
<input type="text" id="card-filter" placeholder="🔍 Filtrer les cartes... ( / )">
<input type="text" id="card-filter" placeholder="Filtrer les cartes... ( / )">
<select id="label-filter">
<option value="">Toutes les étiquettes</option>
{% for label in labels %}
<option value="{{ label.name }}">{{ label.name }}</option>
{% endfor %}
</select>
<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 %}
<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 %}
<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" %}
+10 -10
View File
@@ -12,13 +12,13 @@
<main class="focus-main">
<section class="briefing-card">
<h1>⚡ Capture éclair</h1>
<h1>Capture éclair</h1>
<p>Une idée, un bogue, une pensée — capturés en deux secondes, ils deviennent une issue dans le bon backlog.</p>
</section>
{% 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
@@ -41,7 +41,7 @@
<input type="text" name="repo_full_name" id="capture-repo" value="{{ capture_repo }}" placeholder="Projets/kanbante" required autocomplete="off" role="combobox" aria-expanded="false" aria-controls="repo-listbox" aria-autocomplete="list">
<span class="repo-list" id="repo-listbox" role="listbox" hidden></span>
</span>
<span class="repo-hint" id="repo-hint" hidden>⚠️ Aucun dépôt de ce nom dans Gitea — vérifie l'orthographe, la capture échouera sinon.</span>
<span class="repo-hint" id="repo-hint" hidden>Aucun dépôt de ce nom dans Gitea — vérifie l'orthographe, la capture échouera sinon.</span>
<span class="repo-tip">Astuce : tape un début, <kbd>Tab</kbd> complète segment par segment (<code>pr</code> → <code>Projets/</code> → <code>kan</code> → <code>kanbante</code>), ↑↓ + Entrée choisissent dans la liste.</span>
</label>
<label>Titre de l'issue
@@ -52,16 +52,16 @@
</label>
<label>Colonne
<select name="column">
<option value="backlog" selected>📥 Backlog</option>
<option value="todo">📋 À faire</option>
<option value="backlog" selected>Backlog</option>
<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>
+56 -45
View File
@@ -55,7 +55,7 @@
<button type="button" role="tab" :class="{ active: tab === 'active' }" @click="tab = 'active'">Actifs <span class="seg-count">{{ active_repos|length }}</span></button>
<button type="button" role="tab" :class="{ active: tab === 'inactive' }" @click="tab = 'inactive'">Inactifs <span class="seg-count">{{ inactive_repos|length }}</span></button>
<button type="button" role="tab" :class="{ active: tab === 'archived' }" @click="tab = 'archived'">Archivés <span class="seg-count">{{ archived_repos|length }}</span></button>
<a href="/archives" class="seg-link" title="Gérer les archives (locales et forge)">🗄️ Gérer</a>
<a href="/archives" class="seg-link" title="Gérer les archives (locales et forge)">Gérer</a>
</div>
<div class="search-bar toolbar-search">
<input type="text" id="project-search" placeholder="Filtrer les projets..." x-model="search" autocomplete="off">
@@ -64,7 +64,7 @@
<label class="dash-tool">Tri
<select id="dash-sort">
<option value="default">Par défaut</option>
<option value="review">🔍 En attente de revue ↓</option>
<option value="review">En attente de revue ↓</option>
<option value="updated">Activité Gitea récente</option>
<option value="activity">Activité kanban récente</option>
<option value="open">Issues ouvertes ↓</option>
@@ -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="/triage/{{ full }}">📥 Triage du backlog</a>
<a href="/health/{{ full }}">🩺 Santé</a>
<a href="/sprint/{{ full }}">📖 Récit de sprint</a>
<a href="/board/{{ full }}">Board</a>
<a href="/capture?repo={{ full|urlencode }}">Capturer ici</a>
<a href="/triage/{{ full }}">Triage du backlog</a>
<a href="/health/{{ full }}">Santé</a>
<a href="/sprint/{{ full }}">Récit de sprint</a>
{% if repo.html_url %}
<a href="{{ repo.html_url }}" target="_blank" rel="noopener">🔗 Gitea ↗</a>
<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: {{ COLUMN_COLORS[col] }}20; color: {{ COLUMN_COLORS[col] }}">
{{ 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>
@@ -228,21 +239,21 @@
<div x-show="tab === 'active'" class="project-grid" id="grid-active">
{% for repo in active_listed %}{{ project_card(repo) }}{% endfor %}
{% if not active_listed and not pinned_repos %}
<div class="empty-state">🌱 Aucun projet actif en ce moment. Les projets bougent dès qu'une issue ou un commit arrive dans Gitea.</div>
<div class="empty-state">Aucun projet actif en ce moment. Les projets bougent dès qu'une issue ou un commit arrive dans Gitea.</div>
{% endif %}
</div>
<div x-show="tab === 'inactive'" class="project-grid" id="grid-inactive">
{% for repo in inactive_listed %}{{ project_card(repo) }}{% endfor %}
{% if not inactive_listed %}
<div class="empty-state">✨ Rien d'endormi : tous vos projets ont eu de l'activité récemment.</div>
<div class="empty-state">Rien d'endormi : tous vos projets ont eu de l'activité récemment.</div>
{% endif %}
</div>
<div x-show="tab === 'archived'" class="project-grid" id="grid-archived">
{% for repo in archived_repos %}{{ project_card(repo, archived=True) }}{% endfor %}
{% if not archived_repos %}
<div class="empty-state">🗄️ Aucun projet archivé. L'archivage se fait dans Gitea (Réglages du dépôt).</div>
<div class="empty-state">Aucun projet archivé. L'archivage se fait dans Gitea (Réglages du dépôt).</div>
{% endif %}
</div>
</div>
+9 -9
View File
@@ -12,11 +12,11 @@
<main class="focus-main">
<section class="briefing-card">
<h1>🧠 Digest hebdo → ObsiGate</h1>
<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 %}
+1 -1
View File
@@ -10,7 +10,7 @@
{% include "_header.html" %}
<main class="error-main">
<div class="error-box">
<h2>⚠️ Quelque chose s'est mal passé</h2>
<h2>Quelque chose s'est mal passé</h2>
<p>{{ message }}</p>
<p><a href="/" class="btn-sm">← Retour au dashboard</a></p>
</div>
+4 -4
View File
@@ -11,9 +11,9 @@
{% include "_header.html" %}
<main class="focus-main">
<p class="focus-intro">Tout ce qui est <strong>En cours</strong> ou <strong>En révision</strong>, tous projets actifs confondus.</p>
<div class="page-head"><div class="page-title"><h1>Focus</h1><p class="page-sub">Tout ce qui est <strong>En cours</strong> ou <strong>En révision</strong>, tous projets actifs confondus.</p></div></div>
{% if not groups %}
<p class="focus-empty">🎉 Rien en cours ni en révision pour le moment.</p>
<p class="focus-empty">Rien en cours ni en révision pour le moment.</p>
{% endif %}
{% for group in groups %}
<section class="focus-group">
@@ -26,7 +26,7 @@
<div class="kanban-card focus-card">
<div class="card-id">
#{{ card.issue.number }}
<span class="chip" style="background: {{ column_colors[card.column] }}20; color: {{ column_colors[card.column] }}">
<span class="chip" style="background: color-mix(in srgb, {{ column_colors[card.column] }} 25%, transparent); color: var(--text)">
{{ column_labels[card.column] }}
</span>
{% if card.issue.html_url %}
@@ -37,7 +37,7 @@
{% if card.issue.labels %}
<div class="card-labels">
{% for label in card.issue.labels %}
<span class="label-tag" style="background: #{{ label.color }}30; color: #{{ label.color }}">
<span class="label-tag" style="background: color-mix(in srgb, #{{ label.color }} 25%, transparent); color: var(--text)">
{{ label.name }}
</span>
{% endfor %}
+10 -10
View File
@@ -12,15 +12,15 @@
<main class="focus-main">
<section class="briefing-card">
<h1>🔮 Prévisions — « on finit quand ? »</h1>
<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>
{% 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 %}
<span class="chip chip-total">{{ summary.total }} jalon(s) ouvert(s)</span>
{% if summary.late %}<span class="chip chip-late">{{ summary.late }} en retard à ce rythme</span>{% endif %}
{% if summary.ontime %}<span class="chip chip-ok">{{ summary.ontime }} dans les temps</span>{% endif %}
</p>
<div class="search-bar forecast-filter">
<input type="text" id="ms-filter" placeholder="🔍 Filtrer par projet ou jalon..." autocomplete="off">
<input type="text" id="ms-filter" placeholder="Filtrer par projet ou jalon..." autocomplete="off">
</div>
</section>
@@ -56,9 +56,9 @@
<td>{{ row.eta or '—' }}</td>
<td>
{% if row.verdict == 'late' %}
<span class="verdict verdict-late">⚠️ +{{ row.gap_days }} j</span>
<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,10 +75,10 @@
{% 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>
<div class="forecast-proj-name"><a href="/project/{{ project.full_name }}">{{ project.full_name }}</a></div>
<svg class="sparkline" viewBox="0 0 168 48" role="img" aria-label="Fermetures par semaine">
{% for bar in project.bars %}
{% set px = (bar.h * 0.42)|round|int %}
@@ -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 %}
+3 -3
View File
@@ -12,7 +12,7 @@
<main class="focus-main">
<section class="briefing-card">
<h1>🩺 Santé du flux</h1>
<h1>Santé du flux</h1>
<p>Tous les projets actifs, <strong>triés par sévérité</strong> — le rouge d'abord. Quelle colonne accumule (temps médian par colonne, d'après le journal) et quelles cartes sont devenues des <strong>zombies</strong>. Cliquez un projet pour le détail et les actions rapides.</p>
</section>
@@ -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>
+11 -11
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>
@@ -25,20 +25,20 @@
{% endif %}
{% if health is none %}
<section class="briefing-card">
<h1>🩺 {{ full_name }}</h1>
<h1>{{ full_name }}</h1>
<p>Projet introuvable.</p>
</section>
{% else %}
<section class="briefing-card">
<h1>🩺 {{ full_name }}</h1>
<h2>⏱️ Temps passé par colonne (journal)</h2>
<h1>{{ full_name }}</h1>
<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,23 +55,23 @@
</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 %}
</ul>
<p class="notes-hint">« Fermer » déplace la carte en Terminé (l'issue est fermée dans Gitea par la synchro habituelle). « Toujours pertinent ? » poste un commentaire du même texte sur l'issue.</p>
{% else %}
<p>Aucune carte zombie. Le flux est propre. ✨</p>
<p>Aucune carte zombie. Le flux est propre. </p>
{% endif %}
</section>
{% endif %}
+5 -5
View File
@@ -15,16 +15,16 @@
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: {{ column_colors[group.column] }}20; color: {{ column_colors[group.column] }}">
<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 %}
</span>
{% endfor %}
</div>
{% if not groups %}
<p class="focus-empty">🎉 Rien en vol pour le moment — aucun encours, aucune révision en attente.</p>
<p class="focus-empty">Rien en vol pour le moment — aucun encours, aucune révision en attente.</p>
{% endif %}
{% for group in groups %}
@@ -51,7 +51,7 @@
{% if card.issue.labels %}
<div class="card-labels">
{% for label in card.issue.labels %}
<span class="label-tag" style="background: #{{ label.color }}30; color: #{{ label.color }}">
<span class="label-tag" style="background: color-mix(in srgb, #{{ label.color }} 25%, transparent); color: var(--text)">
{{ label.name }}
</span>
{% endfor %}
@@ -62,7 +62,7 @@
<img src="{{ card.issue.assignee.avatar_url }}" class="avatar" alt="{{ card.issue.assignee.login }}">
{% endif %}
<span class="age-badge{% if card.age_days >= aging_days * 2 %} stale{% elif card.age_days >= aging_days %} warn{% endif %}"
title="Dans cette colonne depuis le {{ card.age_since[:10] }}">⏳ {{ card.age_days }} j dans la colonne</span>
title="Dans cette colonne depuis le {{ card.age_since[:10] }}">{{ card.age_days }} j dans la colonne</span>
</div>
</div>
{% endfor %}
+5 -4
View File
@@ -1,7 +1,8 @@
<!DOCTYPE html>
<html lang="fr" data-theme="dark">
<html lang="fr">
<head>
<meta charset="UTF-8">
<script>try{var t=localStorage.getItem("kanbante-theme");document.documentElement.setAttribute("data-theme",t==="light"?"light":"dark");}catch(e){document.documentElement.setAttribute("data-theme","dark");}</script>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="csrf-token" content="{{ csrf_token }}">
<title>Connexion — Kanbante</title>
@@ -11,12 +12,12 @@
<main class="login-wrap">
<section class="login-card">
<div class="login-brand">
<span class="login-mark" aria-hidden="true">📋</span>
<span class="login-mark" aria-hidden="true"><svg viewBox="0 0 24 24" fill="#fff" aria-hidden="true" style="width:20px;height:20px;display:block"><rect x="3" y="4" width="5" height="16" rx="1"/><rect x="9.5" y="4" width="5" height="10" rx="1"/><rect x="16" y="4" width="5" height="13" rx="1"/></svg></span>
<h1>Kanbante</h1>
<p class="login-sub">Pilotage de projets, adossé à votre Gitea</p>
</div>
{% if error %}
<p class="login-error" role="alert">⚠️ {{ error }}</p>
<p class="login-error" role="alert">{{ error }}</p>
{% endif %}
<form method="post" action="/login" class="login-form">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
@@ -29,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>
+7 -7
View File
@@ -12,7 +12,7 @@
<main class="focus-main">
{% if not user %}
<div class="empty-state">🔑 Aucun utilisateur Gitea résolu : définissez <code>GITEA_TOKEN</code> pour voir les issues qui vous sont assignées.</div>
<div class="empty-state">Aucun utilisateur Gitea résolu : définissez <code>GITEA_TOKEN</code> pour voir les issues qui vous sont assignées.</div>
{% else %}
<p class="focus-intro">Issues ouvertes assignées à <strong>{{ user.login }}</strong>, tous projets confondus — {{ total }} au total.</p>
@@ -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>
@@ -58,11 +58,11 @@
</div>
{% endif %}
<div class="empty-state" id="my-empty" hidden>🔍 Aucune issue ne correspond à ces filtres.
<div class="empty-state" id="my-empty" hidden>Aucune issue ne correspond à ces filtres.
<button type="button" id="my-empty-reset" class="btn-sm">↺ Réinitialiser les filtres</button>
</div>
{% if not groups %}
<div class="empty-state">🎉 Aucune issue ne vous est assignée pour le moment.</div>
<div class="empty-state">Aucune issue ne vous est assignée pour le moment.</div>
{% endif %}
{% for group in groups %}
<section class="focus-group" data-repo="{{ group.full_name }}">
@@ -77,13 +77,13 @@
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>
<span class="col-chip" style="background: {{ column_colors[row.column] }}20; color: {{ column_colors[row.column] }}">{{ column_labels[row.column] }}</span>
<span class="col-chip" style="background: color-mix(in srgb, {{ column_colors[row.column] }} 25%, transparent); color: var(--text)">{{ column_labels[row.column] }}</span>
{% for label in issue.labels or [] %}
<span class="label-tag" style="background: #{{ label.color }}30; color: #{{ label.color }}">{{ label.name }}</span>
<span class="label-tag" style="background: color-mix(in srgb, #{{ label.color }} 25%, transparent); color: var(--text)">{{ label.name }}</span>
{% endfor %}
<span class="activity-date">{{ (issue.updated_at or '')[:10] }}</span>
</div>
+11 -11
View File
@@ -12,12 +12,12 @@
<main class="focus-main">
<div class="page-head">
<h1>👤 Mon profil</h1>
<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 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 %}
{% 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 %}
<section class="briefing-card">
<h2>Informations</h2>
@@ -33,7 +33,7 @@
</section>
<section class="briefing-card">
<h2>🔑 Mot de passe</h2>
<h2>Mot de passe</h2>
{% if profile_user.id %}
<form method="post" action="/profile/password" class="admin-form">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
@@ -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,15 +97,15 @@
</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 }}">
<button type="submit" class="btn-mini">🧪 Tester</button>
<button type="submit" class="btn-mini">Tester</button>
</form>
<form method="post" action="/profile/connections/{{ c.id }}/delete" class="inline-form" onsubmit="return confirm('Supprimer cette connexion personnelle ?');">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
+24 -24
View File
@@ -15,7 +15,7 @@
<main class="project-main">
<div class="proj-head">
<div>
<h1>📦 {{ repo_info.name }}</h1>
<h1>{{ repo_info.name }}</h1>
<p class="proj-sub">{{ owner }} · {{ repo_info.description or 'Aucune description dans Gitea.' }}</p>
</div>
{% if health_dot %}
@@ -26,42 +26,42 @@
</div>
<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="/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="/board/{{ full_name }}" class="nav-pill proj-primary">Ouvrir le board</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>
{% 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">
<div class="ms-line">
<a href="/board/{{ full_name }}?milestone={{ ms.title|urlencode }}"><strong>{{ ms.title }}</strong></a>
{% if ms.due_label %}<span class="ms-due{% if ms.late %} late{% endif %}">{{ '⚠️ ' if ms.late }}échéance {{ ms.due_label }}</span>{% endif %}
{% if ms.due_label %}<span class="ms-due{% if ms.late %} late{% endif %}">{{ '' if ms.late }}échéance {{ ms.due_label }}</span>{% endif %}
</div>
{% if ms.total %}
<div class="progress ms-progress"><span style="width: {{ ms.pct }}%"></span></div>
<div class="ms-meta">{{ ms.closed }}/{{ ms.total }} terminées{% if ms.eta_label %} · 🔮 {{ ms.eta_label }}{% 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>
<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>
+7 -7
View File
@@ -12,7 +12,7 @@
<main class="focus-main">
<section class="briefing-card">
<h1>⚙️ Règles d'automatisation</h1>
<h1>Règles d'automatisation</h1>
<p>Des réflexes déclaratifs, évalués toutes les 15 minutes par le planificateur (et à la demande ci-dessous). Chaque action est journalisée comme événement <code>rule</code>. <strong>Garde-fou v1 : une règle ne peut ni fermer ni déplacer une carte entre colonnes</strong> — seulement étiqueter, notifier (ntfy) et remonter en tête de colonne.</p>
{% if not configured %}
<p>Aucune règle configurée. Définis <code>KANBANTE_RULES</code> en JSON, par exemple :</p>
@@ -25,15 +25,15 @@
<p class="notes-hint">Conditions : <code>column</code>, <code>min_age_days</code> (temps dans la colonne), <code>label</code> (présente), <code>wip_over</code> (colonne au-delà de N cartes). Actions : <code>add_label</code>, <code>notify</code>, <code>move_top</code>.</p>
{% endif %}
{% for err in errors %}
<p class="chip-warn">⚠️ {{ err }}</p>
<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>
+5 -5
View File
@@ -20,7 +20,7 @@
{% if q %}
<p class="focus-intro">{{ total }} résultat(s) pour « {{ q }} » — recherche Gitea, tous projets confondus.</p>
{% if not groups %}
<div class="empty-state">🔍 Aucune issue ne correspond. Essayez un mot plus court ou vérifiez l'orthographe.</div>
<div class="empty-state">Aucune issue ne correspond. Essayez un mot plus court ou vérifiez l'orthographe.</div>
{% endif %}
{% for group in groups %}
<section class="focus-group">
@@ -30,13 +30,13 @@
{% 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>
<span class="col-chip" style="background: {{ column_colors[row.column] }}20; color: {{ column_colors[row.column] }}">{{ column_labels[row.column] }}</span>
<span class="col-chip" style="background: color-mix(in srgb, {{ column_colors[row.column] }} 25%, transparent); color: var(--text)">{{ column_labels[row.column] }}</span>
{% for label in issue.labels or [] %}
<span class="label-tag" style="background: #{{ label.color }}30; color: #{{ label.color }}">{{ label.name }}</span>
<span class="label-tag" style="background: color-mix(in srgb, #{{ label.color }} 25%, transparent); color: var(--text)">{{ label.name }}</span>
{% endfor %}
<span class="activity-date">{{ (issue.updated_at or '')[:10] }}</span>
</div>
@@ -45,7 +45,7 @@
</section>
{% endfor %}
{% else %}
<div class="empty-state">🔍 Tapez un mot-clé : la recherche couvre les titres et corps d'issues de <strong>tous</strong> vos projets Gitea, ouverts comme fermés.</div>
<div class="empty-state">Tapez un mot-clé : la recherche couvre les titres et corps d'issues de <strong>tous</strong> vos projets Gitea, ouverts comme fermés.</div>
{% endif %}
</main>
+7 -7
View File
@@ -15,11 +15,11 @@
<main class="focus-main">
{% if markdown %}
<section class="briefing-card">
<h1>📖 {{ milestone_title }}</h1>
<h1>{{ milestone_title }}</h1>
<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>
<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>
</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', () => {
@@ -45,11 +45,11 @@
</script>
{% else %}
<section class="briefing-card">
<h1>📖 Récit de sprint — {{ full_name }}</h1>
<h1>Récit de sprint — {{ full_name }}</h1>
<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 %}
+4 -4
View File
@@ -12,12 +12,12 @@
<main class="focus-main">
<section class="briefing-card">
<h1>🗣️ Standup automatique</h1>
<h1>Standup automatique</h1>
<p>Écrit par le journal, pas par la mémoire : fermé/déplacé <strong>hier</strong>, focus <strong>aujourd'hui</strong>, cartes qui vieillissent en QA/Révision en <strong>blocages</strong>.</p>
<p>
<button type="button" class="btn-save btn-inline" id="standup-copy">📋 Copier le standup</button>
<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>
+10 -11
View File
@@ -11,8 +11,7 @@
{% include "_header.html" %}
<main class="today-main">
<h1 class="today-title">🌅 {{ date_label }}</h1>
<p class="today-sub">Trois cartes, pas une de plus. C'est le plan de la journée.</p>
<div class="page-head"><div class="page-title"><h1 class="today-title">{{ date_label }}</h1><p class="page-sub">Trois cartes, pas une de plus. C'est le plan de la journée.</p></div></div>
{% if picks %}
<section class="today-picks">
@@ -22,24 +21,24 @@
<span class="today-card-repo">{{ pick.repo }}</span>
<span class="today-card-col">{{ pick.column_label }}</span>
</div>
<h2>{% if pick.state == 'closed' %}🎉 {% endif %}#{{ pick.number }} — {{ pick.title }}</h2>
<h2>{% if pick.state == 'closed' %}{% endif %}#{{ pick.number }} — {{ pick.title }}</h2>
<div class="today-card-actions">
<a href="/board/{{ pick.repo }}">📋 Ouvrir le board</a>
<a href="/board/{{ pick.repo }}">Ouvrir le board</a>
{% if pick.html_url %}<a href="{{ pick.html_url }}" target="_blank" rel="noopener">Gitea ↗</a>{% endif %}
<button type="button" class="btn-sm" data-unpin data-repo="{{ pick.repo }}" data-number="{{ pick.number }}">📌 Retirer</button>
<button type="button" class="btn-sm" data-unpin data-repo="{{ pick.repo }}" data-number="{{ pick.number }}">Retirer</button>
</div>
</article>
{% endfor %}
</section>
{% else %}
<section class="briefing-card">
<p>Aucune carte épinglée pour aujourd'hui. Épingle-en jusqu'à 3 depuis un board (bouton 📌 sur les cartes) ou depuis les candidates ci-dessous.</p>
<p>Aucune carte épinglée pour aujourd'hui. Épingle-en jusqu'à 3 depuis un board (bouton épingler sur les cartes) ou depuis les candidates ci-dessous.</p>
</section>
{% endif %}
{% 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>
@@ -50,12 +49,12 @@
{% 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>
<span>{{ pick.repo }} #{{ pick.number }} — {{ pick.title }} <em>({{ pick.column_label }})</em></span>
{% if picks|length < 3 %}<button type="button" class="btn-sm" data-pin data-repo="{{ pick.repo }}" data-number="{{ pick.number }}">📌 Reprendre aujourd'hui</button>{% endif %}
{% if picks|length < 3 %}<button type="button" class="btn-sm" data-pin data-repo="{{ pick.repo }}" data-number="{{ pick.number }}">Reprendre aujourd'hui</button>{% endif %}
</li>
{% endfor %}
</ul>
@@ -64,12 +63,12 @@
{% 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>
<span>{{ card.repo }} #{{ card.number }} — {{ card.title }} <em>({{ card.column_label }})</em></span>
<button type="button" class="btn-sm" data-pin data-repo="{{ card.repo }}" data-number="{{ card.number }}">📌 Épingler</button>
<button type="button" class="btn-sm" data-pin data-repo="{{ card.repo }}" data-number="{{ card.number }}">Épingler</button>
</li>
{% endfor %}
</ul>
+3 -3
View File
@@ -12,20 +12,20 @@
<main class="focus-main">
<section class="briefing-card">
<h1>📥 Mode Triage</h1>
<h1>Mode Triage</h1>
<p>Le backlog se vide carte par carte, au clavier : une décision par carte, aucune souris requise. Choisis un projet pour commencer une session.</p>
</section>
{% if projects %}
<div class="triage-projects">
{% for p in projects %}
<a class="triage-project" href="/triage/{{ p.full_name }}">
<span class="triage-project-name">📂 {{ p.full_name }}</span>
<span class="triage-project-name">{{ p.full_name }}</span>
<span class="triage-project-count">{{ p.backlog }} carte{{ 's' if p.backlog > 1 }} au backlog</span>
</a>
{% endfor %}
</div>
{% else %}
<div class="briefing-card"><p>🎉 Aucun backlog à trier : tous les projets actifs sont à zéro. Inbox zero, déjà.</p></div>
<div class="briefing-card"><p>Aucun backlog à trier : tous les projets actifs sont à zéro. Inbox zero, déjà.</p></div>
{% endif %}
</main>
<footer>
+15 -15
View File
@@ -12,7 +12,7 @@
<main class="triage-main" data-repo="{{ full_name }}">
<div class="triage-head">
<h1>📥 Triage — {{ full_name }}</h1>
<h1>Triage — {{ full_name }}</h1>
<div class="triage-progress-wrap">
<div class="triage-progress"><div class="triage-progress-bar" id="triage-bar"></div></div>
<span class="triage-progress-label" id="triage-progress-label"></span>
@@ -32,32 +32,32 @@
</section>
<section class="triage-done" id="triage-done" hidden>
<h2>🎉 Session terminée</h2>
<h2>Session terminée</h2>
<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="/board/{{ full_name }}">Retour au board</a>
<a class="btn-save btn-inline" href="/triage/{{ full_name }}">Nouvelle session</a>
</p>
</section>
<div class="triage-status" id="triage-status" role="status"></div>
<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="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="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="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>
</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>
+9 -8
View File
@@ -2,7 +2,8 @@
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script>try{var t=localStorage.getItem("kanbante-theme");document.documentElement.setAttribute("data-theme",t==="light"?"light":"dark");}catch(e){document.documentElement.setAttribute("data-theme","dark");}</script>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Kanbante — Wallboard</title>
<link rel="stylesheet" href="/static/css/style.css">
</head>
@@ -13,7 +14,7 @@
<section class="wall-slide{% if loop.first %} active{% endif %}">
<header class="wall-head">
<div>
<div class="wall-kicker">📂 Projet</div>
<div class="wall-kicker">Projet</div>
<h1 class="wall-project">{{ slide.full_name }}</h1>
</div>
<div class="wall-clock">
@@ -41,7 +42,7 @@
<span class="wall-card-col" style="--col: {{ column_colors[card.column] }}">{{ card.column_label }}</span>
<span class="wall-card-title">#{{ card.number }} · {{ card.title }}</span>
{% if card.age_days is not none and card.age_days >= 1 %}
<span class="wall-card-age{% if card.aging %} hot{% endif %}">⏳ {{ card.age_days }} j</span>
<span class="wall-card-age{% if card.aging %} hot{% endif %}">{{ card.age_days }} j</span>
{% endif %}
</li>
{% endfor %}
@@ -50,12 +51,12 @@
{% endif %}
</ul>
{% else %}
<p class="wall-idle">🌿 Rien en vol pour le moment — tout est au Backlog ou terminé.</p>
<p class="wall-idle">Rien en vol pour le moment — tout est au Backlog ou terminé.</p>
{% endif %}
<footer class="wall-foot">
<span>✅ Fermées aujourd'hui : <strong>{{ slide.closed_today }}</strong></span>
<span>📋 Ouvertes : <strong>{{ slide.open_total }}</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>
</section>
@@ -66,12 +67,12 @@
<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 %}
<div class="wall-empty">
<h1>🖥️ Wallboard</h1>
<h1>Wallboard</h1>
<p>Aucun projet actif à afficher{% if single %} pour ce dépôt{% endif %}.</p>
<p class="wall-empty-hint">Astuce : <code>/wall?repo=Proprietaire/depot</code> fige un seul projet.</p>
</div>
+370 -180
View File
File diff suppressed because it is too large Load Diff
+39
View File
@@ -34,8 +34,47 @@
if (window.innerWidth > 900) closeDrawer();
});
/* Bascule Dark/Light (#52) : le choix est mémorisé et
l'amorce de _header.html l'a déjà appliqué avant le rendu.
Ici, on ne fait que synchroniser le libellé et le clic. */
var themeBtn = document.getElementById('theme-toggle');
function syncThemeBtn(theme) {
if (!themeBtn) return;
var light = theme === 'light';
/* Bouton dans la section Administration (#55) : icône et
libellé sont des <span> séparés ; l'ancien bouton de
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 (lbl) lbl.textContent = light ? 'Mode sombre' : 'Mode clair';
if (!ico && !lbl) {
themeBtn.textContent = light ? '☾ Mode sombre' : '☀ Mode clair';
}
themeBtn.setAttribute('aria-pressed', light ? 'true' : 'false');
}
if (themeBtn) {
syncThemeBtn(document.documentElement.getAttribute('data-theme') || 'dark');
themeBtn.addEventListener('click', function () {
var next = document.documentElement.getAttribute('data-theme') === 'light' ? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', next);
try { localStorage.setItem('kanbante-theme', next); } catch (e) { /* non-fatal */ }
syncThemeBtn(next);
});
}
/* 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 @@
"""Identité Kanbante (#54, #55) : sidebar charbon (pas bleutée),
accent vert Kanbante (pas rose Spark), icônes de navigation en
trait (pas d'emoji), bouton Dark/Light dans la section
Administration (pas dans l'en-tête).
"""
def test_sidebar_charbon_et_accent_vert(client, fake_gitea):
css = client.get("/static/css/style.css").text
assert "rgba(26, 28, 26, 0.96)" in css
assert "rgba(13, 16, 24" not in css
assert "--accent: #0d9488;" in css
assert "#c2437e" not in css
def test_nav_icons_sont_des_svg(client, fake_gitea):
page = client.get("/")
assert 'class="side-ico"><svg' in page.text
# Le logo Kanbante est aussi un SVG (colonnes blanches).
assert 'class="logo-mark"><svg' in page.text
def test_theme_toggle_dans_admin_pas_dans_en_tete(client, fake_gitea):
page = client.get("/")
assert 'id="theme-toggle"' in page.text
head = page.text.split("header-actions", 1)[1].split("</header>", 1)[0]
assert 'id="theme-toggle"' not in head
# Placement administrateur : le bouton est dans le bloc de la
# section Administration du gabarit (les non-admins, eux, le
# voient dans le pied de la barre, la section n'existant pas
# pour eux — sans quoi ils perdraient le basculeur).
tpl = open("app/templates/_header.html", encoding="utf-8").read()
admin_block = tpl.split("gkey == 'admin'", 1)[1]
assert 'id="theme-toggle"' in admin_block
assert "data-theme-label" in admin_block
+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
+39
View File
@@ -0,0 +1,39 @@
"""Navigation (#51) et bascule Dark/Light (#52).
L'accordéon de la barre latérale omettait la section admin :
le JS posait nav-closed-admin, mais aucun style ne la suivait.
Le thème clair, lui, doit être atteignable par un bouton et
mémorisé, avec les jetons draco clairs.
"""
def test_admin_accordion_closes(client, fake_gitea):
css = client.get("/static/css/style.css").text
assert 'body.nav-closed-admin [data-group="admin"] .side-acc' in css
assert 'body.nav-closed-admin [data-group="admin"] .side-chevron' in css
def test_theme_toggle_present_and_versioned(client, fake_gitea):
page = client.get("/")
assert 'id="theme-toggle"' in page.text
assert "/static/js/nav.js?v=" in page.text
# #55 : le bouton n'est plus dans l'en-tête applicatif.
head = page.text.split("header-actions", 1)[1].split("</header>", 1)[0]
assert 'id="theme-toggle"' not in head
def test_theme_js_contract(client, fake_gitea):
js = client.get("/static/js/nav.js").text
assert "kanbante-theme" in js
assert "theme-toggle" in js
assert "data-theme" in js
def test_light_theme_tokens(client, fake_gitea):
css = client.get("/static/css/style.css").text
assert 'html[data-theme="light"]' in css
assert "--bg: #f2f2f0;" in css
assert "--surface: #ffffff;" in css
assert "--accent-2: #0f766e;" in css
assert 'html[data-theme="light"] .draco-bar' in css
assert 'html[data-theme="light"] .card-menu' in css
+2 -2
View File
@@ -25,7 +25,7 @@ def test_sidebar_on_app_pages(client, fake_gitea):
# Bouton palette : icône seule, à droite du titre — pas de
# libellé texte « Recherche & commandes » visible.
assert 'class="side-head"' in page.text
assert ">🔍</button>" in page.text
assert 'class="side-search palette-trigger"' in page.text
# Le libellé de l'ancien bouton palette a disparu (le lien de
# navigation « Recherche », lui, existe toujours — c'est le
# texte complet « Recherche & commandes » qui doit manquer).
@@ -89,4 +89,4 @@ def test_wall_more_cards_line(client, fake_gitea, monkeypatch):
page = client.get("/wall")
assert page.status_code == 200
assert "+3 autres cartes en vol" in page.text
assert "📋 Ouvertes" in page.text
assert "Ouvertes" in page.text
+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
@@ -197,7 +197,7 @@ def test_board_filter_by_milestone_id(client):
def test_dashboard_uses_new_design_system(client):
r = client.get("/")
assert "app-header" in r.text
assert "nav-pill" in r.text
assert "seg-tabs" in r.text
# En-tête refondu en 0.20.0 : bande KPI unifiée (les stat-tile
# ne servent plus qu'au briefing).
assert "kpi-strip" in r.text
+1 -1
View File
@@ -8,7 +8,7 @@ from app import models
def test_dashboard_sort_offers_review_mode(client):
r = client.get("/")
assert r.status_code == 200
assert '<option value="review">🔍 En attente de revue ↓</option>' in r.text
assert '<option value="review">En attente de revue ↓</option>' in r.text
def test_dashboard_cards_carry_review_count(client):
+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
+31
View File
@@ -0,0 +1,31 @@
"""Thème du site (#49, #54) : Kanbante doit ressembler à la console
agent / barre Fusion (draco-barre, Console v7.2), pas au bleu/mauve
historique — et l'accent est désormais le vert Kanbante, pas le
rose Spark. Le test fige les jetons et interdit le retour des
littéraux rose/violet/bleu de thème dans style.css.
"""
def test_theme_agent_tokens(client, fake_gitea):
css = client.get("/static/css/style.css").text
assert "--bg: #101211;" in css
assert "--surface: #1a1c1a;" in css
assert "--surface-2: #242624;" in css
assert "--surface-3: #20221f;" in css
assert "--border: #2e302c;" in css
assert "--text: #eceee9;" in css
assert "--accent: #0d9488;" in css
assert "--accent-2: #14b8a6;" in css
def test_no_blue_purple_theme_literals(client, fake_gitea):
css = client.get("/static/css/style.css").text
for banned in (
"#7c5cfc", "#5b8af7", "#58a6ff", "#a78bfa", "#7dd3fc",
"#c4b5fd", "#a5c4fd", "#0b0e14", "#141822", "#1b202e",
"#232939", "124, 92, 252", "124,92,252",
"91, 138, 247", "91,138,247",
"#c2437e", "#e078a8", "194, 67, 126", "194,67,126",
"224, 120, 168", "224,120,168",
):
assert banned not in css, banned
+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
+50
View File
@@ -0,0 +1,50 @@
"""Lot UX (#57 à #68) : jetons, boutons, accent-text, icônes,
thème clair sur connexion/wallboard, en-tête de page commun.
"""
def test_tokens_et_accent_text(client, fake_gitea):
css = client.get("/static/css/style.css").text
for token in (
"--accent-text: #14b8a6;",
"--accent-text: #0f766e;",
"--radius-full: 999px;",
"--shadow-lg:",
"--font-xl: 1.55rem;",
):
assert token in css
assert "linear-gradient(135deg, rgba(13,148,136,.9)" not in css
assert '"Inter"' not in css
def test_boutons_et_skeleton(client, fake_gitea):
css = client.get("/static/css/style.css").text
for cls in (".btn-primary", ".btn-danger", ".btn-icon", ".skeleton", ".field-error-msg"):
assert cls in css
def test_cartes_et_labels_lisibles(client, fake_gitea):
css = client.get("/static/css/style.css").text
assert "-webkit-line-clamp: 2" in css
card = open("app/templates/_card.html", encoding="utf-8").read()
assert "pin-btn btn-icon" in card
assert "color-mix(in srgb" in card
assert "📌" not in card
def test_pas_de_recherche_en_double_et_theme_clair_login_wall(client, fake_gitea):
header = open("app/templates/_header.html", encoding="utf-8").read()
assert "topbar-search" not in header
for tpl in ("app/templates/login.html", "app/templates/wall.html"):
assert "kanbante-theme" in open(tpl, encoding="utf-8").read()
css = client.get("/static/css/style.css").text
assert 'html[data-theme="light"] .wall-body' in css
assert 'html[data-theme="light"] .login-body' in css
def test_pages_avec_en_tete_commun(client, fake_gitea):
today = open("app/templates/today.html", encoding="utf-8").read()
focus = open("app/templates/focus.html", encoding="utf-8").read()
assert 'class="page-head"' in today
assert 'class="page-head"' in focus
assert "🌅" not in today