Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23bcea4b56 | ||
|
|
e7c65b6f49 | ||
|
|
77b4ee29e6 | ||
|
|
95ba369e04 | ||
|
|
a75a477fea | ||
|
|
c8beccaa2e | ||
|
|
08e912d56f | ||
|
|
0464f6156e | ||
|
|
8bba621223 | ||
|
|
da5000b727 | ||
|
|
3539611aa7 | ||
|
|
1f82c2653a | ||
|
|
e24b2a9495 | ||
|
|
73cd12d9de | ||
|
|
d113ce8c5f | ||
|
|
4b1888eebb | ||
|
|
1e733f3662 | ||
|
|
6eccacd982 | ||
|
|
5508d21e0a | ||
|
|
49bc6ca54a | ||
|
|
b58172dcdd | ||
|
|
4fdfc0c385 | ||
|
|
e2886ccc95 | ||
|
|
427df2ca92 | ||
|
|
1a717ede16 | ||
|
|
d3abeb197a | ||
|
|
dee5fb5752 | ||
|
|
9de1166661 | ||
|
|
c0add7b6de | ||
|
|
d29986e51d | ||
|
|
cd8969a3a2 | ||
|
|
e8cbd8e0f3 | ||
|
|
d40044e0c4 | ||
|
|
41865c702a | ||
|
|
56b8ff2072 | ||
|
|
fc5431308c | ||
|
|
7d31af8ec2 | ||
|
|
2e73802f5f | ||
|
|
46b876eea8 | ||
|
|
2431bb6059 | ||
|
|
ac2f0c8cfc | ||
|
|
58ad5d9164 | ||
|
|
91fad4eba3 | ||
|
|
7995dbe9d2 | ||
|
|
0e3e6ec197 | ||
|
|
d6ea617ed7 | ||
|
|
f7c2ea0d34 | ||
|
|
d16666be85 | ||
|
|
fb4ce38403 | ||
|
|
34253c7b3f | ||
|
|
4490a2ed70 | ||
|
|
c9f7754f5f | ||
|
|
43e416a889 | ||
|
|
fefd9b1ad3 | ||
|
|
3b43be3761 | ||
|
|
972d40a75f | ||
|
|
fb942bc871 | ||
|
|
b8ebea1479 | ||
|
|
ee065978b2 | ||
|
|
71c7113a8c | ||
|
|
c0bf0e8523 | ||
|
|
9ae054d1ba | ||
|
|
1e552a55a4 | ||
|
|
fd9df60788 | ||
|
|
984e92b593 | ||
|
|
8ba38a69a4 | ||
|
|
ee477bf2d1 | ||
|
|
ba5f8c8eab | ||
|
|
d4fef9c1bf | ||
|
|
f708fa77e2 | ||
|
|
7711bd3af7 | ||
|
|
b4923493ac | ||
|
|
0ead9fea4a |
@@ -38,6 +38,7 @@ OBSIGATE_CONSUMER_TOKEN=
|
||||
DRACO_AGENT_URL= # Service d'agents Spark — défaut https://agent.dracodev.net ; dev : https://agent-dev.dracodev.net
|
||||
TIKTAK_URL= # TikTak (preset MCP, barre, refs) — défaut https://tiktak.dracodev.net ; dev : https://tiktak-dev.dracodev.net
|
||||
KANBANTE_URL= # Kanbante (barre, refs) — défaut https://kanbante.dracodev.net ; dev : https://kanbante-dev.dracodev.net
|
||||
SHAARIT_URL= # ShaarIt (barre v0.2.0) — défaut https://shaarit.dracodev.net ; dev : https://shaarit-dev.dracodev.net
|
||||
FLOWDECK_URL= # URL publique de CETTE instance — défaut : dérivée d'OAUTH_REDIRECT_URI, sinon https://flowdeck.dracodev.net ; dev : https://flowdeck-dev.dracodev.net
|
||||
# SSO draco (Fusion ACC-3.2) — secret partagé avec l'émettrice ObsiGate.
|
||||
DRACO_SESSION_SECRET=
|
||||
|
||||
@@ -1,3 +1,75 @@
|
||||
## v7.84.14 (2026-11-05) — Barre commune v0.4.0 : icônes monoline du menu compte (HUB-11)
|
||||
|
||||
- **Barre d'apps v0.4.0 vendangée (dracodev-commons, branche `menu-compte-icones-v0-4-0`, PR commons #10)** : `static/css/draco-barre.css` et `static/js/vendor/draco-barre.js` remplacés verbatim par la v0.4.0 ; le fragment `app/templates/_draco_bar.html` est inchangé (aucune clé d'URL nouvelle).
|
||||
- **Menu compte à icônes** : **Télécharger**, **À Propos**, **Aides**, **Profil** et **Déconnexion** reçoivent des **icônes SVG monoline** (contour, trait fin, `currentColor`, ~18 px, couleur atténuée suivant le survol) dans le style des icônes du rail de la console Spark — aucun emoji. L'en-tête d'identité reste sans icône ; les liens Hub et le relais « Déconnexion » sont inchangés.
|
||||
- **Cache** : le bump 7.84.13 → 7.84.14 change aussi `asset_version`, donc les navigateurs ne garderont pas l'ancienne barre.
|
||||
- Tests : `tests/test_draco_bar_texte.py` étendu (marqueurs v0.4.0 du JS et du CSS vendangés).
|
||||
|
||||
## v7.84.13 (2026-11-04) — Barre commune v0.3.0 : liens Hub du menu compte (HUB-1)
|
||||
|
||||
- **Barre d'apps v0.3.0 vendangée (dracodev-commons, branche `hub-menu-compte-v0-3-0`, PR commons #9)** : `static/css/draco-barre.css` et `static/js/vendor/draco-barre.js` remplacés verbatim par la v0.3.0 ; fragment `app/templates/_draco_bar.html` porté (note de vendange + `urls.hub` dans la configuration), le reste de l'intégration Flowdeck inchangé.
|
||||
- **Menu compte** : liens **Télécharger**, **À Propos**, **Aides** et **Profil** (devenu un lien vers `<hub>/profil`, plus un relais) construits par le JS vendangé depuis la base Hub résolue ; « Déconnexion » reste relayée à l'hôte, inchangée.
|
||||
- **URL Hub par le même mécanisme que les autres** : `app/services/draco_urls.py` gagne la clé `hub` (défaut officiel `https://hub.dracodev.net` ; explicite `HUB_URL` / `settings.hub_url` ; dérivation par l'hôte de la requête — motif fermé étendu à `hub` — et par l'hôte de l'agent : `hub-dev` en dev), exposée dans `draco_urls.toutes()` et passée au gabarit en `urls.hub`. Les consommateurs par listes de clés (CORS, CSP, recherche fédérée) ne sont pas affectés : le Hub n'y entre pas.
|
||||
- **Cache** : le bump 7.84.12 → 7.84.13 change aussi `asset_version`, donc les navigateurs ne garderont pas l'ancienne barre.
|
||||
- Tests : `tests/test_draco_urls_devprod.py` (résolution Hub : défaut, dérivations, explicite) et `tests/test_draco_bar_texte.py` (marqueurs v0.3.0 du JS vendangé et du gabarit) étendus.
|
||||
|
||||
## v7.84.11 (2026-11-04) — Recherche fédérée : ShaarIt, 5e source de « Rechercher partout »
|
||||
|
||||
- **`static/js/recherche.js` re-vendangé depuis dracodev-commons v0.2.1** (source canonique `ui/recherche/`, contrat `contrats/recherche-federee.md`) : ShaarIt devient une source de la page `/recherche` comme les autres — bases prod/dev, onglet, normaliseur (lien profond canonique `{base}shaare/<shorturl>`, contexte « Bookmark · privé · tags », repli recherche native `/?q=`). Le fichier reste **strictement identique** chez les cinq hôtes.
|
||||
- **Gabarit et route `/recherche`** : onglet et section « ShaarIt — bookmarks » ; la route expose désormais `bases.shaarit` (la clé existait dans `draco_urls.toutes()` depuis 7.84.10) et le gabarit rend `data-base-shaarit`.
|
||||
- **CORS de recherche** (`app/middleware/recherche_cors.py`) : origines officielles ShaarIt prod et dev ajoutées aux listes explicites, `shaarit` ajoutée aux clés sœurs résolues — `GET /api/search` de Flowdeck devient lisible depuis les pages de ShaarIt, et réciproquement.
|
||||
- Tests : `tests/recherche.test.js` porté à 5 sections (83 assertions, dont le normaliseur ShaarIt) ; `tests/test_recherche_onglets.py` (onglets attendus) et `tests/test_recherche.py` (allowlist à 12 origines) mis à jour. Fonds préexistant inchangé (`test_webhooks_crud`, échec confirmé sur main vierge).
|
||||
|
||||
## v7.84.10 (2026-11-03) — Barre commune v0.2.0 : onglet ShaarIt, garde data-theme
|
||||
|
||||
- **Barre d'apps v0.2.0 vendangée (dracodev-commons @70c837a)** : `static/css/draco-barre.css` et `static/js/vendor/draco-barre.js` remplacés verbatim par la v0.2.0 (hachages SHA-256 vérifiés contre la source) ; fragment `app/templates/_draco_bar.html` porté à la v0.2.0, le reste de son intégration Flowdeck (URL `draco_urls()`, `onAction`, nonce CSP, `asset_version`) est inchangé.
|
||||
- **Onglet ShaarIt** : 5e application (bookmarks, réécriture Rust de Shaarli) — nouvel onglet `data-app="shaarit"` après Kanbante et avant l'onglet Console Spark, logo `static/img/logos/shaarit.svg` ajouté tel quel (copie verbatim de `ui/barre-apps/logos/` de commons). Le menu de la marque, construit depuis les onglets, l'inclut automatiquement.
|
||||
- **URL ShaarIt par le même mécanisme que les autres** : `app/services/draco_urls.py` gagne la clé `shaarit` (défaut officiel `https://shaarit.dracodev.net` ; explicite `SHAARIT_URL` / `settings.shaarit_url` ; dérivation par l'hôte de la requête et par l'hôte de l'agent : `shaarit-dev` en dev) et `draco_urls().shaarit` alimente le href de l'onglet — aucune URL codée en dur hors des défauts officiels du résolveur.
|
||||
- **⚠️ Changement de comportement visible — garde `data-theme`** : le CSS v0.2.0 préfixe ses blocs `prefers-color-scheme` par `html:not([data-theme])`. La copie v0.1.11 n'avait pas cette garde : c'est le **piège connu de Flowdeck** — système en mode sombre + `fd_theme` clair (`data-theme="light"` posé) ⇒ la barre s'affichait quand même en sombre. La re-vendange corrige ce point : la barre suit désormais le thème choisi dans l'app. Signalé en tête de la PR pour arbitrage à la fusion.
|
||||
- **Cache** : le bump 7.84.9 → 7.84.10 change aussi `asset_version`, donc les navigateurs ne garderont pas l'ancienne barre.
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -156,6 +156,8 @@ class Settings(BaseSettings):
|
||||
# DRACO_AGENT_URL=https://agent-dev.dracodev.net
|
||||
# TIKTAK_URL=https://tiktak-dev.dracodev.net
|
||||
# KANBANTE_URL=https://kanbante-dev.dracodev.net
|
||||
# SHAARIT_URL=https://shaarit-dev.dracodev.net
|
||||
# HUB_URL=https://hub-dev.dracodev.net
|
||||
# FLOWDECK_URL=https://flowdeck-dev.dracodev.net
|
||||
draco_sso_url: str = "https://og.dracodev.net" # ObsiGate (émettrice SSO)
|
||||
# ── Coffre ObsiGate, côté consommateur Flowdeck (Phase 4, issue #22) ──
|
||||
@@ -168,6 +170,8 @@ class Settings(BaseSettings):
|
||||
draco_agent_url: str = "https://agent.dracodev.net" # service d'agents (Spark)
|
||||
tiktak_url: str = "https://tiktak.dracodev.net"
|
||||
kanbante_url: str = "https://kanbante.dracodev.net"
|
||||
shaarit_url: str = "https://shaarit.dracodev.net"
|
||||
hub_url: str = "https://hub.dracodev.net"
|
||||
# URL publique de CETTE instance ; vide = dérivée de
|
||||
# OAUTH_REDIRECT_URI, puis d'APP_BASE_URL (si non locale), puis du
|
||||
# nom officiel.
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -9,7 +9,10 @@ Ce module n'autorise donc que :
|
||||
- le chemin ``/api/search`` (jamais les autres routes, jamais les
|
||||
écritures) ;
|
||||
- les origines sœurs : les origines officielles **prod ET dev,
|
||||
listées explicitement** (``PROD_ORIGINS | DEV_ORIGINS``), **plus**
|
||||
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
|
||||
@@ -40,6 +43,7 @@ PROD_ORIGINS = frozenset({
|
||||
"https://flowdeck.dracodev.net",
|
||||
"https://tiktak.dracodev.net",
|
||||
"https://kanbante.dracodev.net",
|
||||
"https://shaarit.dracodev.net",
|
||||
})
|
||||
|
||||
DEV_ORIGINS = frozenset({
|
||||
@@ -47,10 +51,17 @@ DEV_ORIGINS = frozenset({
|
||||
"https://flowdeck-dev.dracodev.net",
|
||||
"https://tiktak-dev.dracodev.net",
|
||||
"https://kanbante-dev.dracodev.net",
|
||||
"https://shaarit-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")
|
||||
_SOEUR_KEYS = ("obsigate", "flowdeck", "tiktak", "kanbante", "shaarit")
|
||||
|
||||
|
||||
def is_dev_host(hostname: str | None) -> bool:
|
||||
@@ -113,7 +124,7 @@ def sister_origins(resolved: dict | None = None, request_host: str | None = None
|
||||
# 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
|
||||
origins |= PROD_ORIGINS | DEV_ORIGINS | AGENT_ORIGINS
|
||||
return frozenset(origins)
|
||||
|
||||
|
||||
@@ -184,7 +195,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()
|
||||
]
|
||||
|
||||
@@ -178,10 +178,17 @@ class ContentSecurityPolicyMiddleware(BaseHTTPMiddleware):
|
||||
# notifications des applications soeurs depuis cet hôte ;
|
||||
# leurs origines rejoignent connect-src (même résolution
|
||||
# draco_urls que les liens de la barre et frame-src).
|
||||
# Fusion #111 : la recherche fédérée appelle en plus
|
||||
# l'API de ShaarIt depuis la page /recherche — sans son
|
||||
# origine ici, le navigateur bloque l'appel et la section
|
||||
# ShaarIt affichait « Injoignable depuis cet hôte (CORS ou
|
||||
# réseau) » alors que le CORS de ShaarIt était correct
|
||||
# (retour d'usage du 2026-11-04).
|
||||
_toutes69 = draco_urls.toutes()
|
||||
sisters = " ".join(sorted({
|
||||
draco_urls.origine(_toutes69[k])
|
||||
for k in ("obsigate", "flowdeck", "tiktak", "kanbante")
|
||||
for k in ("obsigate", "flowdeck", "tiktak", "kanbante",
|
||||
"shaarit")
|
||||
} - {""}))
|
||||
response.headers[self.CSP_HEADER] = self.CSP_VALUE.format(
|
||||
nonce=nonce, host=host, agent=agent, sisters=sisters
|
||||
|
||||
@@ -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", "shaarit", "agent")},
|
||||
))
|
||||
|
||||
@@ -12,6 +12,7 @@ car il est passé par ``environment:``/``env_file:``) :
|
||||
DRACO_AGENT_URL=https://agent-dev.dracodev.net
|
||||
TIKTAK_URL=https://tiktak-dev.dracodev.net
|
||||
KANBANTE_URL=https://kanbante-dev.dracodev.net
|
||||
SHAARIT_URL=https://shaarit-dev.dracodev.net
|
||||
FLOWDECK_URL=https://flowdeck-dev.dracodev.net (ou OAUTH_REDIRECT_URI)
|
||||
|
||||
Priorité de résolution, à chaque appel (les tests et les modules qui
|
||||
@@ -19,15 +20,16 @@ lisaient déjà ``os.environ`` au moment de l'appel gardent ce
|
||||
comportement) : variable d'environnement > ``app.config.settings`` >
|
||||
défaut officiel. Aucun défaut ne contient ``-dev``.
|
||||
|
||||
**Dérivation d'environnement (v7.82.3)** — pour les quatre URL
|
||||
sœurs (ObsiGate, Flowdeck, TikTak, Kanbante), entre la configuration
|
||||
**Dérivation d'environnement (v7.82.3)** — pour les URL
|
||||
sœurs (ObsiGate, Flowdeck, TikTak, Kanbante, et ShaarIt depuis la
|
||||
barre v0.2.0), entre la configuration
|
||||
explicite et le défaut officiel : si l'origine de l'agent telle que
|
||||
l'app la connaît déjà (``AGENT_EVENEMENTS_URL``, via
|
||||
``agent_evenements_url()``) a un hôte de la forme
|
||||
``agent<SUFFIXE>.dracodev.net`` (SUFFIXE éventuellement vide,
|
||||
typiquement ``-dev``), l'URL sœur est dérivée :
|
||||
``https://<slug><SUFFIXE>.dracodev.net`` (slugs : ``og``,
|
||||
``flowdeck``, ``tiktak``, ``kanbante``). Ainsi une installation dev
|
||||
``flowdeck``, ``tiktak``, ``kanbante``, ``shaarit``). Ainsi une installation dev
|
||||
qui ne déclare que l'agent pointe ses liens inter-apps en ``-dev``
|
||||
sans quatre variables de plus. Si l'hôte de l'agent ne correspond
|
||||
pas au motif (autre domaine, LAN, IP), **aucune dérivation** : on
|
||||
@@ -94,7 +96,9 @@ OFFICIELLES = {
|
||||
"agent": "https://agent.dracodev.net",
|
||||
"tiktak": "https://tiktak.dracodev.net",
|
||||
"kanbante": "https://kanbante.dracodev.net",
|
||||
"shaarit": "https://shaarit.dracodev.net",
|
||||
"flowdeck": "https://flowdeck.dracodev.net",
|
||||
"hub": "https://hub.dracodev.net",
|
||||
"gitea": "https://git.dracodev.net",
|
||||
}
|
||||
|
||||
@@ -119,6 +123,8 @@ _SLUGS_SOEURS = {
|
||||
"flowdeck": "flowdeck",
|
||||
"tiktak": "tiktak",
|
||||
"kanbante": "kanbante",
|
||||
"shaarit": "shaarit",
|
||||
"hub": "hub",
|
||||
}
|
||||
|
||||
# Hôte de l'agent : agent<SUFFIXE>.dracodev.net (SUFFIXE = ``-dev``…).
|
||||
@@ -129,7 +135,7 @@ _MOTIF_HOTE_AGENT = re.compile(r"^agent(-[a-z0-9-]+)?\.dracodev\.net$")
|
||||
# de TOUTE l'installation — jamais un suffixe inventé depuis un
|
||||
# hôte arbitraire : hors motif (autre domaine, LAN, IP), None.
|
||||
_MOTIF_HOTE_PUBLIC = re.compile(
|
||||
r"^(og|flowdeck|tiktak|kanbante|agent)(-[a-z0-9-]+)?\.dracodev\.net$")
|
||||
r"^(og|flowdeck|tiktak|kanbante|agent|hub)(-[a-z0-9-]+)?\.dracodev\.net$")
|
||||
|
||||
# Slugs pour la résolution par l'hôte de la requête (l'agent inclus).
|
||||
_SLUGS_TOUTES = {**_SLUGS_SOEURS, "agent": "agent"}
|
||||
@@ -274,6 +280,18 @@ def kanbante_url() -> str:
|
||||
return _resoudre_soeur("KANBANTE_URL", "kanbante_url", "kanbante")
|
||||
|
||||
|
||||
def shaarit_url() -> str:
|
||||
return _resoudre_soeur("SHAARIT_URL", "shaarit_url", "shaarit")
|
||||
|
||||
|
||||
def hub_url() -> str:
|
||||
"""Hub (barre v0.3.0, HUB-1) : même résolution que les sœurs —
|
||||
il n'a pas d'onglet, sa base sert les liens du menu compte
|
||||
(Télécharger, À Propos, Aides, Profil) via ``urls.hub`` de la
|
||||
configuration de la barre."""
|
||||
return _resoudre_soeur("HUB_URL", "hub_url", "hub")
|
||||
|
||||
|
||||
def gitea_url() -> str:
|
||||
return _resoudre("GITEA_URL", "gitea_url", "gitea")
|
||||
|
||||
@@ -356,7 +374,9 @@ def toutes() -> dict[str, str]:
|
||||
"agent": agent_url(),
|
||||
"tiktak": tiktak_url(),
|
||||
"kanbante": kanbante_url(),
|
||||
"shaarit": shaarit_url(),
|
||||
"flowdeck": flowdeck_url(),
|
||||
"hub": hub_url(),
|
||||
"gitea": gitea_url(),
|
||||
}
|
||||
|
||||
|
||||
@@ -1,42 +1,77 @@
|
||||
{# Barre d'apps commune dracodev — Fusion ACC-1.1 (issue #17).
|
||||
Snippet FIGÉ vendangé depuis Projets/dracodev-commons v0.1.6
|
||||
(ui/barre-apps/) : ne pas modifier la structure ici ; toute évolution
|
||||
passe par une nouvelle version de commons.
|
||||
Snippet FIGÉ vendangé depuis Projets/dracodev-commons v0.3.0
|
||||
(ui/barre-apps/, branche hub-menu-compte-v0-3-0 de la PR commons
|
||||
#9 — HUB-1, fusion #115) : ne pas modifier la structure ici ;
|
||||
toute évolution passe par une nouvelle version de commons.
|
||||
v0.3.0 : menu compte — liens Hub Télécharger / À Propos / Aides
|
||||
et Profil devenu lien (base draco_urls().hub passée en urls.hub
|
||||
de la configuration ci-dessous) ; « Déconnexion » reste relayée.
|
||||
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.
|
||||
v0.2.0 : 6e onglet ShaarIt (data-app="shaarit",
|
||||
draco_urls().shaarit, logo static/img/logos/shaarit.svg copie
|
||||
verbatim de commons), après Kanbante et avant l'onglet agent ;
|
||||
le CSS vendangé gagne la GARDE data-theme : ses blocs
|
||||
prefers-color-scheme sont préfixés html:not([data-theme]) —
|
||||
c'est le correctif du piège connu chez Flowdeck (barre sombre
|
||||
forcée quand le système est sombre mais que fd_theme = light) :
|
||||
changement de comportement visible, signalé en tête de la PR. #}
|
||||
<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'application" title="Changer d'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="shaarit" href="{{ draco_urls().shaarit }}/">
|
||||
<span class="draco-apptab-ico" style="--app:#059669" data-icon="shaarit"><img src="/static/img/logos/shaarit.svg" alt="ShaarIt" width="30" height="30"></span>
|
||||
<span class="draco-apptab-txt"><strong>ShaarIt</strong><small>Bookmarks</small></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 +92,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).
|
||||
@@ -77,6 +108,12 @@
|
||||
repli si le module ne charge pas). */
|
||||
window.DRACO_BAR_CONFIG = {
|
||||
current: "flowdeck",
|
||||
/* Barre v0.3.0 (HUB-1) : le Hub n'a pas d'onglet — sa base
|
||||
(draco_urls().hub : HUB_URL explicite, hôte de la requête,
|
||||
dérivation par l'agent, défaut officiel) alimente les
|
||||
liens du menu compte (Télécharger, À Propos, Aides,
|
||||
Profil) via urls.hub. */
|
||||
urls: { hub: "{{ draco_urls().hub }}/" },
|
||||
domain: (function () { try { return localStorage.getItem("draco_domain") || "pro"; } catch (e) { return "pro"; } })(),
|
||||
counts: {},
|
||||
onAction: function (act, el) {
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title_prefix %}Rechercher partout{% endblock %}
|
||||
{% block content %}
|
||||
<link rel="stylesheet" href="/static/css/recherche.css">
|
||||
{# Console : pas de data-base-console ici — la route Flowdeck de /recherche
|
||||
n'expose pas la base agent dans son contexte `bases` ; recherche.js
|
||||
dérive donc la base console (hôte -dev -> agent-dev, sinon prod) ou
|
||||
reprend le href d'un onglet de barre console/spark quand il existe.
|
||||
Les 3 autres hôtes rendent data-base-console explicitement. #}
|
||||
{# 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-shaarit="{{ bases.shaarit }}"
|
||||
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. « Tout voir » bascule vers l'onglet de l'application, sans quitter la page.</p>
|
||||
<p class="rp-hint">Notes ObsiGate, pages Flowdeck, tâches TikTak, cartes Kanbante, bookmarks ShaarIt — 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>
|
||||
@@ -24,6 +30,7 @@
|
||||
<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="shaarit">ShaarIt <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">
|
||||
@@ -42,6 +49,10 @@
|
||||
<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="shaarit">
|
||||
<div class="rp-head"><h2>ShaarIt — bookmarks</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>
|
||||
@@ -49,5 +60,5 @@
|
||||
</main>
|
||||
{% endblock %}
|
||||
{% block scripts %}
|
||||
<script src="/static/js/recherche.js"></script>
|
||||
<script src="/static/js/recherche.js?v={{ asset_version }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -7,14 +7,36 @@
|
||||
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.
|
||||
v0.2.0 : nouvel onglet « ShaarIt » (data-app="shaarit", --app émeraude
|
||||
#059669, logo logos/shaarit.svg via {{LOGO_SHAARIT}}) avant l'onglet
|
||||
agent ; GARDE data-theme : les blocs prefers-color-scheme ne
|
||||
s'appliquent que si <html> n'a AUCUN attribut data-theme
|
||||
(html:not([data-theme])) — avant ce correctif, un hôte ayant choisi
|
||||
le clair via data-theme="light" héritait quand même du sombre
|
||||
système (piège constaté dans les copies vendangées de
|
||||
Kanbante/Flowdeck ; les re-vendanges de la v0.2.0 le corrigent).
|
||||
v0.3.0 (HUB-1) : aucun style nouveau — les liens du Hub du menu
|
||||
compte (Télécharger, À Propos, Aides, Profil) réutilisent
|
||||
.draco-menu-item (déjà ancre-compatible : text-decoration none)
|
||||
et le séparateur .draco-menu-sep du menu des applications.
|
||||
v0.4.0 (HUB-11) : .draco-menu-icone — les icônes monoline des
|
||||
items du menu compte (SVG inline construit par le JS) : 18 px,
|
||||
replacées à gauche du libellé par order: -1 (l'item est en
|
||||
flex), couleur atténuée --ink-2, pleine --ink au survol/focus.
|
||||
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. */
|
||||
ou prefers-color-scheme si l'app intégratrice ne force rien
|
||||
(aucun attribut data-theme sur <html>). */
|
||||
|
||||
.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;
|
||||
@@ -22,8 +44,12 @@
|
||||
background: var(--surface); border-bottom: 1px solid var(--line);
|
||||
font: 500 14px/1.3 system-ui, "Segoe UI", sans-serif; color: var(--ink);
|
||||
}
|
||||
/* Garde data-theme (v0.2.0) : le sombre système ne s'applique QUE si
|
||||
* l'hôte n'a posé aucun data-theme sur <html>. Sans cette garde, un
|
||||
* hôte en data-theme="light" sur un système sombre affichait la barre
|
||||
* sombre malgré son choix explicite. */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.draco-bar { --bg:#101211; --surface:#1a1c1a; --surface-2:#242624; --surface-3:#20221f;
|
||||
html:not([data-theme]) .draco-bar { --bg:#101211; --surface:#1a1c1a; --surface-2:#242624; --surface-3:#20221f;
|
||||
--ink:#eceee9; --ink-2:#a6aba2; --ink-3:#8b9089; --line:#2e302c; }
|
||||
}
|
||||
[data-theme="dark"] .draco-bar { --bg:#101211; --surface:#1a1c1a; --surface-2:#242624; --surface-3:#20221f;
|
||||
@@ -37,7 +63,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) {
|
||||
html:not([data-theme]) .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 {
|
||||
@@ -64,6 +98,15 @@
|
||||
text-decoration: none; cursor: pointer;
|
||||
}
|
||||
.draco-menu-item:hover, .draco-menu-item:focus-visible { background: var(--surface-2); }
|
||||
/* Icônes monoline du menu compte (v0.4.0) : appendues après le
|
||||
libellé dans le DOM, replacées à gauche par order: -1. */
|
||||
.draco-menu-icone {
|
||||
order: -1; flex: none; display: inline-flex; align-items: center;
|
||||
width: 18px; height: 18px; color: var(--ink-2);
|
||||
}
|
||||
.draco-menu-icone svg { display: block; width: 18px; height: 18px; }
|
||||
.draco-menu-item:hover .draco-menu-icone,
|
||||
.draco-menu-item:focus-visible .draco-menu-icone { color: var(--ink); }
|
||||
.draco-menu-sep { height: 1px; margin: 6px 10px; background: var(--line); }
|
||||
.draco-compte-nom {
|
||||
padding: 8px 10px 10px; font-weight: 700; font-size: 14px;
|
||||
@@ -94,6 +137,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 +181,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 +224,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; }
|
||||
@@ -193,6 +248,9 @@
|
||||
.draco-bar[data-current="flowdeck"] .draco-apptab[data-app="flowdeck"],
|
||||
.draco-bar[data-current="tiktak"] .draco-apptab[data-app="tiktak"],
|
||||
.draco-bar[data-current="kanbante"] .draco-apptab[data-app="kanbante"],
|
||||
/* v0.2.0 : sur ShaarIt elle-même (data-current="shaarit"), c'est
|
||||
l'onglet ShaarIt qui reste visible en mobile. */
|
||||
.draco-bar[data-current="shaarit"] .draco-apptab[data-app="shaarit"],
|
||||
/* Chantier 5 : sur la console elle-même (data-current="agent",
|
||||
programme console #71), c'est l'onglet Console Spark qui reste
|
||||
visible en mobile — les 4 hôtes gardent leur propre courant. */
|
||||
|
||||
|
After Width: | Height: | Size: 91 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 40 KiB |
@@ -0,0 +1,9 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 240">
|
||||
<!-- ShaarIt — signet (bookmark) blanc sur fond émeraude #059669,
|
||||
couleur signature de l'app dans la barre (#059669, distincte du
|
||||
teal Kanbante #0d9488). Fond arrondi comme les logos PNG 180 px
|
||||
des autres apps ; géométrie simple, aucun redessin d'un logo
|
||||
existant : le signet EST le symbole de l'app (bookmarks). -->
|
||||
<rect x="8" y="8" width="224" height="224" rx="48" fill="#059669"/>
|
||||
<path d="M 82 52 L 158 52 L 158 190 L 120 152 L 82 190 Z" fill="#ffffff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 569 B |
|
After Width: | Height: | Size: 38 KiB |
@@ -0,0 +1,8 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 240">
|
||||
<!-- TikTak — deux arcs (les deux temps), coche-flèche. Bleu #0076fb, orange #fc750c.
|
||||
Géométrie mesurée sur le visuel validé par Bruno (variante B). -->
|
||||
<path d="M 38.2 101.1 A 84 84 0 0 1 201.8 101.1" fill="none" stroke="#0076fb" stroke-width="28" stroke-linecap="round"/>
|
||||
<path d="M 201.5 140.3 A 84 84 0 0 1 38.5 140.3" fill="none" stroke="#fc750c" stroke-width="28" stroke-linecap="round"/>
|
||||
<polyline points="80,129 106,146 163,84" fill="none" stroke="#0076fb" stroke-width="24" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M 128 80 L 163 84 L 167 122" fill="none" stroke="#0076fb" stroke-width="22" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 760 B |
@@ -2,11 +2,13 @@
|
||||
* 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.
|
||||
* hôte (ObsiGate, Flowdeck, TikTak, Kanbante, ShaarIt) : la configuration
|
||||
* vient des attributs data-* de #recherche, rendus côté serveur par
|
||||
* l'hôte. Source canonique : dracodev-commons, ui/recherche/
|
||||
* (contrat : contrats/recherche-federee.md).
|
||||
*
|
||||
* Présentation en onglets : Tout, ObsiGate, Flowdeck, TikTak, Kanbante,
|
||||
* Console Spark. « Tout » = les 4 sections groupées, chaque liste
|
||||
* ShaarIt, Console Spark. « Tout » = les 5 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
|
||||
@@ -46,23 +48,40 @@
|
||||
* -> { 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 : AUCUN endpoint de recherche mergé au moment de
|
||||
* cette livraison (agent-commun : seule la PR #12 est ouverte, autre
|
||||
* programme — ce dépôt n'est pas touché ici). Le code tente quand
|
||||
* même l'appel conventionnel GET {base-console}/api/search?q= (même
|
||||
* convention que les autres hôtes ; chemin À CONFIRMER avec le
|
||||
* travailleur console dès que son endpoint atterrit) et, en cas
|
||||
* d'échec, de HTTP non-OK ou de format non reconnu, 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" 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é.
|
||||
* - ShaarIt : GET {base}/api/search?q=
|
||||
* -> { links: [{ id, title, url, description, tags, shorturl,
|
||||
* private }] }
|
||||
* Lien profond (contrat liens-profonds) : {base}shaare/<shorturl>.
|
||||
* Recherche native adressable : {base}?q=<q> (page d'accueil).
|
||||
* - 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 >
|
||||
@@ -75,13 +94,16 @@
|
||||
* Authentification cross-origin : les appels sœurs partent avec
|
||||
* 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 #68).
|
||||
* Chaque sœur n'autorise en CORS que les origines sœurs
|
||||
* (GET sur l'endpoint de recherche uniquement, pas de "*").
|
||||
* (Domain=.dracodev.net), accepté par les 5 API de recherche
|
||||
* (ObsiGate et TikTak nativement, Flowdeck et Kanbante via #68,
|
||||
* ShaarIt via la fédération v0.2.1 de commons)
|
||||
* 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.
|
||||
* Le correctif CORS lui-même est chez un autre travailleur.
|
||||
*/
|
||||
(function (global) {
|
||||
"use strict";
|
||||
@@ -95,24 +117,46 @@
|
||||
flowdeck: "https://flowdeck.dracodev.net/",
|
||||
tiktak: "https://tiktak.dracodev.net/",
|
||||
kanbante: "https://kanbante.dracodev.net/",
|
||||
shaarit: "https://shaarit.dracodev.net/",
|
||||
};
|
||||
var DEV = {
|
||||
obsigate: "https://og-dev.dracodev.net/",
|
||||
flowdeck: "https://flowdeck-dev.dracodev.net/",
|
||||
tiktak: "https://tiktak-dev.dracodev.net/",
|
||||
kanbante: "https://kanbante-dev.dracodev.net/",
|
||||
shaarit: "https://shaarit-dev.dracodev.net/",
|
||||
};
|
||||
var PROD_AGENT = "https://agent.dracodev.net/";
|
||||
var DEV_AGENT = "https://agent-dev.dracodev.net/";
|
||||
var CONSOLE_SEARCH_PATH = "api/search";
|
||||
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" },
|
||||
{ key: "shaarit", label: "ShaarIt" },
|
||||
];
|
||||
var CONSOLE_APP = { key: "console", label: "Console Spark" };
|
||||
var TABS = ["all", "obsigate", "flowdeck", "tiktak", "kanbante", "console"];
|
||||
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", "shaarit", "console"];
|
||||
var COLUMN_LABELS = {
|
||||
backlog: "Backlog",
|
||||
in_progress: "En cours",
|
||||
@@ -182,7 +226,8 @@
|
||||
if (!u && isDevHost(opts.host)) u = DEV_AGENT;
|
||||
if (!u)
|
||||
u = cleanUrl(
|
||||
opts.tabHrefs && (opts.tabHrefs.console || opts.tabHrefs.spark)
|
||||
opts.tabHrefs &&
|
||||
(opts.tabHrefs.console || opts.tabHrefs.spark || opts.tabHrefs.agent)
|
||||
);
|
||||
if (!u) u = PROD_AGENT;
|
||||
return withSlash(u);
|
||||
@@ -193,7 +238,7 @@
|
||||
return TABS.indexOf(v) !== -1 ? v : "all";
|
||||
}
|
||||
|
||||
/* Sections visibles pour un onglet. « Tout » = les 4 sections
|
||||
/* Sections visibles pour un onglet. « Tout » = les 5 sections
|
||||
* groupées (vue #68) ; la console n'y figure pas (voir en-tête). */
|
||||
function visibleKeys(tab) {
|
||||
tab = parseTab(tab);
|
||||
@@ -222,7 +267,7 @@
|
||||
* 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. */
|
||||
* connus, "" si les 5 sections sont en erreur/inactives. */
|
||||
function tabCounts(states) {
|
||||
states = states || {};
|
||||
var out = {};
|
||||
@@ -333,22 +378,90 @@
|
||||
};
|
||||
});
|
||||
}
|
||||
if (app === "shaarit") {
|
||||
return cut(data.links || []).map(function (l) {
|
||||
var tags = (l.tags || []).filter(function (t) { return !!t; });
|
||||
var ctx = "Bookmark";
|
||||
if (l.private) ctx += " · privé";
|
||||
if (tags.length) ctx += " · " + tags.join(", ");
|
||||
return {
|
||||
title: l.title || l.url || "Sans titre",
|
||||
snippet: snippetOf(l.description || ""),
|
||||
context: ctx,
|
||||
/* Lien profond canonique (contrats/liens-profonds.md) ;
|
||||
* sans shorturl, repli sur l'URL du bookmark lui-même. */
|
||||
url: l.shorturl ? base + "shaare/" + l.shorturl : l.url || null,
|
||||
};
|
||||
});
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/* Normalisation console, défensive : accepte results/discussions/
|
||||
* items. Un élément sans URL exploitable (http(s), /relatif, #ancre)
|
||||
* garde url=null et sera rendu en <span>, pas en faux lien profond.
|
||||
* Retourne { recognized, items } : recognized=false -> l'appelant
|
||||
* affiche « indisponible », jamais un vide mensonger. */
|
||||
/* 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 items = list.map(function (r) {
|
||||
var legacy = list.map(function (r) {
|
||||
r = r || {};
|
||||
var raw = r.url || r.href || r.link || "";
|
||||
var url = null;
|
||||
@@ -361,7 +474,7 @@
|
||||
url: url,
|
||||
};
|
||||
});
|
||||
return { recognized: true, items: items };
|
||||
return { recognized: true, items: legacy };
|
||||
}
|
||||
|
||||
/* Recherche native de l'app (repli quand la section est en erreur). */
|
||||
@@ -369,13 +482,16 @@
|
||||
if (app === "tiktak" || app === "kanbante") {
|
||||
return base + "search?q=" + encodeURIComponent(q || "");
|
||||
}
|
||||
if (app === "shaarit") {
|
||||
return base + "?q=" + encodeURIComponent(q || "");
|
||||
}
|
||||
/* ObsiGate et Flowdeck n'ont pas de page de recherche adressable :
|
||||
* leur recherche native vit dans la palette (Ctrl+K) de l'app. */
|
||||
return base;
|
||||
}
|
||||
|
||||
function nativeSearchLabel(app) {
|
||||
if (app === "tiktak" || app === "kanbante") {
|
||||
if (app === "tiktak" || app === "kanbante" || app === "shaarit") {
|
||||
return "Ouvrir la recherche dans " + appLabel(app);
|
||||
}
|
||||
return "Ouvrir " + appLabel(app) + " (recherche via la palette Ctrl+K)";
|
||||
@@ -408,6 +524,9 @@
|
||||
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,
|
||||
};
|
||||
@@ -671,12 +790,20 @@
|
||||
};
|
||||
renderSection(sec, states.console, true);
|
||||
renderTabs();
|
||||
var url = consoleBase + CONSOLE_SEARCH_PATH + "?q=" + encodeURIComponent(q);
|
||||
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 = {
|
||||
|
||||
@@ -8,7 +8,34 @@
|
||||
* correctif fusion #70 (SANS bump de version, publication séparée) :
|
||||
* bouton trois points et son menu par défaut SUPPRIMÉS, « Console Spark »
|
||||
* ajoutée au menu de la marque, fermeture/focus corrigés (voir
|
||||
* « Menu des applications », « Focus » et « Écouteurs globaux »).
|
||||
* « Menu des applications », « Focus » et « Écouteurs globaux ») ;
|
||||
* v0.2.0 : 5e application ShaarIt — clé urls.shaarit, défaut officiel
|
||||
* https://shaarit.dracodev.net/, dérivation -dev shaarit-dev (même
|
||||
* contrat que les autres apps), entrée APP_LABELS ; l'onglet
|
||||
* (data-app="shaarit", après Kanbante, avant agent) est repris
|
||||
* automatiquement par la construction du menu de la marque.
|
||||
* v0.3.0 (carte HUB-1, fusion #115) : le Hub, point central de la
|
||||
* suite — nouvelle clé d'URL « hub » (jeton {{URL_HUB}} côté hôte,
|
||||
* DRACO_BAR_CONFIG.urls.hub), défaut officiel
|
||||
* https://hub.dracodev.net/, dérivation -dev hub-dev comme les
|
||||
* sœurs. Le menu compte gagne trois vrais liens (patron
|
||||
* buildAppsMenu : <a href>, classe draco-menu-item) — Télécharger
|
||||
* → <hub>/telecharger, À Propos → <hub>/a-propos, Aides →
|
||||
* <hub>/aide — et « Profil » devient lui aussi un lien, vers
|
||||
* <hub>/profil (il n'est plus relayé aux hôtes : un seul Profil,
|
||||
* celui du Hub). « Déconnexion » reste l'action relayée
|
||||
* (data-compte-act="deconnexion" / onAction), inchangée.
|
||||
* refreshMenuHrefs() resynchronise aussi les liens du Hub à
|
||||
* l'ouverture des menus. Le Hub n'a pas d'onglet dans la barre :
|
||||
* comme spark, sa clé est résolue et exposée via DRACO_BAR.urls,
|
||||
* sans étape « href rendu » (voir « URL » ci-dessous).
|
||||
* v0.4.0 (carte HUB-11, fusion #125) : icônes monoline dans le
|
||||
* menu compte — chaque item (les 4 liens du Hub et « Déconnexion »)
|
||||
* reçoit un glyphe SVG inline dans le style des icônes du rail de
|
||||
* la console (contour, trait fin régulier, currentColor, aucun
|
||||
* remplissage), aligné à gauche du libellé via .draco-menu-icone.
|
||||
* L'en-tête d'identité ne reçoit pas d'icône. Aucun changement de
|
||||
* comportement : liens résolus et Déconnexion relayée inchangés.
|
||||
*
|
||||
* Aucun gestionnaire inline : un seul écouteur délégué sur le document
|
||||
* (patron validé par le prototype — les onclick du HTML généré sont
|
||||
@@ -26,8 +53,10 @@
|
||||
* flowdeck: "https://flowdeck.dracodev.net/",
|
||||
* tiktak: "https://tiktak.dracodev.net/",
|
||||
* kanbante: "https://kanbante.dracodev.net/",
|
||||
* shaarit: "https://shaarit.dracodev.net/",
|
||||
* agent: "https://agent.dracodev.net/",
|
||||
* spark: "https://spark.dracodev.net/"
|
||||
* spark: "https://spark.dracodev.net/",
|
||||
* hub: "https://hub.dracodev.net/"
|
||||
* },
|
||||
* onAction: function (act, el) { … } // l'app relaie les actions
|
||||
* } // qu'elle ne connaît pas
|
||||
@@ -54,10 +83,19 @@
|
||||
* La clé spark ne correspond à aucun onglet de la barre : elle est
|
||||
* résolue et exposée via DRACO_BAR.urls pour le panneau Spark
|
||||
* (panneau-agent), qui suit le même contrat d'injection. La clé agent,
|
||||
* elle, a un onglet depuis le chantier 5 (ordre : ObsiGate, Flowdeck,
|
||||
* TikTak, Kanbante, Console Spark) : elle suit donc l'étape « href
|
||||
* elle, a un onglet depuis le chantier 5 (ordre, v0.2.0 : ObsiGate,
|
||||
* Flowdeck, TikTak, Kanbante, ShaarIt, Console Spark) : elle suit
|
||||
* donc l'étape « href
|
||||
* rendu » comme les autres onglets, en plus d'alimenter le menu de
|
||||
* la marque et DRACO_BAR.urls.
|
||||
* la marque et DRACO_BAR.urls. Même résolution pour shaarit (v0.2.0) :
|
||||
* explicite > dérivation -dev shaarit-dev > href rendu
|
||||
* ({{URL_SHAARIT}}) > défaut officiel.
|
||||
* Cas hub (v0.3.0) : PAS d'onglet, donc pas d'étape « href rendu » —
|
||||
* explicite (DRACO_BAR_CONFIG.urls.hub, alimenté par le jeton
|
||||
* {{URL_HUB}} remplacé côté serveur par l'hôte) > dérivation -dev
|
||||
* hub-dev sur hôte -dev > défaut officiel
|
||||
* https://hub.dracodev.net/. La base résolue sert les quatre liens
|
||||
* du menu compte (telecharger, a-propos, aide, profil).
|
||||
* Actions émises (data-act) : rail, palette, domaine (data-dom),
|
||||
* alertes, spark, rappel-entete. L'app intégratrice décide de chacune
|
||||
* (ouvrir la palette, le panneau Spark, changer de domaine…).
|
||||
@@ -132,6 +170,14 @@
|
||||
* mais le menu affiche l'identité même sans gestionnaire hôte.
|
||||
* Accessibilité : vrais <button>, aria-haspopup / aria-expanded,
|
||||
* role="menu" / "menuitem", fermeture par Échap et clic extérieur.
|
||||
* Menu compte (v0.3.0) — Profil n'est PLUS relayé : c'est un lien
|
||||
* vers <hub>/profil, comme Télécharger / À Propos / Aides (liens
|
||||
* Hub, data-hub-link, href resynchronisés à chaque ouverture via
|
||||
* refreshCompteHrefs / refreshMenuHrefs). Seule « Déconnexion »
|
||||
* reste relayée à l'hôte (data-compte-act="deconnexion"), inchangée.
|
||||
* Les hôtes qui traitaient onAction("profil") (Kanbante → /profile)
|
||||
* voient ce comportement remplacé par le lien Hub — décision du
|
||||
* plan Hub : un seul Profil, celui du Hub.
|
||||
*/
|
||||
(function () {
|
||||
"use strict";
|
||||
@@ -144,8 +190,10 @@
|
||||
flowdeck: "https://flowdeck.dracodev.net/",
|
||||
tiktak: "https://tiktak.dracodev.net/",
|
||||
kanbante: "https://kanbante.dracodev.net/",
|
||||
shaarit: "https://shaarit.dracodev.net/",
|
||||
agent: "https://agent.dracodev.net/",
|
||||
spark: "https://spark.dracodev.net/"
|
||||
spark: "https://spark.dracodev.net/",
|
||||
hub: "https://hub.dracodev.net/"
|
||||
};
|
||||
|
||||
/* Dérivation -dev (v0.1.6) — appliquée UNIQUEMENT quand l'hôte courant
|
||||
@@ -157,12 +205,14 @@
|
||||
flowdeck: "https://flowdeck-dev.dracodev.net/",
|
||||
tiktak: "https://tiktak-dev.dracodev.net/",
|
||||
kanbante: "https://kanbante-dev.dracodev.net/",
|
||||
agent: "https://agent-dev.dracodev.net/"
|
||||
shaarit: "https://shaarit-dev.dracodev.net/",
|
||||
agent: "https://agent-dev.dracodev.net/",
|
||||
hub: "https://hub-dev.dracodev.net/"
|
||||
};
|
||||
|
||||
var APP_LABELS = {
|
||||
obsigate: "ObsiGate", flowdeck: "Flowdeck",
|
||||
tiktak: "TikTak", kanbante: "Kanbante"
|
||||
tiktak: "TikTak", kanbante: "Kanbante", shaarit: "ShaarIt"
|
||||
};
|
||||
|
||||
/* Vrai si hostname est un hôte -dev dracodev.net : première étiquette
|
||||
@@ -296,6 +346,28 @@
|
||||
menu.querySelectorAll(".draco-apps-item").forEach(function (a) {
|
||||
if (a.dataset.app && resolvedUrls[a.dataset.app]) a.setAttribute("href", resolvedUrls[a.dataset.app]);
|
||||
});
|
||||
/* v0.3.0 : les liens du Hub (menu compte) sont resynchronisés
|
||||
* eux aussi à l'ouverture d'un menu, pour refléter setUrls et
|
||||
* la dérivation -dev même s'ils ont été construits avant. */
|
||||
refreshCompteHrefs();
|
||||
}
|
||||
/* Liens du Hub dans le menu compte (v0.3.0) : chemin → libellé.
|
||||
* « Profil » est le quatrième lien (vers <hub>/profil) ; il
|
||||
* remplace l'ancien item relayé « profil ». */
|
||||
var HUB_LINKS = [
|
||||
["telecharger", "Télécharger"],
|
||||
["a-propos", "À Propos"],
|
||||
["aide", "Aides"],
|
||||
["profil", "Profil"]
|
||||
];
|
||||
function hubHref(path) {
|
||||
return withSlash(resolvedUrls.hub || DEFAULT_URLS.hub) + path;
|
||||
}
|
||||
function refreshCompteHrefs() {
|
||||
if (!compteMenu) return;
|
||||
compteMenu.querySelectorAll("[data-hub-link]").forEach(function (a) {
|
||||
a.setAttribute("href", hubHref(a.getAttribute("data-hub-link")));
|
||||
});
|
||||
}
|
||||
function togglePopup(menu, btn, force) {
|
||||
if (!menu) return;
|
||||
@@ -365,11 +437,15 @@
|
||||
if (badge) a.appendChild(badge.cloneNode(true));
|
||||
appsMenu.appendChild(a);
|
||||
});
|
||||
/* Entrée « Console Spark » (#70) : après Kanbante, vers la console
|
||||
/* Entrée « Console Spark » (#70) : après les onglets d'apps
|
||||
* (ObsiGate…ShaarIt, v0.2.0), vers la console
|
||||
* 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 +455,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";
|
||||
@@ -406,10 +490,74 @@
|
||||
if (!appsMenu.hidden) lastOpener = brand;
|
||||
}
|
||||
|
||||
/* Menu compte (v0.1.6) — en-tête d'identité + Profil / Déconnexion
|
||||
* relayés à l'hôte. L'en-tête est rafraîchi à chaque ouverture, pour
|
||||
* refléter un title posé après coup par l'hôte (ex. ObsiGate, qui
|
||||
* remplit l'avatar depuis /api/auth/me après chargement). */
|
||||
/* Icônes monoline du menu compte (v0.4.0) — glyphes contour
|
||||
* dans le style des icônes du rail de la console : viewBox 24,
|
||||
* stroke currentColor, remplissage nul, extrémités rondes.
|
||||
* Chaque glyphe est une liste de primitives [balise, attributs]
|
||||
* construites en DOM (jamais d'innerHTML), comme le reste du
|
||||
* fichier. Le libellé reste le nœud texte de l'item ; l'icône,
|
||||
* appendue après, est replacée à gauche par le CSS (order: -1
|
||||
* sur .draco-menu-icone, l'item étant déjà en flex). */
|
||||
var MENU_ICONES = {
|
||||
/* Flèche vers le bas dans un plateau. */
|
||||
"telecharger": [
|
||||
["path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }],
|
||||
["polyline", { points: "7 10 12 15 17 10" }],
|
||||
["line", { x1: "12", y1: "15", x2: "12", y2: "3" }]
|
||||
],
|
||||
/* Cercle d'information. */
|
||||
"a-propos": [
|
||||
["circle", { cx: "12", cy: "12", r: "9" }],
|
||||
["line", { x1: "12", y1: "11", x2: "12", y2: "16" }],
|
||||
["line", { x1: "12", y1: "8", x2: "12.01", y2: "8" }]
|
||||
],
|
||||
/* Cercle d'interrogation. */
|
||||
"aide": [
|
||||
["circle", { cx: "12", cy: "12", r: "9" }],
|
||||
["path", { d: "M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" }],
|
||||
["line", { x1: "12", y1: "17", x2: "12.01", y2: "17" }]
|
||||
],
|
||||
/* Personne : tête + épaules. */
|
||||
"profil": [
|
||||
["path", { d: "M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" }],
|
||||
["circle", { cx: "12", cy: "7", r: "4" }]
|
||||
],
|
||||
/* Flèche sortant d'un cadre (porte). */
|
||||
"deconnexion": [
|
||||
["path", { d: "M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" }],
|
||||
["polyline", { points: "16 17 21 12 16 7" }],
|
||||
["line", { x1: "21", y1: "12", x2: "9", y2: "12" }]
|
||||
]
|
||||
};
|
||||
function menuIcone(nom) {
|
||||
var span = document.createElement("span");
|
||||
span.className = "draco-menu-icone";
|
||||
span.setAttribute("aria-hidden", "true");
|
||||
var svg = document.createElement("svg");
|
||||
svg.setAttribute("viewBox", "0 0 24 24");
|
||||
svg.setAttribute("fill", "none");
|
||||
svg.setAttribute("stroke", "currentColor");
|
||||
svg.setAttribute("stroke-width", "1.8");
|
||||
svg.setAttribute("stroke-linecap", "round");
|
||||
svg.setAttribute("stroke-linejoin", "round");
|
||||
svg.setAttribute("focusable", "false");
|
||||
(MENU_ICONES[nom] || []).forEach(function (spec) {
|
||||
var el = document.createElement(spec[0]);
|
||||
for (var k in spec[1]) el.setAttribute(k, spec[1][k]);
|
||||
svg.appendChild(el);
|
||||
});
|
||||
span.appendChild(svg);
|
||||
return span;
|
||||
}
|
||||
|
||||
/* Menu compte (v0.1.6 ; v0.3.0 pour les liens) — en-tête
|
||||
* d'identité, puis les liens du Hub (Télécharger, À Propos,
|
||||
* Aides, Profil — vrais <a href>, même patron que le menu des
|
||||
* applications), un séparateur, puis « Déconnexion » relayée à
|
||||
* l'hôte. L'en-tête et les href sont rafraîchis à chaque
|
||||
* ouverture, pour refléter un title posé après coup par l'hôte
|
||||
* (ex. ObsiGate, qui remplit l'avatar depuis /api/auth/me après
|
||||
* chargement) et la résolution d'URL la plus récente. */
|
||||
function buildCompteMenu() {
|
||||
if (!bar || !avatarBtn || compteMenu) return;
|
||||
compteMenu = document.createElement("div");
|
||||
@@ -420,15 +568,28 @@
|
||||
var head = document.createElement("div");
|
||||
head.className = "draco-compte-nom";
|
||||
compteMenu.appendChild(head);
|
||||
[["profil", "Profil"], ["deconnexion", "Déconnexion"]].forEach(function (pair) {
|
||||
var b = document.createElement("button");
|
||||
b.type = "button";
|
||||
b.className = "draco-menu-item";
|
||||
b.setAttribute("data-compte-act", pair[0]);
|
||||
b.setAttribute("role", "menuitem");
|
||||
b.textContent = pair[1];
|
||||
compteMenu.appendChild(b);
|
||||
HUB_LINKS.forEach(function (pair) {
|
||||
var a = document.createElement("a");
|
||||
a.className = "draco-menu-item";
|
||||
a.href = hubHref(pair[0]);
|
||||
a.setAttribute("data-hub-link", pair[0]);
|
||||
a.setAttribute("role", "menuitem");
|
||||
a.textContent = pair[1];
|
||||
a.appendChild(menuIcone(pair[0]));
|
||||
compteMenu.appendChild(a);
|
||||
});
|
||||
var sep = document.createElement("div");
|
||||
sep.className = "draco-menu-sep";
|
||||
sep.setAttribute("aria-hidden", "true");
|
||||
compteMenu.appendChild(sep);
|
||||
var b = document.createElement("button");
|
||||
b.type = "button";
|
||||
b.className = "draco-menu-item";
|
||||
b.setAttribute("data-compte-act", "deconnexion");
|
||||
b.setAttribute("role", "menuitem");
|
||||
b.textContent = "Déconnexion";
|
||||
b.appendChild(menuIcone("deconnexion"));
|
||||
compteMenu.appendChild(b);
|
||||
bar.appendChild(compteMenu);
|
||||
}
|
||||
function refreshCompteMenu() {
|
||||
@@ -436,6 +597,7 @@
|
||||
var head = compteMenu.querySelector(".draco-compte-nom");
|
||||
var name = displayName();
|
||||
if (head) head.textContent = name || "Compte";
|
||||
refreshCompteHrefs();
|
||||
}
|
||||
function toggleCompteMenu(force) {
|
||||
buildCompteMenu();
|
||||
@@ -532,6 +694,7 @@
|
||||
resolvedUrls = resolveUrls();
|
||||
window.DRACO_BAR.urls = resolvedUrls;
|
||||
applyUrls(resolvedUrls);
|
||||
refreshCompteHrefs();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -61,8 +61,18 @@ items = rp.normalize("kanbante", { groups: [{ full_name: "Projets/site", rows: [
|
||||
ok(items[0].title === "#12 Bug" && items[0].url === "https://kanbante-dev.dracodev.net/board/Projets/site", "lien tableau Kanbante");
|
||||
ok(items[0].context === "Projets/site · En cours", "contexte Kanbante");
|
||||
|
||||
/* normalize ShaarIt (commons v0.2.1) : lien profond shaare/<shorturl>,
|
||||
* contexte privé + tags, repli sur l'URL du bookmark sans shorturl */
|
||||
items = rp.normalize("shaarit", { links: [{ title: "Yandex", url: "https://yandex.com", description: "moteur", tags: ["recherche", "web"], shorturl: "xYz9", private: true }] }, "https://shaarit-dev.dracodev.net/", "recherche");
|
||||
ok(items[0].title === "Yandex" && items[0].url === "https://shaarit-dev.dracodev.net/shaare/xYz9", "lien profond ShaarIt (shaare/<shorturl>)");
|
||||
ok(items[0].context === "Bookmark · privé · recherche, web", "contexte ShaarIt (privé + tags)");
|
||||
ok(items[0].snippet === "moteur", "extrait ShaarIt = description");
|
||||
items = rp.normalize("shaarit", { links: [{ title: "", url: "https://exemple.fr", tags: [], shorturl: "" }] }, "https://shaarit.dracodev.net/", "x");
|
||||
ok(items[0].url === "https://exemple.fr" && items[0].title === "https://exemple.fr", "ShaarIt sans shorturl : repli URL + titre");
|
||||
|
||||
/* nativeSearchUrl */
|
||||
ok(rp.nativeSearchUrl("tiktak", "https://tiktak-dev.dracodev.net/", "q") === "https://tiktak-dev.dracodev.net/search?q=q", "repli TikTak");
|
||||
ok(rp.nativeSearchUrl("shaarit", "https://shaarit-dev.dracodev.net/", "q") === "https://shaarit-dev.dracodev.net/?q=q", "repli ShaarIt = accueil ?q=");
|
||||
ok(rp.nativeSearchUrl("obsigate", "https://og-dev.dracodev.net/", "q") === "https://og-dev.dracodev.net/", "repli ObsiGate = accueil (palette)");
|
||||
|
||||
|
||||
@@ -70,15 +80,17 @@ ok(rp.nativeSearchUrl("obsigate", "https://og-dev.dracodev.net/", "q") === "http
|
||||
|
||||
/* parseTab */
|
||||
ok(rp.parseTab("obsigate") === "obsigate", "onglet valide conservé");
|
||||
ok(rp.parseTab("shaarit") === "shaarit", "onglet shaarit valide");
|
||||
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");
|
||||
/* visibleKeys : Tout = les 5 sections groupées, sans la console */
|
||||
ok(JSON.stringify(rp.visibleKeys("all")) === JSON.stringify(["obsigate", "flowdeck", "tiktak", "kanbante", "shaarit"]), "Tout = 5 sections, pas la console");
|
||||
ok(JSON.stringify(rp.visibleKeys("tiktak")) === JSON.stringify(["tiktak"]), "onglet app = sa seule section");
|
||||
ok(JSON.stringify(rp.visibleKeys("shaarit")) === JSON.stringify(["shaarit"]), "onglet shaarit = 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");
|
||||
ok(JSON.stringify(rp.visibleKeys("bidon")) === JSON.stringify(["obsigate", "flowdeck", "tiktak", "kanbante", "shaarit"]), "onglet inconnu -> Tout");
|
||||
|
||||
/* pageItems : troncature Tout (8), complet dans l'onglet de l'app */
|
||||
let douze = Array.from({ length: 12 }, (_, i) => ({ title: "T" + i }));
|
||||
@@ -91,9 +103,11 @@ let c = rp.tabCounts({
|
||||
flowdeck: { status: "ok", total: 3 },
|
||||
tiktak: { status: "empty", total: 0 },
|
||||
kanbante: { status: "error", total: 0 },
|
||||
shaarit: { status: "ok", total: 2 },
|
||||
console: { status: "idle", total: 0 },
|
||||
});
|
||||
ok(c.obsigate === "…", "compteur … pendant le chargement");
|
||||
ok(c.shaarit === "2", "compteur shaarit");
|
||||
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");
|
||||
@@ -102,18 +116,20 @@ c = rp.tabCounts({
|
||||
flowdeck: { status: "ok", total: 2 },
|
||||
tiktak: { status: "empty", total: 0 },
|
||||
kanbante: { status: "error", total: 0 },
|
||||
shaarit: { status: "ok", total: 1 },
|
||||
console: { status: "ok", total: 4 },
|
||||
});
|
||||
ok(c.all === "7", "Tout = somme des totaux connus");
|
||||
ok(c.all === "8", "Tout = somme des totaux connus (5 sections)");
|
||||
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 },
|
||||
shaarit: { status: "error", total: 0 },
|
||||
console: { status: "idle", total: 0 },
|
||||
});
|
||||
ok(c.all === "", "Tout vide si les 4 sections sont en erreur");
|
||||
ok(c.all === "", "Tout vide si les 5 sections sont en erreur");
|
||||
|
||||
/* buildSearchUrl : état onglet partageable */
|
||||
ok(rp.buildSearchUrl("café", "tiktak") === "?q=caf%C3%A9&tab=tiktak", "URL q+onglet");
|
||||
@@ -147,4 +163,38 @@ ok(rp.resolveConsoleBase({ host: "tiktak-dev.dracodev.net" }) === "https://agent
|
||||
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");
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
"""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_v020_onglet_shaarit_et_garde_data_theme():
|
||||
"""Barre v0.2.0 (commons @70c837a) : onglet ShaarIt entre Kanbante
|
||||
et l'onglet agent, logo fourni tel quel, CSS vendangé verbatim
|
||||
avec la garde data-theme sur prefers-color-scheme."""
|
||||
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="shaarit"')
|
||||
assert nav.index('data-app="shaarit"') < nav.index('data-app="agent"')
|
||||
assert 'href="{{ draco_urls().shaarit }}/"' in nav
|
||||
assert 'data-icon="shaarit"><img src="/static/img/logos/shaarit.svg"' in nav
|
||||
assert (ROOT / "static/img/logos/shaarit.svg").is_file()
|
||||
css = (ROOT / "static/css/draco-barre.css").read_text()
|
||||
assert "html:not([data-theme]) .draco-bar {" in css
|
||||
assert "html:not([data-theme]) .draco-bar .draco-brand-mark img" in css
|
||||
js = (ROOT / "static/js/vendor/draco-barre.js").read_text()
|
||||
assert 'shaarit: "https://shaarit.dracodev.net/"' in js
|
||||
assert 'shaarit: "https://shaarit-dev.dracodev.net/"' in js
|
||||
|
||||
|
||||
def test_barre_v030_liens_hub_menu_compte():
|
||||
"""Barre v0.3.0 (commons, HUB-1 / fusion #115) : JS vendangé avec
|
||||
la clé hub (défaut + dérivation hub-dev) et les liens du menu
|
||||
compte ; le gabarit passe draco_urls().hub en urls.hub."""
|
||||
js = (ROOT / "static/js/vendor/draco-barre.js").read_text()
|
||||
assert 'hub: "https://hub.dracodev.net/"' in js
|
||||
assert 'hub: "https://hub-dev.dracodev.net/"' in js
|
||||
assert '"telecharger", "Télécharger"' in js
|
||||
assert '"a-propos", "À Propos"' in js
|
||||
assert '"aide", "Aides"' in js
|
||||
assert '"profil", "Profil"' in js
|
||||
assert "data-hub-link" in js
|
||||
assert 'data-compte-act", "profil"' not in js
|
||||
assert 'data-compte-act", "deconnexion"' in js
|
||||
gabarit = (ROOT / "app/templates/_draco_bar.html").read_text()
|
||||
rendu = _hors_commentaires(gabarit)
|
||||
assert 'urls: { hub: "{{ draco_urls().hub }}/" }' in rendu
|
||||
|
||||
|
||||
def test_barre_v040_icones_menu_compte():
|
||||
"""Barre v0.4.0 (commons, HUB-11 / fusion #125) : les items du
|
||||
menu compte portent des icônes SVG monoline (construites en DOM,
|
||||
currentColor, sans remplissage) ; le libellé reste un nœud
|
||||
texte ; l'en-tête d'identité reste sans icône."""
|
||||
js = (ROOT / "static/js/vendor/draco-barre.js").read_text()
|
||||
assert "MENU_ICONES" in js
|
||||
for nom in ("telecharger", "a-propos", "aide", "profil", "deconnexion"):
|
||||
assert f'"{nom}"' in js
|
||||
assert "draco-menu-icone" in js
|
||||
assert 'setAttribute("stroke", "currentColor")' in js
|
||||
assert 'setAttribute("fill", "none")' in js
|
||||
css = (ROOT / "static/css/draco-barre.css").read_text()
|
||||
assert ".draco-menu-icone" in css
|
||||
assert "width: 18px" in css
|
||||
|
||||
|
||||
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
|
||||
@@ -12,7 +12,8 @@ import pytest
|
||||
from app.services import draco_urls
|
||||
|
||||
VARS = ("DRACO_SSO_URL", "DRACO_AGENT_URL", "TIKTAK_URL", "KANBANTE_URL",
|
||||
"FLOWDECK_URL", "OAUTH_REDIRECT_URI", "AGENT_EVENEMENTS_URL")
|
||||
"SHAARIT_URL", "HUB_URL", "FLOWDECK_URL", "OAUTH_REDIRECT_URI",
|
||||
"AGENT_EVENEMENTS_URL")
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
@@ -45,6 +46,41 @@ def test_surcharge_dev_par_env(monkeypatch):
|
||||
assert urls["flowdeck"] == "https://flowdeck-dev.dracodev.net"
|
||||
|
||||
|
||||
def test_shaarit_defaut_derivation_et_explicite(monkeypatch, avec_hote):
|
||||
# Barre v0.2.0 : ShaarIt suit le même mécanisme que les autres
|
||||
# sœurs — défaut officiel, dérivation -dev (agent connu puis
|
||||
# hôte de la requête), explicite prioritaire.
|
||||
assert draco_urls.shaarit_url() == "https://shaarit.dracodev.net"
|
||||
assert draco_urls.toutes()["shaarit"] == "https://shaarit.dracodev.net"
|
||||
monkeypatch.setenv(
|
||||
"AGENT_EVENEMENTS_URL",
|
||||
"https://agent-dev.dracodev.net/api/v1/evenements")
|
||||
assert draco_urls.shaarit_url() == "https://shaarit-dev.dracodev.net"
|
||||
monkeypatch.delenv("AGENT_EVENEMENTS_URL")
|
||||
avec_hote("flowdeck-dev.dracodev.net")
|
||||
assert draco_urls.shaarit_url() == "https://shaarit-dev.dracodev.net"
|
||||
monkeypatch.setenv("SHAARIT_URL", "https://bm.example.net/")
|
||||
assert draco_urls.shaarit_url() == "https://bm.example.net"
|
||||
|
||||
|
||||
def test_hub_defaut_derivation_et_explicite(monkeypatch, avec_hote):
|
||||
# Barre v0.3.0 (HUB-1) : le Hub suit le même mécanisme que les
|
||||
# autres sœurs — défaut officiel, dérivation -dev (agent connu
|
||||
# puis hôte de la requête), explicite prioritaire. Pas d'onglet :
|
||||
# sa base sert les liens du menu compte via urls.hub.
|
||||
assert draco_urls.hub_url() == "https://hub.dracodev.net"
|
||||
assert draco_urls.toutes()["hub"] == "https://hub.dracodev.net"
|
||||
monkeypatch.setenv(
|
||||
"AGENT_EVENEMENTS_URL",
|
||||
"https://agent-dev.dracodev.net/api/v1/evenements")
|
||||
assert draco_urls.hub_url() == "https://hub-dev.dracodev.net"
|
||||
monkeypatch.delenv("AGENT_EVENEMENTS_URL")
|
||||
avec_hote("flowdeck-dev.dracodev.net")
|
||||
assert draco_urls.hub_url() == "https://hub-dev.dracodev.net"
|
||||
monkeypatch.setenv("HUB_URL", "https://hub.example.net/")
|
||||
assert draco_urls.hub_url() == "https://hub.example.net"
|
||||
|
||||
|
||||
def test_flowdeck_url_derivée_oauth_redirect(monkeypatch):
|
||||
monkeypatch.setenv(
|
||||
"OAUTH_REDIRECT_URI", "https://flowdeck-dev.dracodev.net/auth/callback")
|
||||
|
||||
@@ -38,20 +38,38 @@ def test_origin_of():
|
||||
|
||||
|
||||
def test_sister_origins_dev():
|
||||
# Correctif 2026-10-28 : 8 origines officielles, prod ET dev,
|
||||
# quelle que soit la résolution.
|
||||
# Correctif 2026-10-28 : origines officielles prod ET dev, quelle
|
||||
# que soit la résolution ; ShaarIt ajoutée par la fédération
|
||||
# v0.2.1 (12 origines : 10 sœurs + 2 agent).
|
||||
assert sister_origins(RESOLVED_DEV) == {
|
||||
"https://og.dracodev.net",
|
||||
"https://flowdeck.dracodev.net",
|
||||
"https://tiktak.dracodev.net",
|
||||
"https://kanbante.dracodev.net",
|
||||
"https://shaarit.dracodev.net",
|
||||
"https://og-dev.dracodev.net",
|
||||
"https://flowdeck-dev.dracodev.net",
|
||||
"https://tiktak-dev.dracodev.net",
|
||||
"https://kanbante-dev.dracodev.net",
|
||||
"https://shaarit-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():
|
||||
# Correctif 2026-10-28 : configuration muette, quel que soit
|
||||
# l'hôte de la requête, prod ET dev passent — l'hôte ne
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,17 @@
|
||||
"""Recherche fédérée « Rechercher partout » (fusion #111) — garde-fous
|
||||
côté Flowdeck : l'API /api/search et les origines sœurs sont couvertes
|
||||
par les suites Node et les tests CORS ; ici, la CSP des pages, qui
|
||||
conditionne les appels cross-origin de la page /recherche."""
|
||||
|
||||
|
||||
def test_csp_connect_src_contient_shaarit(client):
|
||||
"""La CSP des pages doit autoriser l'origine de ShaarIt en
|
||||
connect-src, sinon le navigateur bloque l'appel de la page
|
||||
/recherche avant même le CORS et la section ShaarIt affiche
|
||||
« Injoignable depuis cet hôte » (retour d'usage du 2026-11-04 :
|
||||
la liste des sœurs du connect-src, écrite pour les Alertes
|
||||
#69, n'avait pas été étendue à ShaarIt)."""
|
||||
r = client.get("/")
|
||||
csp = r.headers.get("content-security-policy", "")
|
||||
assert "connect-src" in csp
|
||||
assert "https://shaarit.dracodev.net" in csp, csp
|
||||
@@ -0,0 +1,86 @@
|
||||
"""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",
|
||||
"shaarit", "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", "shaarit",
|
||||
"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
|
||||