Compare commits

..
Author SHA1 Message Date
bruno 427df2ca92 Merge pull request 'Flowdeck 7.84.9 : barre v0.1.11 — marque monochrome, anneau rouge avatar' (#44) from barre-v0111-7-84-9 into main
FlowDeck CI / lint (push) Failing after 0s
FlowDeck CI / test (push) Failing after 0s
FlowDeck CI / docker (push) Skipped
2026-10-29 23:09:35 -04:00
bruno 1a717ede16 Flowdeck 7.84.9 : étoile Spark monochrome pour la marque
FlowDeck CI / lint (push) Failing after 1s
FlowDeck CI / test (push) Failing after 0s
FlowDeck CI / docker (push) Skipped
FlowDeck CI / lint (pull_request) Failing after 1s
FlowDeck CI / test (pull_request) Failing after 1s
FlowDeck CI / docker (pull_request) Skipped
2026-10-29 23:09:19 -04:00
bruno d3abeb197a Flowdeck 7.84.9 : barre v0.1.11, marque monochrome, anneau avatar (CHANGELOG.md)
FlowDeck CI / lint (push) Failing after 0s
FlowDeck CI / test (push) Failing after 0s
FlowDeck CI / docker (push) Skipped
2026-10-29 23:09:17 -04:00
bruno dee5fb5752 Flowdeck 7.84.9 : barre v0.1.11, marque monochrome, anneau avatar (VERSION)
FlowDeck CI / lint (push) Failing after 0s
FlowDeck CI / test (push) Failing after 1s
FlowDeck CI / docker (push) Skipped
2026-10-29 23:09:15 -04:00
bruno 9de1166661 Flowdeck 7.84.9 : barre v0.1.11, marque monochrome, anneau avatar (static/css/draco-barre.css)
FlowDeck CI / test (push) Failing after 0s
FlowDeck CI / docker (push) Skipped
FlowDeck CI / lint (push) Failing after 1m54s
2026-10-29 23:09:13 -04:00
bruno c0add7b6de Flowdeck 7.84.9 : barre v0.1.11, marque monochrome, anneau avatar (app/templates/_draco_bar.html)
FlowDeck CI / lint (push) Failing after 1s
FlowDeck CI / test (push) Failing after 1s
FlowDeck CI / docker (push) Skipped
2026-10-29 23:09:12 -04:00
bruno d29986e51d Merge pull request 'Flowdeck 7.84.8 : barre v0.1.10, rouge Spark, avatar flottant, Alertes cloche' (#43) from barre-rouge-spark-7-84-8 into main
FlowDeck CI / test (push) Failing after 1s
FlowDeck CI / docker (push) Skipped
FlowDeck CI / lint (push) Failing after 1m52s
2026-10-29 22:55:18 -04:00
bruno cd8969a3a2 Flowdeck 7.84.8 : avatar Spark flottant (commons v0.1.10)
FlowDeck CI / lint (push) Failing after 0s
FlowDeck CI / test (push) Failing after 0s
FlowDeck CI / docker (push) Skipped
FlowDeck CI / lint (pull_request) Failing after 1s
FlowDeck CI / test (pull_request) Failing after 0s
FlowDeck CI / docker (pull_request) Skipped
2026-10-29 22:55:00 -04:00
bruno e8cbd8e0f3 Flowdeck 7.84.8 : barre v0.1.10, rouge Spark, avatar flottant, Alertes cloche (CHANGELOG.md)
FlowDeck CI / lint (push) Failing after 1s
FlowDeck CI / test (push) Failing after 0s
FlowDeck CI / docker (push) Skipped
2026-10-29 22:54:57 -04:00
bruno d40044e0c4 Flowdeck 7.84.8 : barre v0.1.10, rouge Spark, avatar flottant, Alertes cloche (VERSION)
FlowDeck CI / lint (push) Failing after 1s
FlowDeck CI / test (push) Failing after 0s
FlowDeck CI / docker (push) Skipped
2026-10-29 22:54:56 -04:00
bruno 41865c702a Flowdeck 7.84.8 : barre v0.1.10, rouge Spark, avatar flottant, Alertes cloche (static/css/draco-barre.css)
FlowDeck CI / lint (push) Failing after 0s
FlowDeck CI / test (push) Failing after 0s
FlowDeck CI / docker (push) Skipped
2026-10-29 22:54:54 -04:00
bruno 56b8ff2072 Flowdeck 7.84.8 : barre v0.1.10, rouge Spark, avatar flottant, Alertes cloche (app/templates/_draco_bar.html)
FlowDeck CI / lint (push) Failing after 4m22s
FlowDeck CI / test (push) Failing after 3h8m21s
FlowDeck CI / docker (push) Skipped
2026-10-29 22:54:53 -04:00
bruno fc5431308c Merge pull request 'Flowdeck 7.84.7 : les 5 logos dans le menu et la barre commune' (#42) from barre-logos-7-84-7 into main
FlowDeck CI / lint (push) Failing after 0s
FlowDeck CI / test (push) Failing after 1s
FlowDeck CI / docker (push) Skipped
2026-10-29 22:22:14 -04:00
bruno 7d31af8ec2 Flowdeck 7.84.7 : logo fourni spark.png pour la barre et le menu
FlowDeck CI / lint (push) Failing after 1s
FlowDeck CI / test (push) Failing after 0s
FlowDeck CI / docker (push) Skipped
FlowDeck CI / lint (pull_request) Failing after 0s
FlowDeck CI / test (pull_request) Failing after 0s
FlowDeck CI / docker (pull_request) Skipped
2026-10-29 22:21:59 -04:00
bruno 2e73802f5f Flowdeck 7.84.7 : logo fourni kanbante.png pour la barre et le menu
FlowDeck CI / lint (push) Failing after 1s
FlowDeck CI / test (push) Failing after 0s
FlowDeck CI / docker (push) Skipped
2026-10-29 22:21:57 -04:00
bruno 46b876eea8 Flowdeck 7.84.7 : logo fourni tiktak.svg pour la barre et le menu
FlowDeck CI / lint (push) Failing after 0s
FlowDeck CI / test (push) Failing after 0s
FlowDeck CI / docker (push) Skipped
2026-10-29 22:21:55 -04:00
bruno 2431bb6059 Flowdeck 7.84.7 : logo fourni flowdeck.png pour la barre et le menu
FlowDeck CI / lint (push) Failing after 1s
FlowDeck CI / test (push) Failing after 0s
FlowDeck CI / docker (push) Skipped
2026-10-29 22:21:53 -04:00
bruno ac2f0c8cfc Flowdeck 7.84.7 : logo fourni obsigate.png pour la barre et le menu
FlowDeck CI / lint (push) Failing after 0s
FlowDeck CI / test (push) Failing after 0s
FlowDeck CI / docker (push) Skipped
2026-10-29 22:21:51 -04:00
bruno 58ad5d9164 Flowdeck 7.84.7 : logos barre-apps v0.1.9 dans le menu et la barre (CHANGELOG.md)
FlowDeck CI / lint (push) Failing after 1s
FlowDeck CI / test (push) Failing after 0s
FlowDeck CI / docker (push) Skipped
2026-10-29 22:21:48 -04:00
bruno 91fad4eba3 Flowdeck 7.84.7 : logos barre-apps v0.1.9 dans le menu et la barre (VERSION)
FlowDeck CI / lint (push) Failing after 0s
FlowDeck CI / test (push) Failing after 0s
FlowDeck CI / docker (push) Skipped
2026-10-29 22:21:46 -04:00
bruno 7995dbe9d2 Flowdeck 7.84.7 : logos barre-apps v0.1.9 dans le menu et la barre (static/css/draco-barre.css)
FlowDeck CI / lint (push) Failing after 1s
FlowDeck CI / test (push) Failing after 0s
FlowDeck CI / docker (push) Skipped
2026-10-29 22:21:44 -04:00
bruno 0e3e6ec197 Flowdeck 7.84.7 : logos barre-apps v0.1.9 dans le menu et la barre (static/js/vendor/draco-barre.js)
FlowDeck CI / lint (push) Failing after 1s
FlowDeck CI / test (push) Failing after 1s
FlowDeck CI / docker (push) Skipped
2026-10-29 22:21:42 -04:00
bruno d6ea617ed7 Flowdeck 7.84.7 : logos barre-apps v0.1.9 dans le menu et la barre (app/templates/_draco_bar.html)
FlowDeck CI / lint (push) Failing after 1s
FlowDeck CI / test (push) Failing after 1s
FlowDeck CI / docker (push) Skipped
2026-10-29 22:21:40 -04:00
bruno f7c2ea0d34 docs: changelog 7.84.6 (PR #41 CORS sans doublons)
FlowDeck CI / lint (push) Failing after 0s
FlowDeck CI / test (push) Failing after 0s
FlowDeck CI / docker (push) Skipped
2026-10-28 18:23:40 -04:00
bruno d16666be85 chore: bump version 7.84.5 -> 7.84.6
FlowDeck CI / test (push) Failing after 1s
FlowDeck CI / docker (push) Skipped
FlowDeck CI / lint (push) Failing after 1m58s
2026-10-28 18:23:32 -04:00
bruno fb4ce38403 Merge pull request 'fix(cors): /api/search ne doit plus empiler les en-têtes CORS (défaut 3b)' (#41) from fix-cors-search-doublons into main
FlowDeck CI / lint (push) Failing after 4s
FlowDeck CI / test (push) Failing after 12m39s
FlowDeck CI / docker (push) Skipped
2026-10-28 18:22:08 -04:00
bruno 34253c7b3f test(cors): /api/search un seul jeu d'en-tetes CORS, origine refusee inchangee (defaut 3b)
FlowDeck CI / lint (push) Failing after 1s
FlowDeck CI / test (push) Failing after 1s
FlowDeck CI / docker (push) Skipped
FlowDeck CI / lint (pull_request) Failing after 1m49s
FlowDeck CI / test (pull_request) Failing after 32m50s
FlowDeck CI / docker (pull_request) Skipped
2026-10-28 16:49:49 -04:00
bruno 4490a2ed70 fix(cors): /api/search remplace les en-tetes CORS au lieu de les empiler (defaut 3b)
FlowDeck CI / test (push) Failing after 0s
FlowDeck CI / docker (push) Skipped
FlowDeck CI / lint (push) Failing after 2m9s
2026-10-28 16:49:45 -04:00
bruno c9f7754f5f Changelog 7.84.5 : texte parasite, onglets recherche, CORS console (PR #37/#38/#40)
FlowDeck CI / lint (push) Failing after 0s
FlowDeck CI / test (push) Failing after 0s
FlowDeck CI / docker (push) Skipped
2026-10-28 16:21:42 -04:00
bruno 43e416a889 Bump 7.84.5
FlowDeck CI / lint (push) Failing after 2s
FlowDeck CI / test (push) Failing after 1s
FlowDeck CI / docker (push) Skipped
2026-10-28 16:21:38 -04:00
bruno fefd9b1ad3 Merge pull request 'CORS recherche : autoriser la console Spark (agent prod+dev)' (#40) from federation-recherche-console into main
FlowDeck CI / lint (push) Failing after 1s
FlowDeck CI / test (push) Failing after 1s
FlowDeck CI / docker (push) Skipped
2026-10-28 16:17:04 -04:00
bruno 3b43be3761 Merge pull request 'Recherche : onglets visibles apres deploiement (base console explicite + purge cache SW)' (#38) from flowdeck-recherche-onglets into main
FlowDeck CI / lint (push) Failing after 1s
FlowDeck CI / test (push) Failing after 0s
FlowDeck CI / docker (push) Skipped
2026-10-28 16:17:01 -04:00
bruno 972d40a75f Merge pull request 'Barre : supprimer le texte parasite « Chantier 5 » rendu au-dessus du header' (#37) from flowdeck-texte-parasite into main
FlowDeck CI / lint (push) Failing after 0s
FlowDeck CI / test (push) Failing after 0s
FlowDeck CI / docker (push) Skipped
2026-10-28 16:16:59 -04:00
bruno fb942bc871 Tests CORS recherche: origines agent autorisees, tiers refuse
FlowDeck CI / lint (push) Failing after 0s
FlowDeck CI / test (push) Failing after 1s
FlowDeck CI / docker (push) Skipped
FlowDeck CI / lint (pull_request) Failing after 3s
FlowDeck CI / test (pull_request) Failing after 1s
FlowDeck CI / docker (pull_request) Skipped
2026-10-28 16:11:58 -04:00
bruno b8ebea1479 CORS recherche: autoriser les origines agent (console Spark) prod+dev
FlowDeck CI / lint (push) Failing after 2s
FlowDeck CI / test (push) Failing after 0s
FlowDeck CI / docker (push) Skipped
2026-10-28 16:11:53 -04:00
bruno ee065978b2 Recherche onglets (defaut 6) : tests/test_recherche_onglets.py
FlowDeck CI / lint (push) Failing after 1s
FlowDeck CI / test (push) Failing after 2s
FlowDeck CI / docker (push) Skipped
FlowDeck CI / lint (pull_request) Failing after 2s
FlowDeck CI / test (pull_request) Failing after 18m37s
FlowDeck CI / docker (pull_request) Skipped
2026-10-28 16:01:30 -04:00
bruno 71c7113a8c Recherche onglets (defaut 6) : static/sw.js
FlowDeck CI / test (push) Failing after 0s
FlowDeck CI / docker (push) Skipped
FlowDeck CI / lint (push) Failing after 1m52s
2026-10-28 16:01:25 -04:00
bruno c0bf0e8523 Recherche onglets (defaut 6) : app/routers/search.py
FlowDeck CI / lint (push) Failing after 2s
FlowDeck CI / test (push) Failing after 1s
FlowDeck CI / docker (push) Skipped
2026-10-28 16:01:21 -04:00
bruno 9ae054d1ba Recherche onglets (defaut 6) : app/templates/recherche.html
FlowDeck CI / test (push) Failing after 1s
FlowDeck CI / docker (push) Skipped
FlowDeck CI / lint (push) Failing after 1m51s
2026-10-28 16:01:18 -04:00
bruno 1e552a55a4 Tests : aucun texte de chantier rendu par la barre, onglets intacts
FlowDeck CI / test (push) Failing after 3s
FlowDeck CI / docker (push) Skipped
FlowDeck CI / lint (push) Failing after 1s
FlowDeck CI / test (pull_request) Failing after 1s
FlowDeck CI / docker (pull_request) Skipped
FlowDeck CI / lint (pull_request) Failing after 1m53s
2026-10-28 15:59:44 -04:00
bruno fd9df60788 Barre : texte de chantier 5 hors commentaire Jinja supprime du rendu (5e onglet + menu marque intacts)
FlowDeck CI / lint (push) Failing after 0s
FlowDeck CI / test (push) Failing after 0s
FlowDeck CI / docker (push) Skipped
2026-10-28 15:59:41 -04:00
bruno 984e92b593 Bump Flowdeck 7.84.4 : changelog (alignement recherche console #68)
FlowDeck CI / test (push) Failing after 41m28s
FlowDeck CI / lint (push) Failing after 2s
FlowDeck CI / docker (push) Skipped
2026-10-28 15:06:59 -04:00
bruno 8ba38a69a4 Bump Flowdeck 7.84.4 (alignement recherche console #68)
FlowDeck CI / lint (push) Failing after 2s
FlowDeck CI / test (push) Failing after 0s
FlowDeck CI / docker (push) Skipped
2026-10-28 15:06:55 -04:00
bruno ee477bf2d1 Merge pull request 'recherche : onglet Console Spark sur GET /api/v1/recherche (alignement)' (#36) from align-recherche-console into main
FlowDeck CI / test (push) Failing after 1s
FlowDeck CI / docker (push) Skipped
FlowDeck CI / lint (push) Failing after 1m51s
2026-10-28 15:04:27 -04:00
bruno ba5f8c8eab recherche: tests alignement console /api/v1/recherche
FlowDeck CI / lint (push) Failing after 1s
FlowDeck CI / test (push) Failing after 1s
FlowDeck CI / docker (push) Skipped
FlowDeck CI / lint (pull_request) Failing after 2s
FlowDeck CI / test (pull_request) Failing after 2s
FlowDeck CI / docker (pull_request) Skipped
2026-10-28 14:44:35 -04:00
bruno d4fef9c1bf recherche: onglet Console Spark sur GET /api/v1/recherche (alignement)
FlowDeck CI / lint (push) Failing after 2s
FlowDeck CI / test (push) Failing after 1s
FlowDeck CI / docker (push) Skipped
2026-10-28 14:44:30 -04:00
bruno f708fa77e2 Bump v7.84.3 : CHANGELOG.md
FlowDeck CI / lint (push) Failing after 3s
FlowDeck CI / test (push) Failing after 1s
FlowDeck CI / docker (push) Skipped
2026-10-28 14:29:54 -04:00
bruno 7711bd3af7 Bump v7.84.3 : app/main.py
FlowDeck CI / lint (push) Failing after 0s
FlowDeck CI / test (push) Failing after 0s
FlowDeck CI / docker (push) Skipped
2026-10-28 14:29:50 -04:00
bruno b4923493ac Bump v7.84.3 : VERSION
FlowDeck CI / lint (push) Failing after 0s
FlowDeck CI / test (push) Failing after 1s
FlowDeck CI / docker (push) Skipped
2026-10-28 14:29:48 -04:00
bruno 0ead9fea4a Merge pull request 'Chantier 5 — Alertes en onglets + onglet Console Spark (rebase de #34 sur main post-#33)' (#35) from alertes-onglets-5-rebase-main into main
FlowDeck CI / lint (push) Failing after 1s
FlowDeck CI / test (push) Failing after 0s
FlowDeck CI / docker (push) Skipped
2026-10-28 14:28:30 -04:00
bruno 8b871ee39f Rebase chantier 5 (alertes) sur main post-#33 : tests/test_draco_alertes.py
FlowDeck CI / lint (push) Failing after 0s
FlowDeck CI / test (push) Failing after 0s
FlowDeck CI / docker (push) Skipped
FlowDeck CI / lint (pull_request) Failing after 0s
FlowDeck CI / test (pull_request) Failing after 0s
FlowDeck CI / docker (pull_request) Skipped
2026-10-28 14:27:58 -04:00
bruno 171fdf063b Rebase chantier 5 (alertes) sur main post-#33 : tests/draco-alertes.test.js
FlowDeck CI / lint (push) Failing after 1s
FlowDeck CI / test (push) Failing after 0s
FlowDeck CI / docker (push) Skipped
2026-10-28 14:27:55 -04:00
bruno ea12938e7c Rebase chantier 5 (alertes) sur main post-#33 : static/js/vendor/draco-barre.js
FlowDeck CI / lint (push) Failing after 0s
FlowDeck CI / test (push) Failing after 1s
FlowDeck CI / docker (push) Skipped
2026-10-28 14:27:52 -04:00
bruno 3f14dc7130 Rebase chantier 5 (alertes) sur main post-#33 : static/js/draco-alertes.js
FlowDeck CI / lint (push) Failing after 0s
FlowDeck CI / test (push) Failing after 0s
FlowDeck CI / docker (push) Skipped
2026-10-28 14:27:49 -04:00
bruno 71454f94d1 Rebase chantier 5 (alertes) sur main post-#33 : static/css/draco-barre.css
FlowDeck CI / lint (push) Failing after 1s
FlowDeck CI / test (push) Failing after 0s
FlowDeck CI / docker (push) Skipped
2026-10-28 14:27:46 -04:00
bruno 3145cea8e1 Rebase chantier 5 (alertes) sur main post-#33 : static/css/draco-alertes.css
FlowDeck CI / lint (push) Failing after 0s
FlowDeck CI / test (push) Failing after 0s
FlowDeck CI / docker (push) Skipped
2026-10-28 14:27:43 -04:00
bruno f49eb62172 Rebase chantier 5 (alertes) sur main post-#33 : app/templates/_draco_bar.html
FlowDeck CI / lint (push) Failing after 0s
FlowDeck CI / test (push) Failing after 0s
FlowDeck CI / docker (push) Skipped
2026-10-28 14:27:41 -04:00
bruno 746ab4c0e1 Merge pull request 'Recherche partout (#68, chantier 2+3) : onglets, Tout voir sur place, focus, mise en page' (#33) from recherche-onglets-ux-68 into main
FlowDeck CI / lint (push) Failing after 0s
FlowDeck CI / test (push) Failing after 0s
FlowDeck CI / docker (push) Skipped
2026-10-28 14:25:36 -04:00
bruno cdf943aff5 Merge pull request 'CORS recherche/alertes : allowlist soeurs prod+dev explicites (fusion #68/#69)' (#32) from cors-recherche-alertes-68-69 into main
FlowDeck CI / lint (push) Failing after 1s
FlowDeck CI / test (push) Failing after 0s
FlowDeck CI / docker (push) Skipped
2026-10-28 14:25:22 -04:00
bruno 751daa43dd Recherche partout (#68, chantier 2+3) : onglets, Tout voir sur place, focus, mise en page
FlowDeck CI / lint (push) Failing after 0s
FlowDeck CI / test (push) Failing after 0s
FlowDeck CI / docker (push) Skipped
FlowDeck CI / lint (pull_request) Failing after 1s
FlowDeck CI / test (pull_request) Failing after 0s
FlowDeck CI / docker (pull_request) Skipped
2026-10-28 13:50:22 -04:00
bruno 3895e79167 Recherche partout (#68, chantier 2+3) : onglets, Tout voir sur place, focus, mise en page
FlowDeck CI / lint (push) Failing after 1s
FlowDeck CI / test (push) Failing after 0s
FlowDeck CI / docker (push) Skipped
2026-10-28 13:50:18 -04:00
bruno 8dcddfe34f Recherche partout (#68, chantier 2+3) : onglets, Tout voir sur place, focus, mise en page
FlowDeck CI / lint (push) Failing after 1s
FlowDeck CI / test (push) Failing after 0s
FlowDeck CI / docker (push) Skipped
2026-10-28 13:50:15 -04:00
bruno b506a447c5 Recherche partout (#68, chantier 2+3) : onglets, Tout voir sur place, focus, mise en page
FlowDeck CI / lint (push) Failing after 1s
FlowDeck CI / test (push) Failing after 1s
FlowDeck CI / docker (push) Skipped
2026-10-28 13:50:12 -04:00
bruno 9ca831f816 Tests CORS alertes : origine dev autorisee en preflight
FlowDeck CI / lint (push) Failing after 0s
FlowDeck CI / test (push) Failing after 0s
FlowDeck CI / docker (push) Skipped
FlowDeck CI / lint (pull_request) Failing after 0s
FlowDeck CI / test (pull_request) Failing after 0s
FlowDeck CI / docker (pull_request) Skipped
2026-10-28 13:45:50 -04:00
bruno c09a0ac51d Tests CORS recherche : prod+dev autorises, tiers refuse
FlowDeck CI / lint (push) Failing after 1s
FlowDeck CI / test (push) Failing after 0s
FlowDeck CI / docker (push) Skipped
2026-10-28 13:45:48 -04:00
bruno 2d3a93511b CORS alertes (fusion #69) : allow_origins /api/notifications prod+dev
FlowDeck CI / lint (push) Failing after 0s
FlowDeck CI / test (push) Failing after 0s
FlowDeck CI / docker (push) Skipped
2026-10-28 13:45:45 -04:00
bruno dd979f81a7 CORS recherche/alertes (fusion #68/#69) : allowlist soeurs prod+dev explicites
FlowDeck CI / lint (push) Failing after 0s
FlowDeck CI / test (push) Failing after 0s
FlowDeck CI / docker (push) Skipped
2026-10-28 13:45:42 -04:00
25 changed files with 1343 additions and 125 deletions
+42
View File
@@ -1,3 +1,45 @@
## v7.84.9 (2026-10-29) — Barre commune : marque monochrome, anneau rouge avatar
- **Barre d'apps v0.1.11 vendangée (dracodev-commons)** : `static/css/draco-barre.css` remplacé verbatim par la v0.1.11 ; `static/js/vendor/draco-barre.js` inchangé (déjà identique à commons, JS inchangé en v0.1.11) ; fragment `app/templates/_draco_bar.html` porté à la v0.1.11, le reste de son intégration Flowdeck (URL `draco_urls()`, `onAction`, nonce CSP, `asset_version`) est inchangé.
- **Marque monochrome** : la marque n'est plus le logo rouge — c'est l'étoile Spark monochrome (`static/img/brand-spark.png`, copie de `ui/barre-apps/brand-spark.png` de commons, étoile blanche sur fond transparent, 26 px), blanche sur barre foncée et noire sur barre claire via le filtre du CSS vendangé. L'onglet agent et son entrée de menu GARDENT le logo rouge.
- **Anneau rouge avatar** : le bouton avatar flottant bas-centre gagne un anneau rouge `#c00000` (bordure 3 px du CSS vendangé, `var(--agent)`).
- **Thème** : la barre suit `data-theme` sur `<html>` (choix Flowdeck `fd_theme`, défaut light) — les règles thème de commons s'appliquent telles quelles, aucune surcharge propre n'a été ajoutée.
- **Cache** : le bump 7.84.8 → 7.84.9 change aussi `asset_version`, donc les navigateurs ne garderont pas l'ancienne barre.
## v7.84.8 (2026-10-29) — Barre commune : rouge Spark, avatar flottant, Alertes cloche seule
- **Barre d'apps v0.1.10 vendangée (dracodev-commons)** : `static/css/draco-barre.css` remplacé verbatim par la v0.1.10 ; `static/js/vendor/draco-barre.js` inchangé (déjà identique à commons entre v0.1.9 et v0.1.10) ; fragment `app/templates/_draco_bar.html` porté à la v0.1.10, le reste de son intégration Flowdeck (URL `draco_urls()`, `onAction`, nonce CSP, `asset_version`) est inchangé.
- **Rouge Spark** : `--agent` rose `#c2437e` → rouge `#c00000` (échantillonné dans le logo Spark) ; onglet agent `--app #c00000`, marque = logo Spark (26 px) au lieu du ✦.
- **Bouton Spark rose supprimé** de la barre, remplacé par le bouton avatar flottant bas-centre (cercle personnage Spark, `static/img/avatar-spark.png`, `data-act="spark"` → le panneau Spark de l'hôte, comme l'ancien bouton).
- **Alertes** : cloche seule, sans libellé — bouton moins large.
- **Cache** : le bump 7.84.7 → 7.84.8 change aussi `asset_version`, donc les navigateurs ne garderont pas l'ancienne barre.
## v7.84.7 (2026-10-29) — Barre commune : les 5 logos dans le menu et la barre
- **Barre d'apps v0.1.9 vendangée (dracodev-commons)** : `static/js/vendor/draco-barre.js` et `static/css/draco-barre.css` remplacés verbatim par la v0.1.9 (les fichiers vendangés précédents étaient identiques à l'octet près à ceux de commons avant la v0.1.9, aucune adaptation locale n'est perdue) ; fragment `app/templates/_draco_bar.html` porté à la v0.1.9, le reste de son intégration Flowdeck (URL `draco_urls()`, `onAction`, nonce CSP) est inchangé.
- **Icônes** : les 5 tuiles de la barre — et donc du menu de la marque, qui les clone — sont désormais les logos fournis, tels quels, jamais redessinés : ObsiGate (arbre), Flowdeck (cartes), TikTak (logo officiel SVG), Kanbante (cycle vert), Console Spark (étoile rouge validée). Fichiers servis depuis `static/img/logos/` (copie de `ui/barre-apps/logos/` de commons, PNG 180 px) ; l'entrée « Console Spark » du menu clone l'icône de l'onglet agent. Les glyphes texte ▤/▦/✓/▥/✦ disparaissent des onglets (la marque ✦ et le bouton Spark gardent leur ✦).
- **Cache** : le bump 7.84.6 → 7.84.7 change aussi `asset_version`, donc les navigateurs ne garderont pas les anciennes icônes.
## v7.84.6 (2026-10-28) — Recherche console : CORS Flowdeck sans doublons (PR #41)
- **CORS recherche (PR #41)** : le middleware de recherche remplace désormais les en-têtes CORS existants au lieu de les empiler — le CORSMiddleware global posait déjà Allow-Credentials: true, l'ancien middleware ajoutait les siens par-dessus (« true, true » rejeté par le navigateur) ; l'onglet Flowdeck de la recherche console agent-dev n'est plus injoignable.
## v7.84.5 (2026-10-28) — Corrections console/recherche : texte parasite, onglets, CORS console
- **Barre (PR #37)** : suppression du texte parasite « Chantier 5 » rendu au-dessus du header.
- **Recherche (PR #38)** : onglets visibles après déploiement (base console explicite + purge cache service worker).
- **CORS recherche (PR #40)** : la recherche Flowdeck autorise désormais la console Spark (agent prod+dev) en inter-origines.
## v7.84.4 (2026-10-28) — Alignement recherche console : onglet Console Spark sur /api/v1/recherche
- **Recherche (#68)** : l'onglet Console Spark de `/recherche` appelle désormais `GET /api/v1/recherche` (au lieu de `/api/search`) et reconstruit les liens profonds console (`?section=…&objet=…`, `disc=`, `activite=`) — les résultats console réels s'affichent enfin dans la page hôte.
## v7.84.3 (2026-10-28) — Corrections recherche/alertes : CORS prod+dev, recherche en onglets, alertes en onglets
- **CORS recherche/alertes (#68/#69)** : allowlist d'origines soeurs prod+dev explicites + credentials, jamais `*` — chaque serveur n'autorisait que l'environnement resolu, d'ou les echecs inter-apps constates.
- **Recherche partout (#68)** : recherche pleine largeur, reintegree dans le gabarit, focus auto, « Tout voir » restant sur place, onglets Tout/ObsiGate/Flowdeck/TikTak/Kanbante/Console Spark.
- **Alertes (#69/#71)** : vue Alertes en onglets (Tout/ObsiGate/Flowdeck/TikTak/Kanbante/Console Spark/Historique, actives sauf Historique = fermees), filtre texte, composant partage vendange byte-identique ; bouton Console Spark en 5e onglet de la barre apres Kanbante.
## v7.84.2 (2026-10-28) — Programme Fusion : menu barre, alertes unifiées, recherche partout
- **Barre (#70)** : bouton « ••• » retiré (menu par défaut supprimé), « Console Spark » ajoutée au menu de la marque, fermeture/focus corrigés.
+1 -1
View File
@@ -1 +1 @@
7.84.2
7.84.9
+12 -1
View File
@@ -216,7 +216,7 @@ def _lire_version() -> str:
# Littéral de version synchronisé avec le fichier VERSION :
# version="7.84.2"
# version="7.84.3"
# Patron historique des bumps (voir scripts/bump_version.py, qui cherche
# ce littéral) et contrôle de tests/test_pwa_offline.py. La version
# runtime reste lue dynamiquement depuis VERSION par _lire_version()
@@ -251,6 +251,17 @@ _CORS_ORIGINS = sorted(set(_CORS_ORIGINS) | {
_draco_urls69.origine(_toutes69[k])
for k in ("obsigate", "flowdeck", "tiktak", "kanbante")
} - {""})
# Correctif CORS alertes (fusion #69, 2026-10-28) : allow_origins
# est figé à l'import (configuration seule, sans hôte de requête)
# — il ne doit donc pas dépendre de l'environnement résolu. Les
# origines sœurs prod ET dev sont listées explicitement (module
# CORS de la recherche, jamais "*") : c'est ce qui manquait à
# /api/notifications lu depuis les autres hôtes.
from app.middleware.recherche_cors import DEV_ORIGINS as _DEV_ORIGINS
from app.middleware.recherche_cors import PROD_ORIGINS as _PROD_ORIGINS
_CORS_ORIGINS = sorted(set(_CORS_ORIGINS) | set(_DEV_ORIGINS)
| set(_PROD_ORIGINS))
# Hors prod : dev local + origines d'extension (Web Clipper, Bearer uniquement —
# pas de cookie → `allow_credentials` ne s'applique pas à ces origines).
app.add_middleware(
+53 -16
View File
@@ -8,15 +8,21 @@ Ce module n'autorise donc que :
- le chemin ``/api/search`` (jamais les autres routes, jamais les
écritures) ;
- les origines sœurs : celles résolues par
``app.services.draco_urls.toutes()`` (explicite > hôte > dérivation
> défaut officiel), **plus** les origines prod ou ``-dev``
officielles correspondant à l'hôte de la requête — ce second
ensemble est nécessaire car ce middleware s'exécute avant le
middleware CSP qui pose le contexte d'hôte de la requête pour
``draco_urls`` ; sans lui, une page servie par ``flowdeck-dev``
verrait ses sœurs ``-dev`` refusées quand la configuration seule
ne les déclare pas ;
- les origines sœurs : les origines officielles **prod ET dev,
listées explicitement** (``PROD_ORIGINS | DEV_ORIGINS``), **plus
les origines de la console Spark** (``AGENT_ORIGINS`` : agent prod
ET dev — fédération de la recherche depuis la console,
2026-10-28), **plus**
celles résolues par ``app.services.draco_urls.toutes()``
(explicite > hôte > dérivation > défaut officiel) — jamais ``*``.
Correctif 2026-10-28 (fusion #68/#69, matrice constatée par
Bruno) : l'ancienne allowlist ne contenait que l'environnement
résolu (configuration seule, ce middleware s'exécutant avant le
middleware CSP qui pose le contexte d'hôte, plus l'environnement
de l'hôte de la requête) — jamais les deux à la fois. Une page
servie dans l'autre environnement voyait donc sa section
afficher « Injoignable depuis cet hôte (CORS ou réseau) », et
c'est Flowdeck qui échouait depuis tous les hôtes ;
- avec credentials (le cookie SSO ``draco_session``, accepté par
``/api/search`` depuis cette livraison, ou la session native).
@@ -46,6 +52,12 @@ DEV_ORIGINS = frozenset({
"https://kanbante-dev.dracodev.net",
})
AGENT_ORIGINS = frozenset({
"https://agent.dracodev.net",
"https://agent-dev.dracodev.net",
})
# Clés de draco_urls.toutes() qui sont des applications sœurs.
_SOEUR_KEYS = ("obsigate", "flowdeck", "tiktak", "kanbante")
@@ -105,12 +117,12 @@ def sister_origins(resolved: dict | None = None, request_host: str | None = None
origin = origin_of((resolved or {}).get(key))
if origin:
origins.add(origin)
if is_dev_host(request_host):
origins |= DEV_ORIGINS
elif is_prod_host(request_host):
origins |= PROD_ORIGINS
if not origins:
return PROD_ORIGINS | DEV_ORIGINS
# Correctif 2026-10-28 : prod ET dev toujours — ``request_host``
# ne restreint plus l'allowlist (il ne servait qu'à ajouter
# l'environnement de la requête, désormais inclus d'office).
# ``resolved`` n'ajoute que des origines de configuration sans
# jamais retirer une origine officielle.
origins |= PROD_ORIGINS | DEV_ORIGINS | AGENT_ORIGINS
return frozenset(origins)
@@ -181,7 +193,32 @@ class RechercheCorsMiddleware:
if message["type"] == "http.response.start":
cors = cors_headers_for(origin, request_host=request_host)
if cors is not None:
message["headers"] = list(message.get("headers", [])) + [
# Remplacer, pas empiler (défaut 3b, 2026-10-28) :
# le CORSMiddleware global de l'application pose
# déjà Access-Control-Allow-Credentials: true sur
# TOUTES les réponses (comportement Starlette avec
# allow_credentials=True, même pour une origine
# hors allowlist et hors /api/search) ; empiler les
# en-têtes de recherche donnait, sur /api/search
# depuis agent-dev, Allow-Credentials en double
# (« true, true » ≠ « true » pour le contrôle CORS
# du navigateur → fetch rejeté → « injoignable
# (CORS ou réseau) ») et, depuis une sœur
# (origine déjà dans la allowlist globale),
# Allow-Origin en double (rejet certain). Les
# hôtes TikTak/Kanbante, sans ce cumul, répondent
# avec des en-têtes uniques — d'où l'échec
# Flowdeck seul. Une origine non autorisée
# traverse sans modification (comportement
# antérieur préservé : le seul en-tête global
# reste, sans Allow-Origin, donc bloqué).
restants = [
(k, v) for k, v in message.get("headers", [])
if k.decode("latin-1").lower() not in (
"access-control-allow-origin",
"access-control-allow-credentials",
"vary")]
message["headers"] = restants + [
(k.lower().encode("latin-1"), v.encode("latin-1"))
for k, v in cors.items()
]
+6 -1
View File
@@ -63,7 +63,12 @@ def recherche_page(request: Request, q: str = Query(default="")):
user = SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
bases = draco_urls.toutes()
template = ENV.get_template("recherche.html")
# Defaut 6 : la base agent est exposee explicitement (data-base-console
# dans recherche.html), comme les 3 autres hotes — recherche.js ne doit
# plus la deviner (derivation -dev / href d'onglet de barre) pour
# l'onglet Console Spark (GET /api/v1/recherche, alignement #36).
return HTMLResponse(template.render(
request=request, q=q.strip(), user=user,
bases={k: bases[k] for k in ("obsigate", "flowdeck", "tiktak", "kanbante")},
bases={k: bases[k] for k in ("obsigate", "flowdeck", "tiktak",
"kanbante", "agent")},
))
+31 -16
View File
@@ -1,42 +1,61 @@
{# Barre d'apps commune dracodev — Fusion ACC-1.1 (issue #17).
Snippet FIGÉ vendangé depuis Projets/dracodev-commons v0.1.6
Snippet FIGÉ vendangé depuis Projets/dracodev-commons v0.1.11
(ui/barre-apps/) : ne pas modifier la structure ici ; toute évolution
passe par une nouvelle version de commons.
CSS/JS : /static/css/draco-barre.css, /static/js/vendor/draco-barre.js.
v7.82.0 (dev/prod) : les href des onglets ne sont plus figés —
l'hôte les injecte depuis sa config (draco_urls(), noms officiels
par défaut, noms en -dev déclarés dans le .env de l'environnement). #}
Chantier 5 (fusion #69/#71, sans bump) : 5e onglet Console Spark
(data-app="agent", draco_urls().agent), après Kanbante ; l'entrée
« Console Spark » du menu de la marque est conservée.
par défaut, noms en -dev déclarés dans le .env de l'environnement).
5e onglet Console Spark (data-app="agent", draco_urls().agent),
après Kanbante ; l'entrée « Console Spark » du menu de la marque
est conservée (menu construit par draco-barre.js depuis les onglets).
Icônes logos v0.1.9 (2026-10-29) : les 5 tuiles sont les logos
fournis, tels quels, servis depuis /static/img/logos/ (copie de
ui/barre-apps/logos/ de dracodev-commons) — jamais de glyphe
texte, jamais de redessin ; l'entrée « Console Spark » du menu
clone l'icône de l'onglet agent (draco-barre.js).
v0.1.10 (2026-10-29) : --agent rose → rouge Spark #c00000 ;
marque ✦ → logo Spark (26 px) ; bouton « Spark » rose SUPPRIMÉ
de la barre, remplacé par le bouton avatar flottant bas-centre
(static/img/avatar-spark.png, data-act="spark" → panneau de l'hôte) ;
Alertes = cloche seule, sans libellé. JS inchangé depuis v0.1.9.
v0.1.11 (2026-10-29) : marque = étoile Spark monochrome
(static/img/brand-spark.png, copie de ui/barre-apps/brand-spark.png
de commons — blanche sur barre foncée, noire sur barre claire
via le filtre du CSS vendangé) ; onglet agent et son entrée de
menu GARDENT le logo rouge ; anneau rouge #c00000 autour du
bouton avatar flottant (bordure du CSS vendangé). Thème de la
barre : data-theme sur <html> (fd_theme, défaut light) — les
règles thème de commons s'appliquent, aucune surcharge propre
n'est nécessaire. #}
<link rel="stylesheet" href="/static/css/draco-alertes.css?v={{ asset_version }}">
<header class="draco-bar" data-current="flowdeck">
<div class="draco-bar-zone draco-bar-gauche">
<button type="button" class="draco-icon-btn" data-act="rail" aria-label="Barre latérale">☰</button>
<a class="draco-brand" href="/" data-act="apps" aria-haspopup="true" aria-expanded="false" aria-label="dracodev — changer d&#39;application" title="Changer d&#39;application">
<span class="draco-brand-mark">✦</span><span class="draco-brand-name">dracodev</span>
<span class="draco-brand-mark"><img src="/static/img/brand-spark.png" alt="" width="26" height="26"></span><span class="draco-brand-name">dracodev</span>
</a>
<nav class="draco-apptabs" aria-label="Applications">
<a class="draco-apptab" data-app="obsigate" href="{{ draco_urls().obsigate }}/">
<span class="draco-apptab-ico" style="--app:#7a5af5">▤</span>
<span class="draco-apptab-ico" style="--app:#7a5af5" data-icon="obsigate"><img src="/static/img/logos/obsigate.png" alt="ObsiGate" width="30" height="30"></span>
<span class="draco-apptab-txt"><strong>ObsiGate</strong><small>Notes</small></span>
</a>
<a class="draco-apptab" data-app="flowdeck" href="{{ draco_urls().flowdeck }}/">
<span class="draco-apptab-ico" style="--app:#2f6fed">▦</span>
<span class="draco-apptab-ico" style="--app:#2f6fed" data-icon="flowdeck"><img src="/static/img/logos/flowdeck.png" alt="Flowdeck" width="30" height="30"></span>
<span class="draco-apptab-txt"><strong>Flowdeck</strong><small>Projets</small></span>
</a>
<a class="draco-apptab" data-app="tiktak" href="{{ draco_urls().tiktak }}/">
<span class="draco-apptab-ico" style="--app:#e8762c">✓</span>
<span class="draco-apptab-ico" style="--app:#e8762c" data-icon="tiktak"><img src="/static/img/logos/tiktak.svg" alt="TikTak" width="30" height="30"></span>
<span class="draco-apptab-txt"><strong>TikTak</strong><small>Tâches</small></span>
<span class="draco-badge" data-badge="tiktak" hidden>0</span>
</a>
<a class="draco-apptab" data-app="kanbante" href="{{ draco_urls().kanbante }}/">
<span class="draco-apptab-ico" style="--app:#0d9488">▥</span>
<span class="draco-apptab-ico" style="--app:#0d9488" data-icon="kanbante"><img src="/static/img/logos/kanbante.png" alt="Kanbante" width="30" height="30"></span>
<span class="draco-apptab-txt"><strong>Kanbante</strong><small>Kanban</small></span>
<span class="draco-badge" data-badge="kanbante" hidden>0</span>
</a>
<a class="draco-apptab" data-app="agent" href="{{ draco_urls().agent }}/">
<span class="draco-apptab-ico" style="--app:#c2437e">✦</span>
<span class="draco-apptab-ico" style="--app:#c00000" data-icon="agent"><img src="/static/img/logos/spark.png" alt="Console Spark" width="30" height="30"></span>
<span class="draco-apptab-txt"><strong>Console Spark</strong><small>Agent commun</small></span>
</a>
</nav>
@@ -57,16 +76,12 @@
</div>
<button type="button" class="draco-alertes" data-act="alertes" aria-label="Alertes unifiées">
<svg viewBox="0 0 24 24" width="19" height="19" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/></svg>
<span class="draco-alertes-txt">Alertes</span>
<span class="draco-badge" data-badge="alertes" hidden>0</span>
</button>
<button type="button" class="draco-spark" data-act="spark" aria-label="Ouvrir Spark, l'agent commun">
<span class="draco-spark-ico">✦</span><span class="draco-spark-txt">Spark</span>
<span class="draco-badge" data-badge="spark" hidden>0</span>
</button>
<button type="button" class="draco-avatar" data-act="compte" aria-label="Compte" aria-haspopup="menu" aria-expanded="false" title="{{ user.get('full_name') if user and user.get('full_name') else '' }}">{{ user.full_name[:1].upper() if user and user.get('full_name') else 'F' }}</button>
</div>
</header>
<button type="button" class="draco-spark-flottant" data-act="spark" aria-label="Ouvrir Spark, l'agent commun"><img src="/static/img/avatar-spark.png" alt="" width="64" height="64"></button>
<div class="draco-bar-rappel" data-act="rappel-entete" hidden>✦ dracodev — afficher l'en-tête</div>
<script nonce="{{ csp_nonce() }}">
/* Configuration Flowdeck de la barre commune (Fusion ACC-1.1).
+26 -4
View File
@@ -1,18 +1,36 @@
{% extends "base.html" %}
{% block title_prefix %}Rechercher partout{% endblock %}
{% block content %}
<link rel="stylesheet" href="/static/css/recherche.css">
{# Versionnes (?v=asset_version) comme les autres feuilles/scripts de
base.html : sans version, le service worker (static/sw.js, cache-first
des .js/.css) resservait indefiniment les copies pre-#33 de ces deux
fichiers aux navigateurs les ayant deja visites — la page paraissait
donc « non corrigee » malgre les PR #33/#36. #}
<link rel="stylesheet" href="/static/css/recherche.css?v={{ asset_version }}">
{# Console : data-base-console rendu explicitement, comme les 3 autres
hotes — la route Flowdeck de /recherche expose la base agent dans
son contexte `bases` (defaut 6). recherche.js garde ses replis
(derivation -dev, href d'onglet de barre) si l'attribut est vide. #}
<main class="rp-page" id="recherche" data-host="flowdeck"
data-base-obsigate="{{ bases.obsigate }}"
data-base-flowdeck="{{ bases.flowdeck }}"
data-base-tiktak="{{ bases.tiktak }}"
data-base-kanbante="{{ bases.kanbante }}">
data-base-kanbante="{{ bases.kanbante }}"
data-base-console="{{ bases.agent }}">
<h1>Rechercher partout</h1>
<p class="rp-hint">Notes ObsiGate, pages Flowdeck, tâches TikTak, cartes Kanbante — dans l'environnement courant (dev reste dev). « / » ou Ctrl+K pour rechercher, ↑/↓ puis Entrée pour ouvrir un résultat.</p>
<p class="rp-hint">Notes ObsiGate, pages Flowdeck, tâches TikTak, cartes Kanbante — dans l'environnement courant (dev reste dev). « / » ou Ctrl+K pour rechercher, ↑/↓ puis Entrée pour ouvrir un résultat. « Tout voir » bascule vers l'onglet de l'application, sans quitter la page.</p>
<div class="rp-form">
<input id="rp-q" class="rp-input" type="search" name="q" value="{{ q }}"
placeholder="Rechercher partout…" autocomplete="off" autofocus>
</div>
<nav class="rp-tabs" role="tablist" aria-label="Résultats par application">
<button type="button" class="rp-tab active" role="tab" aria-selected="true" data-rp-tab="all">Tout <span class="rp-tab-count"></span></button>
<button type="button" class="rp-tab" role="tab" aria-selected="false" tabindex="-1" data-rp-tab="obsigate">ObsiGate <span class="rp-tab-count"></span></button>
<button type="button" class="rp-tab" role="tab" aria-selected="false" tabindex="-1" data-rp-tab="flowdeck">Flowdeck <span class="rp-tab-count"></span></button>
<button type="button" class="rp-tab" role="tab" aria-selected="false" tabindex="-1" data-rp-tab="tiktak">TikTak <span class="rp-tab-count"></span></button>
<button type="button" class="rp-tab" role="tab" aria-selected="false" tabindex="-1" data-rp-tab="kanbante">Kanbante <span class="rp-tab-count"></span></button>
<button type="button" class="rp-tab" role="tab" aria-selected="false" tabindex="-1" data-rp-tab="console">Console Spark <span class="rp-tab-count"></span></button>
</nav>
<section class="rp-section" data-rp-section="obsigate">
<div class="rp-head"><h2>ObsiGate — notes</h2><span class="rp-count"></span><a class="rp-all" href="#">Tout voir</a></div>
<div class="rp-body"><p class="rp-state">Tapez une recherche ci-dessus.</p></div>
@@ -29,8 +47,12 @@
<div class="rp-head"><h2>Kanbante — cartes</h2><span class="rp-count"></span><a class="rp-all" href="#">Tout voir</a></div>
<div class="rp-body"><p class="rp-state">Tapez une recherche ci-dessus.</p></div>
</section>
<section class="rp-section" data-rp-section="console">
<div class="rp-head"><h2>Console Spark — discussions</h2><span class="rp-count"></span><a class="rp-all" href="#">Tout voir</a></div>
<div class="rp-body"><p class="rp-state">Tapez une recherche ci-dessus.</p></div>
</section>
</main>
{% endblock %}
{% block scripts %}
<script src="/static/js/recherche.js"></script>
<script src="/static/js/recherche.js?v={{ asset_version }}"></script>
{% endblock %}
+36 -11
View File
@@ -7,6 +7,11 @@
menu des applications) ; l'avatar est un vrai <button>.
Correctif fusion #70 (SANS bump) : bouton trois points et menu par
défaut supprimés ; le menu de la marque gagne « Console Spark ».
v0.1.10 : rose --agent → rouge Spark #c00000 ; marque = logo Spark ;
bouton Spark supprimé (remplacé par l'avatar flottant bas-centre) ;
Alertes icône seule.
v0.1.11 : marque = étoile Spark monochrome (blanche barre foncée,
noire barre claire) ; anneau rouge #c00000 autour de l'avatar flottant.
Jetons repris du prototype validé Console v7.2.
Thème : clair par défaut, sombre via [data-theme="dark"] sur <html>
ou prefers-color-scheme si l'app intégratrice ne force rien. */
@@ -14,7 +19,7 @@
.draco-bar {
--bg: #f2f2f0; --surface: #ffffff; --surface-2: #e9eae6; --surface-3: #f6f6f3;
--ink: #1b1d1a; --ink-2: #5c615a; --ink-3: #787d75; --line: #dedfd9;
--agent: #c2437e; --danger: #cf4444;
--agent: #c00000; --danger: #cf4444;
position: relative;
height: 60px; box-sizing: border-box;
display: grid; grid-template-columns: auto minmax(200px, 520px) auto;
@@ -37,7 +42,15 @@
.draco-brand { display: flex; align-items: center; gap: 7px; color: var(--ink); text-decoration: none; font-weight: 800; cursor: pointer; border-radius: 10px; padding: 4px 6px; margin: -4px -6px; }
.draco-brand:hover { background: var(--surface-2); }
.draco-brand:focus-visible { outline: 2px solid var(--agent); outline-offset: 2px; }
.draco-brand-mark { color: var(--agent); font-size: 24px; line-height: 1; }
.draco-brand-mark { line-height: 0; }
/* Marque monochrome v0.1.11 : étoile blanche extraite du logo rouge,
fond transparent — noire sur barre claire (filtre), blanche sur
barre foncée (filtre annulé par les règles thème ci-dessous). */
.draco-brand-mark img { width: 26px; height: 26px; object-fit: cover; display: block; filter: brightness(0); }
@media (prefers-color-scheme: dark) {
.draco-bar .draco-brand-mark img { filter: none; }
}
[data-theme="dark"] .draco-bar .draco-brand-mark img { filter: none; }
/* Menu des applications — construit par le JS depuis les onglets */
.draco-apps-menu {
@@ -94,6 +107,13 @@
width: 30px; height: 30px; flex: none; display: grid; place-items: center;
border-radius: 9px; background: var(--app); color: #fff; font-size: 15px;
}
/* Icônes logos (2026-10-29, corrigé le même jour) : l'image du logo
* fourni, telle quelle, remplit la tuile 30 px — jamais de glyphe
* texte, jamais de redessin. Le fond de la tuile est transparent :
* les logos PNG portent déjà leur fond (dégradé), le SVG TikTak
* reste sur le fond de la barre. */
.draco-apptab-ico { background: transparent; }
.draco-apptab-ico img { width: 30px; height: 30px; display: block; border-radius: 9px; object-fit: cover; }
.draco-apptab-txt { display: none; flex-direction: column; line-height: 1.15; }
.draco-apptab-txt strong { font-size: 14px; font-weight: 700; }
.draco-apptab-txt small { font-size: 11.5px; color: var(--ink-2); }
@@ -131,18 +151,23 @@
/* Alertes — la cloche ne disparaît JAMAIS (correctif v7.2) */
.draco-alertes {
position: relative; display: flex; align-items: center; gap: 7px; height: 38px;
padding: 0 11px; border-radius: 10px; border: 1px solid var(--line);
position: relative; display: flex; align-items: center; height: 38px;
padding: 0 9px; border-radius: 10px; border: 1px solid var(--line);
background: transparent; color: var(--ink); font: inherit; cursor: pointer;
}
/* Spark */
.draco-spark {
position: relative; display: flex; align-items: center; gap: 7px; height: 38px;
padding: 0 14px; border: 0; border-radius: 10px; cursor: pointer;
background: var(--agent); color: #fff; font: inherit; font-weight: 700;
/* Bouton avatar Spark flottant — bas-centre, ouvre le panneau Spark
de l'hôte via data-act="spark" (délégation globale existante).
La console Spark ne l'inclut pas : son fragment est propre. */
.draco-spark-flottant {
position: fixed; left: 50%; bottom: 20px; transform: translateX(-50%);
z-index: 65; width: 64px; height: 64px; padding: 0; border: 0;
border-radius: 50%; background: transparent; cursor: pointer;
box-shadow: 0 8px 28px rgb(0 0 0 / .45);
}
.draco-spark .draco-badge { border-color: var(--agent); }
.draco-spark-flottant img { width: 64px; height: 64px; border-radius: 50%; object-fit: cover; display: block; box-sizing: border-box; border: 3px solid var(--agent); }
.draco-spark-flottant:hover { transform: translateX(-50%) scale(1.05); }
.draco-spark-flottant:focus-visible { outline: 2px solid var(--agent); outline-offset: 3px; }
.draco-icon-btn {
width: 38px; height: 38px; border-radius: 10px; border: 1px solid transparent;
@@ -169,7 +194,7 @@
.draco-bar { grid-template-columns: auto minmax(200px, 440px) auto; }
}
@media (max-width: 1200px) {
.draco-search-txt, .draco-search kbd, .draco-alertes-txt, .draco-spark-txt { display: none; }
.draco-search-txt, .draco-search kbd { display: none; }
.draco-search { width: 38px; padding: 0; justify-content: center; }
.draco-bar { grid-template-columns: auto auto 1fr; }
.draco-bar-droite { justify-content: flex-end; }
+55
View File
@@ -33,3 +33,58 @@
.rp-title { display: block; font-weight: 600; }
.rp-context { display: block; font-size: .8rem; opacity: .65; }
.rp-snippet { display: block; font-size: .88rem; opacity: .85; margin-top: 2px; }
/* ---- Onglets (chantier 2+3) : mêmes onglets sur les 4 hôtes. ---- */
.rp-tabs {
display: flex; gap: 4px; flex-wrap: wrap;
border-bottom: 1px solid #888; margin-bottom: 20px;
}
.rp-tab {
background: none; border: none; border-bottom: 2px solid transparent;
padding: 8px 10px; margin-bottom: -1px; font: inherit; cursor: pointer;
color: inherit; opacity: .7; white-space: nowrap;
}
.rp-tab:hover { opacity: 1; }
.rp-tab.active { opacity: 1; border-bottom-color: #3b82f6; font-weight: 700; }
.rp-tab:focus-visible { outline: 2px solid #3b82f6; outline-offset: 2px; }
.rp-tab-count {
font-size: .8rem; font-weight: 700; background: rgba(127, 127, 127, .25);
border-radius: 999px; padding: 1px 7px; margin-left: 2px;
}
.rp-tab-count:empty { display: none; }
.rp-section[hidden] { display: none; }
.rp-all[hidden] { display: none; }
/* ---- TikTak : la page sort de la colonne sidebar (chantier 2+3).
* Cause : recherche.html étend base.html sans layout_class, donc
* .rp-page (enfant direct de .layout) tombait en auto-placement dans
* la 2e colonne de la grille (largeur sidebar). La règle ci-dessous,
* gardée par l'ancêtre .layout (TikTak seul), l'étend de la colonne 2
* à la fin de la grille — juste à droite du rail d'icônes — sur toute
* la largeur disponible, sans toucher aux autres pages (elles ne
* portent pas .rp-page). En mobile (≤760 px, point de bascule TikTak),
* la grille n'a qu'une colonne : la page la prend entièrement. */
.layout > .rp-page {
grid-column: 2 / -1; max-width: none; min-width: 0; overflow-y: auto;
}
@media (max-width: 760px) {
.layout > .rp-page { grid-column: 1 / -1; }
}
/* ---- ObsiGate : navigation statique de la page autonome (chantier
* 2+3). La page /recherche est servie hors SPA (route avant le
* catch-all) : elle reprend les classes de l'app (style.css :
* .app-container, .header, .main-body, .sidebar, .content-wrapper,
* .content-area) et cette navigation, calée sur les variables de
* l'app, en guise de sidebar. Les vaults dynamiques restent dans
* la SPA (voir le commentaire dans recherche.html). */
.rp-og-nav { display: flex; flex-direction: column; gap: 4px; padding: 12px; }
.rp-og-nav a {
display: block; padding: 8px 10px; border-radius: 8px;
text-decoration: none; color: inherit;
}
.rp-og-nav a:hover { background: rgba(127, 127, 127, .15); }
.rp-og-nav a[aria-current="page"] {
background: rgba(127, 127, 127, .18); font-weight: 700;
}
.rp-og-note { opacity: .65; font-size: .85rem; padding: 8px 10px; margin: 8px 0 0; }
Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

+8
View File
@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 240">
<!-- TikTak — deux arcs (les deux temps), coche-flèche. Bleu #0076fb, orange #fc750c.
Géométrie mesurée sur le visuel validé par Bruno (variante B). -->
<path d="M 38.2 101.1 A 84 84 0 0 1 201.8 101.1" fill="none" stroke="#0076fb" stroke-width="28" stroke-linecap="round"/>
<path d="M 201.5 140.3 A 84 84 0 0 1 38.5 140.3" fill="none" stroke="#fc750c" stroke-width="28" stroke-linecap="round"/>
<polyline points="80,129 106,146 163,84" fill="none" stroke="#0076fb" stroke-width="24" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M 128 80 L 163 84 L 167 122" fill="none" stroke="#0076fb" stroke-width="22" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 760 B

+641 -66
View File
@@ -1,11 +1,39 @@
/* Recherche partout — page de résultats unifiée (fusion-trois-apps #68).
/* Recherche partout — page de résultats unifiée (fusion-trois-apps #68,
* chantier 2+3 : onglets, « Tout voir », focus).
*
* Vanilla, sans dépendance. Un seul fichier vendangé tel quel par chaque
* hôte (ObsiGate, Flowdeck, TikTak, Kanbante) : la configuration vient
* des attributs data-* de #recherche, rendus côté serveur par l'hôte.
*
* Présentation en onglets : Tout, ObsiGate, Flowdeck, TikTak, Kanbante,
* Console Spark. « Tout » = les 4 sections groupées, chaque liste
* tronquée à PER_APP_LIMIT ; un onglet application = tous les résultats
* de cette application (aucune troncature côté client — la requête
* ObsiGate part avec limit=FULL_LIMIT, les autres API renvoient leur
* jeu complet). « Tout voir » d'une section ne quitte plus la page :
* il bascule vers l'onglet de l'application (son href reste une URL
* partageable ?q=&tab= au cas où le JS ne tournerait pas).
* L'onglet actif vit dans l'URL (?tab=, absent = Tout) : partageable,
* et le retour arrière navigateur le restaure (popstate).
*
* Compteurs : chaque onglet affiche le nombre TOTAL de résultats de
* son application (pas le nombre tronqué affiché dans « Tout »),
* « … » pendant le chargement, « 0 » pour un vide honnête, rien pour
* une section en erreur/indisponible (pas de faux 0). L'onglet Tout
* affiche « … » tant qu'une section charge, sinon la somme des totaux
* connus (une section en erreur compte pour 0) ; il est vide si les
* 4 sections sont en erreur ou si aucune recherche n'a encore eu lieu.
* Une section injoignable ne casse jamais les autres.
*
* Focus : l'attribut autofocus seul ne survit pas au chargement des
* surcouches (barre, palette, app) — la page appelle donc focus()
* explicitement en fin d'init (plus un rappel rAF/timeout et un
* rappel pageshow). Cliquer la barre « Rechercher partout » (bouton
* data-act="palette") sur cette même page replace aussi le curseur
* dans la boîte ; « / » et Ctrl+K / ⌘K font de même, comme avant.
*
* Contrat d'API (audité, rien d'inventé) :
* - ObsiGate : GET {base}/api/search?q=&vault=all&limit=8
* - ObsiGate : GET {base}/api/search?q=&vault=all&limit=
* -> { results: [{ vault, path, title, snippet }] }
* Lien profond vérifié : {base}#file={vault}:{path} (Viewer.openFile).
* - Flowdeck : GET {base}/api/search?q=
@@ -14,10 +42,39 @@
* - TikTak : GET {base}/api/search?q=
* -> { tasks: [{ id, title, description, status, due_at }], ... }
* Lien profond vérifié : {base}search?q=<q>&task=<id> (page /search).
* - Kanbante : GET {base}/api/search?q= (NOUVEAU, cette livraison)
* - Kanbante : GET {base}/api/search?q=
* -> { groups: [{ full_name, rows: [{ issue, column }] }] }
* Lien profond : {base}board/<full_name> (la carte n'a pas d'URL propre
* adressable — même limite que la page /search native de Kanbante).
* - Console Spark : GET {base-console}/api/v1/recherche?q=&limite=
* (agent-commun, authentifié par le cookie SSO draco_session) ->
* { q, domain, limite_par_type, resultats[], par_type,
* non_cherchables[] } ; chaque résultat porte type, id, titre,
* extrait, statut, domaine, section, objet et lien (relatif à
* l'origine console). q < 2 caractères -> 422. Types cherchables :
* discussion, mission, approbation, livrable, activite,
* planification, objectif, identite (aucun secret). Liens profonds
* (préfixés par la base console) : discussion
* /?section=conversation&disc=<id>, mission
* /?section=missions&objet=<id>, approbation
* /?section=approbations&objet=<id>, objectif
* /?section=objectifs&objet=<id>, activite /?activite=<id> ;
* livrable /?section=livrables, planification /?section=planifie
* et identite /?section=identites ouvrent la vue sans sélection
* d'objet. L'onglet appelle avec limite=20 (plafond par type) car
* il affiche la liste complète, comme les onglets d'applications.
* En cas d'échec, de HTTP non-OK ou de format non reconnu, la
* section affiche honnêtement « Console Spark : recherche
* indisponible pour l'instant » — jamais de faux résultats,
* jamais de faux 0. La base console vient, dans l'ordre, de
* data-base-console rendu par l'hôte, de window.DRACO_URLS.agent
* (ObsiGate) / window.DRACO_AGENT_URL (TikTak), de la dérivation
* -dev si l'hôte courant est -dev, du href d'un onglet de barre
* data-app="console"/"spark"/"agent" s'il existe, sinon du défaut
* prod agent.dracodev.net. L'onglet console n'est interrogé que
* lorsqu'il est affiché (ou quand une recherche a lieu pendant
* qu'il est affiché) : « Tout » ne le contient pas, pour ne pas
* polluer la vue groupée d'un message d'indisponibilité.
*
* Résolution des bases sœurs (même ordre que la barre v0.1.6) :
* 1. data-base-* rendus par l'hôte (son propre résolveur : explicite >
@@ -31,9 +88,12 @@
* credentials:"include". Le navigateur envoie alors les cookies de
* chaque origine, dont le cookie SSO partagé draco_session
* (Domain=.dracodev.net), accepté par les 4 API de recherche
* (ObsiGate et TikTak nativement, Flowdeck et Kanbante via cette
* livraison). Chaque sœur n'autorise en CORS que les origines sœurs
* (GET sur l'endpoint de recherche uniquement, pas de "*").
* (ObsiGate et TikTak nativement, Flowdeck et Kanbante via #68)
* et par GET /api/v1/recherche de la console (identity() accepte
* draco_session). Chaque sœur n'autorise en CORS que les origines
* sœurs (GET sur l'endpoint de recherche uniquement, pas de "*") ;
* côté console, un middleware borné à /api/v1/recherche autorise
* de même les 8 origines hôtes prod ET dev, avec credentials.
* Une sœur injoignable / non autorisée ne casse pas les autres :
* sa section affiche l'erreur + un lien vers sa recherche native.
*/
@@ -56,12 +116,36 @@
tiktak: "https://tiktak-dev.dracodev.net/",
kanbante: "https://kanbante-dev.dracodev.net/",
};
var PROD_AGENT = "https://agent.dracodev.net/";
var DEV_AGENT = "https://agent-dev.dracodev.net/";
var CONSOLE_SEARCH_PATH = "api/v1/recherche";
var CONSOLE_SEARCH_LIMIT = 20;
var APPS = [
{ key: "obsigate", label: "ObsiGate" },
{ key: "flowdeck", label: "Flowdeck" },
{ key: "tiktak", label: "TikTak" },
{ key: "kanbante", label: "Kanbante" },
];
var CONSOLE_APP = { key: "console", label: "Console Spark" };
var CONSOLE_TYPE_LABELS = {
discussion: "Discussion",
mission: "Mission",
approbation: "Approbation",
livrable: "Livrable",
activite: "Activité",
planification: "Planification",
objectif: "Objectif",
identite: "Identité",
};
/* Lien profond console (relatif à l'origine console) quand le
* résultat n'en porte pas : même règle que _lien_recherche
* d'agent-commun (section + objet, activite en section=null). */
var CONSOLE_VIEW_LINKS = {
livrable: "/?section=livrables",
planification: "/?section=planifie",
identite: "/?section=identites",
};
var TABS = ["all", "obsigate", "flowdeck", "tiktak", "kanbante", "console"];
var COLUMN_LABELS = {
backlog: "Backlog",
in_progress: "En cours",
@@ -73,6 +157,7 @@
var TIMEOUT_MS = 8000;
var DEBOUNCE_MS = 250;
var PER_APP_LIMIT = 8;
var FULL_LIMIT = 100;
function esc(s) {
return String(s == null ? "" : s)
@@ -120,9 +205,92 @@
return out;
}
function searchPath(app, q) {
/* Base console : explicite > URL agent injectée par l'hôte >
* dérivation -dev > href d'onglet de barre console/spark > prod.
* opts : { host, explicit, agentUrl, tabHrefs:{} } */
function resolveConsoleBase(opts) {
opts = opts || {};
var u = cleanUrl(opts.explicit);
if (!u) u = cleanUrl(opts.agentUrl);
if (!u && isDevHost(opts.host)) u = DEV_AGENT;
if (!u)
u = cleanUrl(
opts.tabHrefs &&
(opts.tabHrefs.console || opts.tabHrefs.spark || opts.tabHrefs.agent)
);
if (!u) u = PROD_AGENT;
return withSlash(u);
}
/* Onglet valide dans l'URL, sinon "all". */
function parseTab(v) {
return TABS.indexOf(v) !== -1 ? v : "all";
}
/* Sections visibles pour un onglet. « Tout » = les 4 sections
* groupées (vue #68) ; la console n'y figure pas (voir en-tête). */
function visibleKeys(tab) {
tab = parseTab(tab);
if (tab === "all") return APPS.map(function (a) { return a.key; });
return [tab];
}
/* Troncature d'affichage : liste complète dans l'onglet propre à
* l'application, PER_APP_LIMIT partout ailleurs (vue « Tout »). */
function pageItems(items, full) {
items = items || [];
return full ? items : items.slice(0, PER_APP_LIMIT);
}
/* ?q=&tab= partageable. tab "all" omis (c'est le défaut). */
function buildSearchUrl(q, tab) {
var parts = [];
if (q) parts.push("q=" + encodeURIComponent(q));
tab = parseTab(tab);
if (tab !== "all") parts.push("tab=" + encodeURIComponent(tab));
return parts.length ? "?" + parts.join("&") : "";
}
/* Libellés des compteurs d'onglets à partir des états :
* states : { key: { status, total } }.
* Chargement -> "…", ok/vide -> total ("0" honnête),
* erreur/indisponible/inactif -> "" (pas de faux 0).
* Tout : "…" tant qu'une section charge, sinon somme des totaux
* connus, "" si les 4 sections sont en erreur/inactives. */
function tabCounts(states) {
states = states || {};
var out = {};
function labelFor(key) {
var st = states[key] || {};
if (st.status === "loading") return "…";
if (st.status === "ok" || st.status === "empty") return String(st.total || 0);
return "";
}
var allLoading = false;
var allTotal = 0;
var allKnown = false;
APPS.forEach(function (app) {
var st = states[app.key] || {};
out[app.key] = labelFor(app.key);
if (st.status === "loading") allLoading = true;
if (st.status === "ok" || st.status === "empty") {
allKnown = true;
allTotal += st.total || 0;
}
});
out.console = labelFor("console");
out.all = allLoading ? "…" : allKnown ? String(allTotal) : "";
return out;
}
function searchPath(app, q, full) {
if (app === "obsigate") {
return "api/search?q=" + encodeURIComponent(q) + "&vault=all&limit=" + PER_APP_LIMIT;
return (
"api/search?q=" +
encodeURIComponent(q) +
"&vault=all&limit=" +
(full ? FULL_LIMIT : PER_APP_LIMIT)
);
}
return "api/search?q=" + encodeURIComponent(q);
}
@@ -133,11 +301,17 @@
}
/* Normalise la réponse d'une app en [{ title, snippet, context, url }].
* base : base résolue de l'app (avec / final). q : requête (liens TikTak). */
function normalize(app, data, base, q) {
* base : base résolue de l'app (avec / final). q : requête (liens TikTak).
* full=true : aucune troncature (stockage complet) ; sinon PER_APP_LIMIT
* (comportement historique, conservé par défaut pour les tests). */
function normalize(app, data, base, q, full) {
data = data || {};
var limit = full ? null : PER_APP_LIMIT;
function cut(arr) {
return limit == null ? arr : arr.slice(0, limit);
}
if (app === "obsigate") {
return (data.results || []).slice(0, PER_APP_LIMIT).map(function (r) {
return cut(data.results || []).map(function (r) {
return {
title: r.title || r.path || "Sans titre",
snippet: snippetOf(r.snippet || ""),
@@ -153,7 +327,7 @@
}
if (app === "flowdeck") {
var items = (data.pages || []).concat(data.collections || []);
return items.slice(0, PER_APP_LIMIT).map(function (r) {
return cut(items).map(function (r) {
var rel = String(r.url || "").replace(/^\//, "");
return {
title: r.title || "Sans titre",
@@ -164,7 +338,7 @@
});
}
if (app === "tiktak") {
return (data.tasks || []).slice(0, PER_APP_LIMIT).map(function (t) {
return cut(data.tasks || []).map(function (t) {
var ctx = t.status === "done" ? "Terminée" : "Active";
if (t.due_at) ctx += " · échéance " + String(t.due_at).slice(0, 10);
return {
@@ -182,7 +356,7 @@
rows.push({ group: g.full_name, row: row });
});
});
return rows.slice(0, PER_APP_LIMIT).map(function (x) {
return cut(rows).map(function (x) {
var issue = x.row.issue || {};
var col = COLUMN_LABELS[x.row.column] || x.row.column || "";
return {
@@ -196,6 +370,86 @@
return [];
}
/* Normalisation console : format réel GET /api/v1/recherche,
* { resultats: [{ type, id, titre, extrait, statut, domaine,
* section, objet, lien }] }. Le lien relatif est préfixé par la
* base console ; sans lien, il est reconstruit depuis type/id
* (même règle que la console) ; sans lien reconstructible,
* url=null et le résultat est rendu en <span>, pas en faux lien.
* Les anciennes formes tolérées (results/discussions/items, avec
* url/href/link) restent acceptées en repli, pour ne pas casser
* un déploiement console antérieur. Retourne { recognized,
* items } : recognized=false -> l'appelant affiche
* « indisponible », jamais un vide mensonger. */
function consoleLinkFor(r) {
var lien = r.lien || "";
if (lien) return lien;
var id = r.id != null ? String(r.id) : "";
if (r.type === "discussion")
return "/?section=conversation&disc=" + encodeURIComponent(id);
if (r.type === "mission")
return "/?section=missions&objet=" + encodeURIComponent(id);
if (r.type === "approbation")
return "/?section=approbations&objet=" + encodeURIComponent(id);
if (r.type === "objectif")
return "/?section=objectifs&objet=" + encodeURIComponent(id);
if (r.type === "activite") return "/?activite=" + encodeURIComponent(id);
if (CONSOLE_VIEW_LINKS[r.type]) return CONSOLE_VIEW_LINKS[r.type];
if (r.section)
return (
"/?section=" +
encodeURIComponent(r.section) +
(r.objet != null
? "&objet=" + encodeURIComponent(String(r.objet))
: "")
);
return "";
}
function normalizeConsole(data, base) {
data = data || {};
if (Array.isArray(data.resultats)) {
var items = data.resultats.map(function (r) {
r = r || {};
var rel = consoleLinkFor(r);
var url = null;
if (/^https?:\/\//.test(rel)) url = rel;
else if (rel.charAt(0) === "/" || rel.charAt(0) === "#")
url = base + String(rel).replace(/^\//, "");
var label = CONSOLE_TYPE_LABELS[r.type] || r.type || "Console";
var context = label;
if (r.statut) context += " · " + r.statut;
if (r.domaine) context += " · " + r.domaine;
return {
title: r.titre || r.title || "Sans titre",
snippet: snippetOf(r.extrait || r.snippet || ""),
context: context,
url: url,
};
});
return { recognized: true, items: items };
}
var list = null;
if (Array.isArray(data.results)) list = data.results;
else if (Array.isArray(data.discussions)) list = data.discussions;
else if (Array.isArray(data.items)) list = data.items;
if (!list) return { recognized: false, items: [] };
var legacy = list.map(function (r) {
r = r || {};
var raw = r.url || r.href || r.link || "";
var url = null;
if (/^https?:\/\//.test(raw)) url = raw;
else if (raw.charAt(0) === "/" || raw.charAt(0) === "#") url = base + String(raw).replace(/^\//, "");
return {
title: r.title || r.name || r.subject || "Sans titre",
snippet: snippetOf(r.snippet || r.excerpt || r.preview || r.body || ""),
context: r.subtitle || r.source || r.type || "",
url: url,
};
});
return { recognized: true, items: legacy };
}
/* Recherche native de l'app (repli quand la section est en erreur). */
function nativeSearchUrl(app, base, q) {
if (app === "tiktak" || app === "kanbante") {
@@ -214,6 +468,7 @@
}
function appLabel(key) {
if (key === "console") return CONSOLE_APP.label;
for (var i = 0; i < APPS.length; i++) {
if (APPS[i].key === key) return APPS[i].label;
}
@@ -224,11 +479,26 @@
esc: esc,
isDevHost: isDevHost,
resolveBases: resolveBases,
resolveConsoleBase: resolveConsoleBase,
parseTab: parseTab,
visibleKeys: visibleKeys,
pageItems: pageItems,
buildSearchUrl: buildSearchUrl,
tabCounts: tabCounts,
searchPath: searchPath,
normalize: normalize,
normalizeConsole: normalizeConsole,
nativeSearchUrl: nativeSearchUrl,
PROD: PROD,
DEV: DEV,
PROD_AGENT: PROD_AGENT,
DEV_AGENT: DEV_AGENT,
CONSOLE_SEARCH_PATH: CONSOLE_SEARCH_PATH,
CONSOLE_SEARCH_LIMIT: CONSOLE_SEARCH_LIMIT,
CONSOLE_TYPE_LABELS: CONSOLE_TYPE_LABELS,
consoleLinkFor: consoleLinkFor,
PER_APP_LIMIT: PER_APP_LIMIT,
FULL_LIMIT: FULL_LIMIT,
};
if (typeof module !== "undefined" && module.exports) module.exports = api;
global.RecherchePartout = api;
@@ -268,16 +538,24 @@
});
}
function renderSection(section, state) {
/* state : { status: "loading"|"empty"|"error"|"ok", items, message, q, base, app } */
function renderSection(section, state, full) {
/* state : { status: "idle"|"loading"|"empty"|"error"|"ok"|"unavailable",
* items (complets), message, q, base, app }.
* full=true (onglet propre à l'app) : tout afficher ; sinon
* PER_APP_LIMIT (vue « Tout »). Le compteur affiche le TOTAL. */
var body = section.querySelector(".rp-body");
var count = section.querySelector(".rp-count");
var toutVoir = section.querySelector(".rp-all");
toutVoir.href = nativeSearchUrl(state.app, state.base, state.q);
toutVoir.textContent = "Tout voir";
toutVoir.title = nativeSearchLabel(state.app);
if (toutVoir) {
try {
toutVoir.href =
global.location.pathname + buildSearchUrl(state.q, state.app);
} catch (e) {}
toutVoir.textContent = "Tout voir";
toutVoir.title = "Afficher tous les résultats de " + appLabel(state.app);
}
if (state.status === "loading") {
count.textContent = "";
count.textContent = "…";
body.innerHTML = '<p class="rp-state">Recherche…</p>';
return;
}
@@ -293,19 +571,31 @@
"</a></p>";
return;
}
if (state.status === "empty") {
if (state.status === "unavailable") {
count.textContent = "";
body.innerHTML =
'<p class="rp-state">Console Spark : recherche indisponible pour l\'instant' +
(state.message ? " (" + esc(state.message) + ")" : "") +
".</p>";
return;
}
if (state.status === "idle") {
count.textContent = "";
body.innerHTML =
'<p class="rp-state">Tapez une recherche ci-dessus.</p>';
return;
}
if (state.status === "empty") {
count.textContent = "0";
body.innerHTML =
'<p class="rp-state">Aucun résultat pour « ' + esc(state.q) + " ».</p>";
return;
}
var shown = pageItems(state.items, full);
count.textContent = String(state.items.length);
body.innerHTML = state.items
body.innerHTML = shown
.map(function (item) {
return (
'<a class="rp-item" data-rp-link href="' +
esc(item.url) +
'">' +
var inner =
'<span class="rp-title">' +
esc(item.title) +
"</span>" +
@@ -314,9 +604,13 @@
: "") +
(item.snippet
? '<span class="rp-snippet">' + esc(item.snippet) + "</span>"
: "") +
"</a>"
);
: "");
if (item.url) {
return (
'<a class="rp-item" data-rp-link href="' + esc(item.url) + '">' + inner + "</a>"
);
}
return '<span class="rp-item">' + inner + "</span>";
})
.join("");
}
@@ -349,40 +643,213 @@
barUrls: barUrls,
tabHrefs: tabHrefs,
});
var consoleBase = resolveConsoleBase({
host: global.location.hostname,
explicit: root.getAttribute("data-base-console"),
agentUrl:
(global.DRACO_URLS && global.DRACO_URLS.agent) || global.DRACO_AGENT_URL,
tabHrefs: tabHrefs,
});
var allKeys = APPS.map(function (a) { return a.key; }).concat(["console"]);
var sections = {};
APPS.forEach(function (app) {
var el = document.querySelector('[data-rp-section="' + app.key + '"]');
if (el) sections[app.key] = el;
allKeys.forEach(function (key) {
var el = document.querySelector('[data-rp-section="' + key + '"]');
if (el) sections[key] = el;
});
var tabButtons = {};
document.querySelectorAll("[data-rp-tab]").forEach(function (btn) {
tabButtons[btn.getAttribute("data-rp-tab")] = btn;
});
var tablist = document.querySelector(".rp-tabs");
/* États par application (+ console). total = items.length. */
var states = {};
allKeys.forEach(function (key) {
states[key] = {
status: "idle",
items: [],
message: "",
q: "",
base: key === "console" ? consoleBase : bases[key],
app: key,
};
});
var activeTab = "all";
var currentQ = "";
var runId = 0;
var consoleFetchedQ = null;
var debounceTimer = null;
function statesForCounts() {
var out = {};
allKeys.forEach(function (key) {
out[key] = { status: states[key].status, total: states[key].items.length };
});
return out;
}
function renderTabs() {
var counts = tabCounts(statesForCounts());
Object.keys(tabButtons).forEach(function (key) {
var btn = tabButtons[key];
var selected = key === activeTab;
btn.classList.toggle("active", selected);
btn.setAttribute("aria-selected", selected ? "true" : "false");
btn.tabIndex = selected ? 0 : -1;
var span = btn.querySelector(".rp-tab-count");
if (span) span.textContent = counts[key] || "";
});
}
/* Bascule d'onglet : visibilité des sections, re-rendu (plein dans
* l'onglet propre à l'app, tronqué dans « Tout »), compteurs.
* opts : { push (historique), focusTab }. */
function applyTab(tab, opts) {
opts = opts || {};
activeTab = parseTab(tab);
try {
root.setAttribute("data-rp-tab", activeTab);
} catch (e) {}
var visible = visibleKeys(activeTab);
allKeys.forEach(function (key) {
var sec = sections[key];
if (!sec) return;
var show = visible.indexOf(key) !== -1;
sec.hidden = !show;
if (show) renderSection(sec, states[key], activeTab === key);
var tv = sec.querySelector(".rp-all");
if (tv) tv.hidden = activeTab === key;
});
renderTabs();
if (opts.push) {
try {
global.history.pushState(
null,
"",
global.location.pathname + buildSearchUrl(currentQ, activeTab)
);
} catch (e) {}
}
if (opts.focusTab && tabButtons[activeTab]) {
try {
tabButtons[activeTab].focus();
} catch (e) {}
}
}
function setTab(tab, opts) {
applyTab(tab, opts);
/* La console n'est interrogée qu'une fois affichée. */
if (parseTab(tab) === "console" && currentQ && consoleFetchedQ !== currentQ) {
fetchConsole(currentQ, runId);
}
}
function fetchConsole(q, myRun) {
var sec = sections.console;
if (!sec) return;
consoleFetchedQ = q;
states.console = {
status: "loading",
items: [],
message: "",
q: q,
base: consoleBase,
app: "console",
};
renderSection(sec, states.console, true);
renderTabs();
var url =
consoleBase +
CONSOLE_SEARCH_PATH +
"?q=" +
encodeURIComponent(q) +
"&limite=" +
CONSOLE_SEARCH_LIMIT;
fetchSection("console", url, function (err, data) {
if (myRun !== runId) return;
if (err) {
var msg;
if (err.name === "AbortError") msg = "délai dépassé (8 s)";
else if (err.status === 422)
msg = "au moins 2 caractères requis";
else if (err.status) msg = "HTTP " + err.status;
else msg = "injoignable (CORS ou réseau)";
states.console = {
status: "unavailable",
items: [],
message: msg,
q: q,
base: consoleBase,
app: "console",
};
} else {
var norm = normalizeConsole(data, consoleBase);
if (!norm.recognized) {
states.console = {
status: "unavailable",
items: [],
message: "format inattendu",
q: q,
base: consoleBase,
app: "console",
};
} else {
states.console = {
status: norm.items.length ? "ok" : "empty",
items: norm.items,
message: "",
q: q,
base: consoleBase,
app: "console",
};
}
}
renderSection(sec, states.console, true);
renderTabs();
});
}
function runSearch(q) {
currentQ = q;
var myRun = ++runId;
if (!q) {
APPS.forEach(function (app) {
var sec = sections[app.key];
allKeys.forEach(function (key) {
var sec = sections[key];
if (!sec) return;
sec.querySelector(".rp-body").innerHTML =
'<p class="rp-state">Tapez une recherche ci-dessus.</p>';
sec.querySelector(".rp-count").textContent = "";
sec.querySelector(".rp-all").href = nativeSearchUrl(app.key, bases[app.key], "");
states[key] = {
status: "idle",
items: [],
message: "",
q: "",
base: key === "console" ? consoleBase : bases[key],
app: key,
};
renderSection(sec, states[key], activeTab === key);
});
consoleFetchedQ = null;
renderTabs();
return;
}
APPS.forEach(function (app) {
var sec = sections[app.key];
if (!sec) return;
renderSection(sec, { status: "loading", app: app.key, base: bases[app.key], q: q });
states[app.key] = {
status: "loading",
items: [],
message: "",
q: q,
base: bases[app.key],
app: app.key,
};
renderSection(sec, states[app.key], activeTab === app.key);
var url;
if (app.key === hostApp) {
/* Même origine : pas de CORS, pas de CSP cross-origin. */
url = "/" + searchPath(app.key, q);
url = "/" + searchPath(app.key, q, true);
} else {
url = bases[app.key] + searchPath(app.key, q);
url = bases[app.key] + searchPath(app.key, q, true);
}
fetchSection(app.key, url, function (err, data) {
if (myRun !== runId) return;
@@ -393,25 +860,45 @@
msg = "Connexion requise sur " + app.label;
else if (err.status) msg = "Indisponible (HTTP " + err.status + ")";
else msg = "Injoignable depuis cet hôte (CORS ou réseau)";
renderSection(sec, {
states[app.key] = {
status: "error",
items: [],
message: msg,
app: app.key,
base: bases[app.key],
q: q,
});
return;
base: bases[app.key],
app: app.key,
};
} else {
var items = normalize(app.key, data, bases[app.key], q, true);
states[app.key] = {
status: items.length ? "ok" : "empty",
items: items,
message: "",
q: q,
base: bases[app.key],
app: app.key,
};
}
var items = normalize(app.key, data, bases[app.key], q);
renderSection(sec, {
status: items.length ? "ok" : "empty",
items: items,
app: app.key,
base: bases[app.key],
q: q,
});
renderSection(sec, states[app.key], activeTab === app.key);
renderTabs();
});
});
/* Console : seulement si son onglet est affiché ; sinon son
* compteur reste vide (pas de faux 0) jusqu'à son activation. */
if (activeTab === "console") {
fetchConsole(q, myRun);
} else {
states.console = {
status: "idle",
items: [],
message: "",
q: q,
base: consoleBase,
app: "console",
};
consoleFetchedQ = null;
renderTabs();
}
}
function scheduleSearch(q, immediate) {
@@ -427,13 +914,33 @@
function syncUrl(q) {
try {
var url = new URL(global.location.href);
if (q) url.searchParams.set("q", q);
else url.searchParams.delete("q");
global.history.replaceState(null, "", url.toString());
global.history.replaceState(
null,
"",
global.location.pathname + buildSearchUrl(q, activeTab)
);
} catch (e) {}
}
function focusSearch() {
try {
input.focus({ preventScroll: true });
} catch (e) {
try {
input.focus();
} catch (e2) {}
}
}
function visibleLinks() {
return Array.prototype.slice
.call(document.querySelectorAll("[data-rp-link]"))
.filter(function (a) {
var s = a.closest ? a.closest("[data-rp-section]") : null;
return !s || !s.hidden;
});
}
input.addEventListener("input", function () {
var q = input.value.trim();
syncUrl(q);
@@ -441,7 +948,7 @@
});
input.addEventListener("keydown", function (e) {
if (e.key === "Enter") {
var first = document.querySelector("[data-rp-link]");
var first = visibleLinks()[0];
if (first) {
e.preventDefault();
first.click();
@@ -471,6 +978,9 @@
input.select();
return;
}
/* Sur un onglet, ↑/↓ ne déplacent pas les résultats : les
* flèches ←/→ du tablist s'en chargent (voir ci-dessous). */
if (target && target.closest && target.closest(".rp-tab")) return;
if (!typing && (e.key === "ArrowDown" || e.key === "ArrowUp")) {
moveFocus(e.key === "ArrowDown" ? 1 : -1, e);
}
@@ -478,8 +988,62 @@
true
);
/* « Tout voir » reste sur la page : bascule vers l'onglet. */
root.addEventListener("click", function (e) {
var all = e.target && e.target.closest ? e.target.closest(".rp-all") : null;
if (!all) return;
var sec = all.closest ? all.closest("[data-rp-section]") : null;
if (!sec) return;
e.preventDefault();
setTab(sec.getAttribute("data-rp-section"), { push: true, focusTab: true });
});
/* Clic sur la barre « Rechercher partout » (bouton
* data-act="palette") pendant qu'on est déjà sur la page :
* replacer le curseur dans la boîte au lieu de recharger. */
document.addEventListener("click", function (e) {
var p = e.target && e.target.closest ? e.target.closest('[data-act="palette"]') : null;
if (!p) return;
e.preventDefault();
focusSearch();
try {
input.select();
} catch (err) {}
});
/* Onglets : clic + clavier (←/→, Début/Fin). */
Object.keys(tabButtons).forEach(function (key) {
tabButtons[key].addEventListener("click", function () {
setTab(key, { push: true });
});
});
if (tablist) {
tablist.addEventListener("keydown", function (e) {
var order = TABS;
var idx = order.indexOf(activeTab);
var next = null;
if (e.key === "ArrowRight") next = order[(idx + 1) % order.length];
else if (e.key === "ArrowLeft")
next = order[(idx - 1 + order.length) % order.length];
else if (e.key === "Home") next = order[0];
else if (e.key === "End") next = order[order.length - 1];
if (!next) return;
e.preventDefault();
setTab(next, { push: true, focusTab: true });
});
}
/* Retour arrière navigateur : restaurer q + onglet. */
global.addEventListener("popstate", function () {
var params = new URLSearchParams(global.location.search);
var q = (params.get("q") || "").trim();
input.value = q;
applyTab(params.get("tab"));
runSearch(q);
});
function moveFocus(delta, e) {
var links = Array.prototype.slice.call(document.querySelectorAll("[data-rp-link]"));
var links = visibleLinks();
if (!links.length) return;
var active = document.activeElement;
var idx = links.indexOf(active);
@@ -492,12 +1056,23 @@
var params = new URLSearchParams(global.location.search);
var initialQ = (params.get("q") || "").trim();
if (initialQ) {
input.value = initialQ;
runSearch(initialQ);
} else {
runSearch("");
}
var initialTab = parseTab(params.get("tab"));
if (initialQ) input.value = initialQ;
applyTab(initialTab);
runSearch(initialQ);
/* Focus explicite (voir en-tête) : l'attribut autofocus seul ne
* survit pas au chargement des surcouches. */
focusSearch();
try {
if (global.requestAnimationFrame) global.requestAnimationFrame(focusSearch);
} catch (e) {}
try {
setTimeout(focusSearch, 0);
} catch (e) {}
global.addEventListener("pageshow", function (e) {
if (e.persisted) focusSearch();
});
}
if (document.readyState === "loading") {
+16 -5
View File
@@ -369,7 +369,10 @@
* de l'agent commun (URL agent résolue — explicite > -dev >
* défaut prod ; aucun hôte spark-dev inventé). data-app="agent"
* l'intègre à refreshMenuHrefs/applyUrls/setCurrent comme les
* autres items ; l'icône ✦ reprend la couleur agent du CSS. */
* autres items ; son icône CLONE celle de l'onglet agent
* (2026-10-29, logos barre-apps, corrigé le même jour) : le logo
* fourni, tel quel, jamais redessiné — le CSS --agent (rose) des
* autres éléments de la barre n'est pas changé ici. */
var sep = document.createElement("div");
sep.className = "draco-menu-sep";
sep.setAttribute("aria-hidden", "true");
@@ -379,10 +382,18 @@
sparkItem.href = resolvedUrls.agent || "#";
sparkItem.dataset.app = "agent";
sparkItem.setAttribute("role", "menuitem");
var sparkIco = document.createElement("span");
sparkIco.className = "draco-apptab-ico";
sparkIco.setAttribute("style", "--app:#c2437e");
sparkIco.textContent = "✦";
var sparkIco;
var agentTab = document.querySelector('.draco-apptab[data-app="agent"]');
var agentIco = agentTab ? agentTab.querySelector(".draco-apptab-ico") : null;
if (agentIco) {
sparkIco = agentIco.cloneNode(true);
} else {
sparkIco = document.createElement("span");
sparkIco.className = "draco-apptab-ico";
sparkIco.setAttribute("style", "--app:#d21f2c");
sparkIco.setAttribute("data-icon", "agent");
sparkIco.textContent = "✦";
}
sparkItem.appendChild(sparkIco);
var sparkTxt = document.createElement("span");
sparkTxt.className = "draco-apps-item-txt";
+8 -1
View File
@@ -8,7 +8,14 @@
═══════════════════════════════════════════════════════════ */
'use strict';
const CACHE_NAME = 'flowdeck-v8'; // v8 : Alpine CSP build (A20 fini)
const CACHE_NAME = 'flowdeck-v9'; // v9 : purge des caches pre-#33/#36 —
// recherche.js/recherche.css etaient servis cache-first SANS version
// (?v= absent dans recherche.html) : les navigateurs ayant visite
// Flowdeck avant les onglets gardaient les copies pre-#33 (JS sans
// onglets/compteurs/console, CSS sans .rp-tabs) malgre les deplois
// 7.84.3/7.84.4. Le bump purge ces caches a l'activation, et les
// liens versionnes (?v=asset_version) font des futurs changements
// de nouvelles cles de cache.
const DATA_CACHE = 'flowdeck-data-v7';
// App shell (assets versionnés comme référencés dans les templates).
+116
View File
@@ -65,4 +65,120 @@ ok(items[0].context === "Projets/site · En cours", "contexte Kanbante");
ok(rp.nativeSearchUrl("tiktak", "https://tiktak-dev.dracodev.net/", "q") === "https://tiktak-dev.dracodev.net/search?q=q", "repli TikTak");
ok(rp.nativeSearchUrl("obsigate", "https://og-dev.dracodev.net/", "q") === "https://og-dev.dracodev.net/", "repli ObsiGate = accueil (palette)");
/* ---- Chantier 2+3 : onglets, compteurs, URL, console (logique pure) ---- */
/* parseTab */
ok(rp.parseTab("obsigate") === "obsigate", "onglet valide conservé");
ok(rp.parseTab("console") === "console", "onglet console valide");
ok(rp.parseTab("bidon") === "all", "onglet inconnu -> Tout");
ok(rp.parseTab(undefined) === "all", "onglet absent -> Tout");
/* visibleKeys : Tout = les 4 sections groupées, sans la console */
ok(JSON.stringify(rp.visibleKeys("all")) === JSON.stringify(["obsigate", "flowdeck", "tiktak", "kanbante"]), "Tout = 4 sections, pas la console");
ok(JSON.stringify(rp.visibleKeys("tiktak")) === JSON.stringify(["tiktak"]), "onglet app = sa seule section");
ok(JSON.stringify(rp.visibleKeys("console")) === JSON.stringify(["console"]), "onglet console = sa seule section");
ok(JSON.stringify(rp.visibleKeys("bidon")) === JSON.stringify(["obsigate", "flowdeck", "tiktak", "kanbante"]), "onglet inconnu -> Tout");
/* pageItems : troncature Tout (8), complet dans l'onglet de l'app */
let douze = Array.from({ length: 12 }, (_, i) => ({ title: "T" + i }));
ok(rp.pageItems(douze, false).length === 8, "vue Tout tronquée à 8");
ok(rp.pageItems(douze, true).length === 12, "onglet app sans troncature");
/* tabCounts : "…" en chargement, total sinon, "" en erreur (pas de faux 0) */
let c = rp.tabCounts({
obsigate: { status: "loading", total: 0 },
flowdeck: { status: "ok", total: 3 },
tiktak: { status: "empty", total: 0 },
kanbante: { status: "error", total: 0 },
console: { status: "idle", total: 0 },
});
ok(c.obsigate === "…", "compteur … pendant le chargement");
ok(c.flowdeck === "3" && c.tiktak === "0", "compteur total, vide honnête à 0");
ok(c.kanbante === "" && c.console === "", "erreur/inactif : pas de faux 0");
ok(c.all === "…", "Tout = … tant qu'une section charge");
c = rp.tabCounts({
obsigate: { status: "ok", total: 5 },
flowdeck: { status: "ok", total: 2 },
tiktak: { status: "empty", total: 0 },
kanbante: { status: "error", total: 0 },
console: { status: "ok", total: 4 },
});
ok(c.all === "7", "Tout = somme des totaux connus");
ok(c.console === "4", "compteur console");
c = rp.tabCounts({
obsigate: { status: "error", total: 0 },
flowdeck: { status: "error", total: 0 },
tiktak: { status: "error", total: 0 },
kanbante: { status: "error", total: 0 },
console: { status: "idle", total: 0 },
});
ok(c.all === "", "Tout vide si les 4 sections sont en erreur");
/* buildSearchUrl : état onglet partageable */
ok(rp.buildSearchUrl("café", "tiktak") === "?q=caf%C3%A9&tab=tiktak", "URL q+onglet");
ok(rp.buildSearchUrl("x", "all") === "?q=x", "URL Tout sans param tab");
ok(rp.buildSearchUrl("", "all") === "", "URL vide sans recherche ni onglet");
ok(rp.buildSearchUrl("", "console") === "?tab=console", "URL onglet seul");
/* searchPath complet (onglet) vs tronqué (Tout) */
ok(rp.searchPath("obsigate", "café", true) === "api/search?q=caf%C3%A9&vault=all&limit=100", "path ObsiGate complet (100)");
ok(rp.searchPath("obsigate", "café") === "api/search?q=caf%C3%A9&vault=all&limit=8", "path ObsiGate Tout (8)");
/* normalize complet vs tronqué */
let taches12 = Array.from({ length: 12 }, (_, i) => ({ id: i + 1, title: "T" + i, status: "active" }));
ok(rp.normalize("tiktak", { tasks: taches12 }, "https://tiktak-dev.dracodev.net/", "t", true).length === 12, "normalize complet");
ok(rp.normalize("tiktak", { tasks: taches12 }, "https://tiktak-dev.dracodev.net/", "t").length === 8, "normalize tronqué par défaut");
/* normalizeConsole : formes tolérées, pas de faux lien, format inconnu signalé */
let nc = rp.normalizeConsole({ results: [{ title: "D", url: "https://agent.dracodev.net/console/1", snippet: "s" }] }, "https://agent.dracodev.net/");
ok(nc.recognized === true && nc.items[0].url === "https://agent.dracodev.net/console/1", "console : URL absolue conservée");
nc = rp.normalizeConsole({ discussions: [{ name: "Conv", href: "/discussions/9" }] }, "https://agent-dev.dracodev.net/");
ok(nc.recognized === true && nc.items[0].url === "https://agent-dev.dracodev.net/discussions/9", "console : URL relative préfixée");
nc = rp.normalizeConsole({ items: [{ title: "Sans lien", id: 7 }] }, "https://agent.dracodev.net/");
ok(nc.recognized === true && nc.items[0].url === null, "console : sans URL -> pas de faux lien");
nc = rp.normalizeConsole({ autre: [] }, "https://agent.dracodev.net/");
ok(nc.recognized === false, "console : format inconnu -> non reconnu (indisponible, pas vide)");
/* resolveConsoleBase : explicite > agent injecté > dev > href barre > prod */
ok(rp.resolveConsoleBase({ explicit: "https://agent-perso.example/", agentUrl: "https://agent-autre.example/" }) === "https://agent-perso.example/", "console : explicite prioritaire");
ok(rp.resolveConsoleBase({ agentUrl: "https://agent-injecte.example/" }) === "https://agent-injecte.example/", "console : agent injecté par l'hôte");
ok(rp.resolveConsoleBase({ host: "tiktak-dev.dracodev.net" }) === "https://agent-dev.dracodev.net/", "console : dérivation -dev");
ok(rp.resolveConsoleBase({ host: "x.example", tabHrefs: { console: "https://console-perso.example/" } }) === "https://console-perso.example/", "console : href onglet barre");
ok(rp.resolveConsoleBase({}) === "https://agent.dracodev.net/", "console : défaut prod");
/* ---- Alignement console (agent-commun /api/v1/recherche) ---- */
ok(rp.CONSOLE_SEARCH_PATH === "api/v1/recherche", "console : chemin reel /api/v1/recherche");
ok(rp.CONSOLE_SEARCH_LIMIT === 20, "console : limite 20 (plafond par type)");
let rc = rp.normalizeConsole({ resultats: [
{ type: "discussion", id: "c42", titre: "Conv", extrait: "ex", statut: null, domaine: "pro", section: "conversation", objet: "c42", lien: "/?section=conversation&disc=c42" },
{ type: "mission", id: "t1", titre: "Mission", extrait: "ex", statut: "paused_budget", domaine: "pro", section: "missions", objet: "t1", lien: "/?section=missions&objet=t1" },
{ type: "approbation", id: "a9", titre: "publier", extrait: "ex", statut: "pending", domaine: null, section: "approbations", objet: "a9", lien: "/?section=approbations&objet=a9" },
{ type: "objectif", id: "o3", titre: "Obj", extrait: "ex", statut: "actif", domaine: "pro", section: "objectifs", objet: "o3", lien: "/?section=objectifs&objet=o3" },
{ type: "activite", id: "a7", titre: "Act", extrait: "ex", statut: "terminee", domaine: "pro", section: null, objet: "a7", lien: "/?activite=a7" },
{ type: "livrable", id: "l1", titre: "Liv", extrait: "ex", statut: "terminee", domaine: "pro", section: "livrables", objet: null, lien: "/?section=livrables" },
{ type: "planification", id: "p1", titre: "Plan", extrait: "ex", statut: "active", domaine: "pro", section: "planifie", objet: null, lien: "/?section=planifie" },
{ type: "identite", id: "i1", titre: "Id", extrait: "ex", statut: "active", domaine: null, section: "identites", objet: null, lien: "/?section=identites" },
] }, "https://agent-dev.dracodev.net/");
ok(rc.recognized === true && rc.items.length === 8, "console : 8 resultats reconnus");
ok(rc.items[0].url === "https://agent-dev.dracodev.net/?section=conversation&disc=c42", "console : lien profond discussion");
ok(rc.items[1].url === "https://agent-dev.dracodev.net/?section=missions&objet=t1" && rc.items[1].context === "Mission \u00b7 paused_budget \u00b7 pro", "console : lien mission + contexte statut/domaine");
ok(rc.items[2].url === "https://agent-dev.dracodev.net/?section=approbations&objet=a9", "console : lien profond approbation");
ok(rc.items[3].url === "https://agent-dev.dracodev.net/?section=objectifs&objet=o3", "console : lien profond objectif");
ok(rc.items[4].url === "https://agent-dev.dracodev.net/?activite=a7", "console : lien activite sans section");
ok(rc.items[5].url === "https://agent-dev.dracodev.net/?section=livrables", "console : livrable = vue seulement");
ok(rc.items[6].url === "https://agent-dev.dracodev.net/?section=planifie", "console : planification = vue seulement");
ok(rc.items[7].url === "https://agent-dev.dracodev.net/?section=identites", "console : identite = vue seulement");
rc = rp.normalizeConsole({ resultats: [
{ type: "mission", id: "t9", titre: "Sans lien", extrait: "", statut: null, domaine: null, section: "missions", objet: "t9" },
{ type: "activite", id: "a9", titre: "Act sans lien", extrait: "", statut: null, domaine: null, section: null, objet: "a9" },
] }, "https://agent.dracodev.net/");
ok(rc.items[0].url === "https://agent.dracodev.net/?section=missions&objet=t9", "console : lien reconstruit sans lien servi");
ok(rc.items[1].url === "https://agent.dracodev.net/?activite=a9", "console : lien activite reconstruit");
rc = rp.normalizeConsole({ resultats: [] }, "https://agent.dracodev.net/");
ok(rc.recognized === true && rc.items.length === 0, "console : resultats vide reconnu (vide honnete, pas indisponible)");
ok(rp.consoleLinkFor({ type: "discussion", id: "c1" }) === "/?section=conversation&disc=c1", "consoleLinkFor discussion");
ok(rp.resolveConsoleBase({ host: "x.example", tabHrefs: { agent: "https://agent-perso.example/" } }) === "https://agent-perso.example/", "console : href onglet barre agent accepte");
console.log("recherche.test.js : " + n + " assertions OK");
+6
View File
@@ -61,6 +61,12 @@ def test_notifications_api_et_cors_soeur(client):
headers={"Origin": "https://evil.example",
"Access-Control-Request-Method": "GET"})
assert "access-control-allow-origin" not in r4.headers
# Correctif 2026-10-28 : une origine sœur dev passe aussi.
r5 = client.options("/api/notifications",
headers={"Origin": "https://tiktak-dev.dracodev.net",
"Access-Control-Request-Method": "GET"})
assert r5.headers.get("access-control-allow-origin") == "https://tiktak-dev.dracodev.net"
assert r5.headers.get("access-control-allow-credentials") == "true"
def test_chantier5_onglet_console_spark_et_alertes_onglets(client):
+48
View File
@@ -0,0 +1,48 @@
"""Barre d'apps Flowdeck : aucun texte de chantier rendu hors commentaire.
Defaut 5 : les lignes « Chantier 5 (fusion #69/#71, sans bump) : ... »
etaient placees APRES la fermeture `#}` du commentaire Jinja d'en-tete
de `app/templates/_draco_bar.html` (lignes 9-11) : elles etaient donc
rendues telles quelles, juste au-dessus du <header> de la barre, sur
toutes les pages (la barre est incluse par base.html).
Test sans dependance (lecture du gabarit seul, comme
test_draco_alertes.py) : il tourne sans serveur ni base, donc aussi
en sandbox. La note documentaire peut vivre DANS le commentaire Jinja
(jamais rendu) ; elle ne doit jamais en sortir.
"""
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
def _hors_commentaires(texte):
"""Texte du gabarit sans les commentaires Jinja {# ... #}."""
return re.sub(r"\{#.*?#\}", "", texte, flags=re.S)
def test_aucun_texte_chantier_rendu():
gabarit = (ROOT / "app/templates/_draco_bar.html").read_text()
rendu = _hors_commentaires(gabarit)
assert "Chantier 5 (fusion" not in rendu
assert "sans bump" not in rendu
# Aucune ligne de texte libre entre le commentaire d'en-tete et <link> :
# le rendu doit commencer (apres commentaires) par <link> ou <header>.
assert rendu.lstrip().startswith("<link")
def test_barre_intacte_apres_suppression():
"""Le 5e onglet Console Spark et la marque survivent a la suppression."""
gabarit = (ROOT / "app/templates/_draco_bar.html").read_text()
rendu = _hors_commentaires(gabarit)
nav = rendu.split('<nav class="draco-apptabs"')[1].split("</nav>")[0]
assert nav.index('data-app="kanbante"') < nav.index('data-app="agent"')
assert "Console Spark" in nav
assert 'data-app="agent"' in nav
assert 'class="draco-brand"' in rendu
# L'entree « Console Spark » du menu de la marque est construite par
# draco-barre.js depuis les onglets : l'onglet agent suffit a l'alimenter.
js_barre = (ROOT / "static/js/vendor/draco-barre.js").read_text()
assert "Console Spark" in js_barre
+33 -3
View File
@@ -38,25 +38,51 @@ def test_origin_of():
def test_sister_origins_dev():
# Correctif 2026-10-28 : 10 origines officielles (8 sœurs + 2 agent), prod ET dev,
# quelle que soit la résolution.
assert sister_origins(RESOLVED_DEV) == {
"https://og.dracodev.net",
"https://flowdeck.dracodev.net",
"https://tiktak.dracodev.net",
"https://kanbante.dracodev.net",
"https://og-dev.dracodev.net",
"https://flowdeck-dev.dracodev.net",
"https://tiktak-dev.dracodev.net",
"https://kanbante-dev.dracodev.net",
"https://agent.dracodev.net",
"https://agent-dev.dracodev.net",
}
def test_cors_headers_agent_console_autorise():
# Fédération depuis la console (2026-10-28) : les origines agent
# prod ET dev passent, avec credentials ; un tiers reste refusé.
for origine in ("https://agent.dracodev.net",
"https://agent-dev.dracodev.net"):
headers = cors_headers_for(origine, RESOLVED_DEV)
assert headers is not None
assert headers["Access-Control-Allow-Origin"] == origine
assert headers["Access-Control-Allow-Credentials"] == "true"
assert cors_headers_for("https://agent.dracodev.net.evil.example",
RESOLVED_DEV) is None
def test_sister_origins_ajoute_env_de_la_requete():
# Configuration muette (hors requête) + hôte dev : les -dev passent.
# Correctif 2026-10-28 : configuration muette, quel que soit
# l'hôte de la requête, prod ET dev passent — l'hôte ne
# restreint plus l'allowlist.
assert "https://og-dev.dracodev.net" in sister_origins(
{}, request_host="flowdeck-dev.dracodev.net"
)
assert "https://og.dracodev.net" in sister_origins(
{}, request_host="flowdeck.dracodev.net"
)
assert "https://og-dev.dracodev.net" not in sister_origins(
assert "https://og-dev.dracodev.net" in sister_origins(
{}, request_host="flowdeck.dracodev.net"
)
assert "https://og.dracodev.net" in sister_origins(
{}, request_host="flowdeck-dev.dracodev.net"
)
def test_cors_headers():
@@ -64,7 +90,11 @@ def test_cors_headers():
assert headers["Access-Control-Allow-Origin"] == "https://og-dev.dracodev.net"
assert headers["Access-Control-Allow-Credentials"] == "true"
assert cors_headers_for("https://evil.example", RESOLVED_DEV) is None
assert cors_headers_for("https://og.dracodev.net", RESOLVED_DEV) is None
assert cors_headers_for("https://og-dev.dracodev.net.evil.example",
RESOLVED_DEV) is None
# Autre environnement autorisé (correctif 2026-10-28).
assert cors_headers_for("https://og.dracodev.net",
RESOLVED_DEV) is not None
def test_preflight_headers():
+120
View File
@@ -0,0 +1,120 @@
"""Défaut 3b — /api/search Flowdeck depuis la console agent-dev.
Cause prouvée en déployé (flowdeck-dev, 2026-10-28) : sur une réponse
GET /api/search, le CORSMiddleware global pose déjà
``Access-Control-Allow-Credentials: true`` (comportement Starlette
avec ``allow_credentials=True``, même hors allowlist), et l'ancien
middleware de recherche EMPILAIT ses en-têtes par-dessus — d'où, pour
l'origine agent-dev, ``Allow-Credentials`` en double (« true, true »
≠ « true » pour le contrôle CORS du navigateur → fetch rejeté →
« injoignable (CORS ou réseau) » dans la console, alors que TikTak et
Kanbante, sans ce cumul, répondent avec des en-têtes uniques) et,
pour une origine sœur déjà dans la allowlist globale, ``Allow-Origin``
en double (rejet certain). Le correctif : le middleware de recherche
REMPLACE les en-têtes CORS existants au lieu de les empiler (même
patron que app/recherche_cors.py de la console).
Ces tests pilotent le middleware en ASGI pur, avec une application
interne qui imite le CORSMiddleware global (un en-tête
Allow-Credentials déjà posé, et, pour une sœur, Allow-Origin aussi).
``sister_origins`` est figé sur les listes officielles (les fonctions
pures sont déjà couvertes par tests/test_recherche.py) pour garder le
test hermétique (aucune résolution draco_urls, aucune base).
"""
import asyncio
import pytest
from app.middleware import recherche_cors as rc
ORIGINES = rc.PROD_ORIGINS | rc.DEV_ORIGINS | rc.AGENT_ORIGINS
def _lancer(middleware, scope):
"""Exécute le middleware ASGI et retourne les messages envoyés."""
recus = []
async def recevoir():
return {"type": "http.request", "body": b"", "more_body": False}
async def envoyer(message):
recus.append(message)
asyncio.run(middleware(scope, recevoir, envoyer))
return recus
def _application_interne(entetes):
async def appli(scope, recevoir, envoyer):
await envoyer({"type": "http.response.start", "status": 401,
"headers": list(entetes)})
await envoyer({"type": "http.response.body", "body": b"{}"})
return appli
def _scope(methode, origine):
return {"type": "http", "method": methode, "path": "/api/search",
"headers": [(b"origin", origine.encode("latin-1")),
(b"host", b"flowdeck-dev.dracodev.net")]}
def _compter(entetes, nom):
return sum(1 for k, _v in entetes if k == nom.encode("latin-1"))
@pytest.fixture(autouse=True)
def _origines_figees(monkeypatch):
monkeypatch.setattr(rc, "sister_origins", lambda *a, **k: ORIGINES)
def test_get_agent_dev_un_seul_jeu_d_entetes():
"""Cas du défaut : globale (ACAC seul) + recherche → un seul jeu."""
interne = _application_interne([
(b"content-type", b"application/json"),
(b"access-control-allow-credentials", b"true")])
recus = _lancer(rc.RechercheCorsMiddleware(interne),
_scope("GET", "https://agent-dev.dracodev.net"))
entetes = recus[0]["headers"]
assert _compter(entetes, "access-control-allow-credentials") == 1
assert _compter(entetes, "access-control-allow-origin") == 1
assert _compter(entetes, "vary") == 1
assert (b"access-control-allow-origin",
b"https://agent-dev.dracodev.net") in entetes
assert (b"content-type", b"application/json") in entetes
assert recus[0]["status"] == 401 # le statut traverse inchangé
def test_get_origine_soeur_pas_d_allow_origin_double():
"""Sœur déjà dans la allowlist globale : pas de doublon non plus."""
interne = _application_interne([
(b"access-control-allow-origin", b"https://og-dev.dracodev.net"),
(b"access-control-allow-credentials", b"true"),
(b"vary", b"Origin")])
recus = _lancer(rc.RechercheCorsMiddleware(interne),
_scope("GET", "https://og-dev.dracodev.net"))
entetes = recus[0]["headers"]
assert _compter(entetes, "access-control-allow-origin") == 1
assert _compter(entetes, "access-control-allow-credentials") == 1
assert _compter(entetes, "vary") == 1
def test_origine_non_autorisee_traverse_sans_modification():
"""Une origine refusée ne reçoit aucun Allow-Origin ajouté."""
interne = _application_interne([
(b"access-control-allow-credentials", b"true")])
recus = _lancer(rc.RechercheCorsMiddleware(interne),
_scope("GET", "https://example.com"))
entetes = recus[0]["headers"]
assert _compter(entetes, "access-control-allow-origin") == 0
assert entetes == [(b"access-control-allow-credentials", b"true")]
def test_preflight_agent_dev_toujours_unique():
"""Le preflight (court-circuit avant l'interne) reste propre."""
recus = _lancer(rc.RechercheCorsMiddleware(_application_interne([])),
_scope("OPTIONS", "https://agent-dev.dracodev.net"))
entetes = recus[0]["headers"]
assert recus[0]["status"] == 204
assert _compter(entetes, "access-control-allow-origin") == 1
assert _compter(entetes, "access-control-allow-credentials") == 1
+85
View File
@@ -0,0 +1,85 @@
"""Page /recherche Flowdeck : onglets, base console, anti-cache (defaut 6).
Audit prealable (main 7.84.4, tag v7.84.4 = 984e92b) : la route
(app/routers/search.py:51), le gabarit (app/templates/recherche.html),
le JS (static/js/recherche.js) et le CSS (static/css/recherche.css)
contenaient DEJA la page a onglets des PR #33/#36 — aucune route
fantome, aucun bloc `scripts` manquant dans base.html:869, aucune
regle CSP ne bloquant le script (script-src 'self'). Le symptome
deploie (« pas d'onglets, resultats Flowdeck seuls ») ne s'explique
donc PAS par un mauvais gabarit dans main, mais par deux causes
reelles, corrigees ici :
1. Assets recherche servis CACHE-FIRST par le service worker
(static/sw.js) SANS version (?v= absent) sous un CACHE_NAME
inchange depuis A20 ('flowdeck-v8') : un navigateur ayant visite
Flowdeck avant #33 gardait le recherche.js pre-#33 (sans onglets,
sans compteurs, sans logique console, « Tout voir » naviguant
ailleurs) et le recherche.css pre-#33 (sans .rp-tabs) — la page
paraissait donc « non corrigee » malgre les deplois 7.84.3/7.84.4,
et le repli networkFirst du HTML vers le cache couvrait aussi le
cas reseau lent. Correctif : liens versionnes (?v=asset_version,
comme tous les autres assets de base.html) + bump CACHE_NAME
(purge a l'activation du nouveau worker).
2. Flowdeck etait le seul hote a ne pas rendre data-base-console
(son propre commentaire l'admettait) : la base console de l'onglet
Console Spark etait devine (derivation -dev / href d'onglet de
barre) au lieu d'etre explicite. Correctif : la route expose
`bases.agent` et le gabarit le rend en data-base-console.
La logique d'onglets elle-meme (focus, compteurs, « Tout voir » sur
place, etats par application, liens -dev, console sur
GET /api/v1/recherche) est deja couverte par tests/recherche.test.js
(suite Node, lancee en sandbox) ; ce fichier-ci verrouille le
branchement gabarit/route/service worker, sans serveur ni base.
"""
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
ONGLETS_ATTENDUS = ["all", "obsigate", "flowdeck", "tiktak", "kanbante",
"console"]
def test_gabarit_recherche_onglets_complets():
gabarit = (ROOT / "app/templates/recherche.html").read_text()
trouves = re.findall(r'data-rp-tab="([^"]+)"', gabarit)
assert trouves == ONGLETS_ATTENDUS, trouves
for section in ("obsigate", "flowdeck", "tiktak", "kanbante", "console"):
assert f'data-rp-section="{section}"' in gabarit
assert 'id="rp-q"' in gabarit and "autofocus" in gabarit
def test_gabarit_recherche_base_console_explicite():
gabarit = (ROOT / "app/templates/recherche.html").read_text()
assert 'data-base-console="{{ bases.agent }}"' in gabarit
def test_gabarit_recherche_assets_versionnes():
"""Sans ?v=, le SW cache-first ressert les copies pre-#33."""
gabarit = (ROOT / "app/templates/recherche.html").read_text()
assert "/static/css/recherche.css?v={{ asset_version }}" in gabarit
assert "/static/js/recherche.js?v={{ asset_version }}" in gabarit
def test_route_recherche_expose_base_agent():
route = (ROOT / "app/routers/search.py").read_text()
assert '"agent"' in route or "'agent'" in route
assert "bases.agent" in (ROOT / "app/templates/recherche.html").read_text()
def test_service_worker_purge_anciens_caches():
"""Le bump de CACHE_NAME purge les caches pre-#33 a l'activation."""
sw = (ROOT / "static/sw.js").read_text()
assert "const CACHE_NAME = 'flowdeck-v9'" in sw
assert "flowdeck-v8" not in sw
assert "cache-first" in sw or "cacheFirst" in sw
def test_recherche_js_branche_console_v1():
"""Rappel : l'onglet console appelle GET /api/v1/recherche (cf. #36)."""
js = (ROOT / "static/js/recherche.js").read_text()
assert 'CONSOLE_SEARCH_PATH = "api/v1/recherche"' in js
assert "CONSOLE_SEARCH_LIMIT = 20" in js