Compare commits
29
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -1,3 +1,19 @@
|
|||||||
|
## 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
|
## 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.
|
- **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.
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ Ce module n'autorise donc que :
|
|||||||
- le chemin ``/api/search`` (jamais les autres routes, jamais les
|
- le chemin ``/api/search`` (jamais les autres routes, jamais les
|
||||||
écritures) ;
|
écritures) ;
|
||||||
- les origines sœurs : les origines officielles **prod ET dev,
|
- 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()``
|
celles résolues par ``app.services.draco_urls.toutes()``
|
||||||
(explicite > hôte > dérivation > défaut officiel) — jamais ``*``.
|
(explicite > hôte > dérivation > défaut officiel) — jamais ``*``.
|
||||||
Correctif 2026-10-28 (fusion #68/#69, matrice constatée par
|
Correctif 2026-10-28 (fusion #68/#69, matrice constatée par
|
||||||
@@ -49,6 +52,12 @@ DEV_ORIGINS = frozenset({
|
|||||||
"https://kanbante-dev.dracodev.net",
|
"https://kanbante-dev.dracodev.net",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
AGENT_ORIGINS = frozenset({
|
||||||
|
"https://agent.dracodev.net",
|
||||||
|
"https://agent-dev.dracodev.net",
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
# Clés de draco_urls.toutes() qui sont des applications sœurs.
|
# Clés de draco_urls.toutes() qui sont des applications sœurs.
|
||||||
_SOEUR_KEYS = ("obsigate", "flowdeck", "tiktak", "kanbante")
|
_SOEUR_KEYS = ("obsigate", "flowdeck", "tiktak", "kanbante")
|
||||||
|
|
||||||
@@ -113,7 +122,7 @@ def sister_origins(resolved: dict | None = None, request_host: str | None = None
|
|||||||
# l'environnement de la requête, désormais inclus d'office).
|
# l'environnement de la requête, désormais inclus d'office).
|
||||||
# ``resolved`` n'ajoute que des origines de configuration sans
|
# ``resolved`` n'ajoute que des origines de configuration sans
|
||||||
# jamais retirer une origine officielle.
|
# jamais retirer une origine officielle.
|
||||||
origins |= PROD_ORIGINS | DEV_ORIGINS
|
origins |= PROD_ORIGINS | DEV_ORIGINS | AGENT_ORIGINS
|
||||||
return frozenset(origins)
|
return frozenset(origins)
|
||||||
|
|
||||||
|
|
||||||
@@ -184,7 +193,32 @@ class RechercheCorsMiddleware:
|
|||||||
if message["type"] == "http.response.start":
|
if message["type"] == "http.response.start":
|
||||||
cors = cors_headers_for(origin, request_host=request_host)
|
cors = cors_headers_for(origin, request_host=request_host)
|
||||||
if cors is not None:
|
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"))
|
(k.lower().encode("latin-1"), v.encode("latin-1"))
|
||||||
for k, v in cors.items()
|
for k, v in cors.items()
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -63,7 +63,12 @@ def recherche_page(request: Request, q: str = Query(default="")):
|
|||||||
user = SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
|
user = SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
|
||||||
bases = draco_urls.toutes()
|
bases = draco_urls.toutes()
|
||||||
template = ENV.get_template("recherche.html")
|
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(
|
return HTMLResponse(template.render(
|
||||||
request=request, q=q.strip(), user=user,
|
request=request, q=q.strip(), user=user,
|
||||||
bases={k: bases[k] for k in ("obsigate", "flowdeck", "tiktak", "kanbante")},
|
bases={k: bases[k] for k in ("obsigate", "flowdeck", "tiktak",
|
||||||
|
"kanbante", "agent")},
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -1,14 +1,19 @@
|
|||||||
{# Barre d'apps commune dracodev — Fusion ACC-1.1 (issue #17).
|
{# Barre d'apps commune dracodev — Fusion ACC-1.1 (issue #17).
|
||||||
Snippet FIGÉ vendangé depuis Projets/dracodev-commons v0.1.6
|
Snippet FIGÉ vendangé depuis Projets/dracodev-commons v0.1.9
|
||||||
(ui/barre-apps/) : ne pas modifier la structure ici ; toute évolution
|
(ui/barre-apps/) : ne pas modifier la structure ici ; toute évolution
|
||||||
passe par une nouvelle version de commons.
|
passe par une nouvelle version de commons.
|
||||||
CSS/JS : /static/css/draco-barre.css, /static/js/vendor/draco-barre.js.
|
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 —
|
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
|
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). #}
|
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
|
5e onglet Console Spark (data-app="agent", draco_urls().agent),
|
||||||
(data-app="agent", draco_urls().agent), après Kanbante ; l'entrée
|
après Kanbante ; l'entrée « Console Spark » du menu de la marque
|
||||||
« Console Spark » du menu de la marque est conservée.
|
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). #}
|
||||||
<link rel="stylesheet" href="/static/css/draco-alertes.css?v={{ asset_version }}">
|
<link rel="stylesheet" href="/static/css/draco-alertes.css?v={{ asset_version }}">
|
||||||
<header class="draco-bar" data-current="flowdeck">
|
<header class="draco-bar" data-current="flowdeck">
|
||||||
<div class="draco-bar-zone draco-bar-gauche">
|
<div class="draco-bar-zone draco-bar-gauche">
|
||||||
@@ -18,25 +23,25 @@
|
|||||||
</a>
|
</a>
|
||||||
<nav class="draco-apptabs" aria-label="Applications">
|
<nav class="draco-apptabs" aria-label="Applications">
|
||||||
<a class="draco-apptab" data-app="obsigate" href="{{ draco_urls().obsigate }}/">
|
<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>
|
<span class="draco-apptab-txt"><strong>ObsiGate</strong><small>Notes</small></span>
|
||||||
</a>
|
</a>
|
||||||
<a class="draco-apptab" data-app="flowdeck" href="{{ draco_urls().flowdeck }}/">
|
<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>
|
<span class="draco-apptab-txt"><strong>Flowdeck</strong><small>Projets</small></span>
|
||||||
</a>
|
</a>
|
||||||
<a class="draco-apptab" data-app="tiktak" href="{{ draco_urls().tiktak }}/">
|
<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-apptab-txt"><strong>TikTak</strong><small>Tâches</small></span>
|
||||||
<span class="draco-badge" data-badge="tiktak" hidden>0</span>
|
<span class="draco-badge" data-badge="tiktak" hidden>0</span>
|
||||||
</a>
|
</a>
|
||||||
<a class="draco-apptab" data-app="kanbante" href="{{ draco_urls().kanbante }}/">
|
<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-apptab-txt"><strong>Kanbante</strong><small>Kanban</small></span>
|
||||||
<span class="draco-badge" data-badge="kanbante" hidden>0</span>
|
<span class="draco-badge" data-badge="kanbante" hidden>0</span>
|
||||||
</a>
|
</a>
|
||||||
<a class="draco-apptab" data-app="agent" href="{{ draco_urls().agent }}/">
|
<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:#d21f2c" 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>
|
<span class="draco-apptab-txt"><strong>Console Spark</strong><small>Agent commun</small></span>
|
||||||
</a>
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -1,17 +1,22 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title_prefix %}Rechercher partout{% endblock %}
|
{% block title_prefix %}Rechercher partout{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<link rel="stylesheet" href="/static/css/recherche.css">
|
{# Versionnes (?v=asset_version) comme les autres feuilles/scripts de
|
||||||
{# Console : pas de data-base-console ici — la route Flowdeck de /recherche
|
base.html : sans version, le service worker (static/sw.js, cache-first
|
||||||
n'expose pas la base agent dans son contexte `bases` ; recherche.js
|
des .js/.css) resservait indefiniment les copies pre-#33 de ces deux
|
||||||
dérive donc la base console (hôte -dev -> agent-dev, sinon prod) ou
|
fichiers aux navigateurs les ayant deja visites — la page paraissait
|
||||||
reprend le href d'un onglet de barre console/spark quand il existe.
|
donc « non corrigee » malgre les PR #33/#36. #}
|
||||||
Les 3 autres hôtes rendent data-base-console explicitement. #}
|
<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"
|
<main class="rp-page" id="recherche" data-host="flowdeck"
|
||||||
data-base-obsigate="{{ bases.obsigate }}"
|
data-base-obsigate="{{ bases.obsigate }}"
|
||||||
data-base-flowdeck="{{ bases.flowdeck }}"
|
data-base-flowdeck="{{ bases.flowdeck }}"
|
||||||
data-base-tiktak="{{ bases.tiktak }}"
|
data-base-tiktak="{{ bases.tiktak }}"
|
||||||
data-base-kanbante="{{ bases.kanbante }}">
|
data-base-kanbante="{{ bases.kanbante }}"
|
||||||
|
data-base-console="{{ bases.agent }}">
|
||||||
<h1>Rechercher partout</h1>
|
<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 — 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">
|
<div class="rp-form">
|
||||||
@@ -49,5 +54,5 @@
|
|||||||
</main>
|
</main>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script src="/static/js/recherche.js"></script>
|
<script src="/static/js/recherche.js?v={{ asset_version }}"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -94,6 +94,13 @@
|
|||||||
width: 30px; height: 30px; flex: none; display: grid; place-items: center;
|
width: 30px; height: 30px; flex: none; display: grid; place-items: center;
|
||||||
border-radius: 9px; background: var(--app); color: #fff; font-size: 15px;
|
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 { display: none; flex-direction: column; line-height: 1.15; }
|
||||||
.draco-apptab-txt strong { font-size: 14px; font-weight: 700; }
|
.draco-apptab-txt strong { font-size: 14px; font-weight: 700; }
|
||||||
.draco-apptab-txt small { font-size: 11.5px; color: var(--ink-2); }
|
.draco-apptab-txt small { font-size: 11.5px; color: var(--ink-2); }
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
@@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 240">
|
||||||
|
<!-- TikTak — deux arcs (les deux temps), coche-flèche. Bleu #0076fb, orange #fc750c.
|
||||||
|
Géométrie mesurée sur le visuel validé par Bruno (variante B). -->
|
||||||
|
<path d="M 38.2 101.1 A 84 84 0 0 1 201.8 101.1" fill="none" stroke="#0076fb" stroke-width="28" stroke-linecap="round"/>
|
||||||
|
<path d="M 201.5 140.3 A 84 84 0 0 1 38.5 140.3" fill="none" stroke="#fc750c" stroke-width="28" stroke-linecap="round"/>
|
||||||
|
<polyline points="80,129 106,146 163,84" fill="none" stroke="#0076fb" stroke-width="24" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M 128 80 L 163 84 L 167 122" fill="none" stroke="#0076fb" stroke-width="22" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 760 B |
Vendored
+16
-5
@@ -369,7 +369,10 @@
|
|||||||
* de l'agent commun (URL agent résolue — explicite > -dev >
|
* de l'agent commun (URL agent résolue — explicite > -dev >
|
||||||
* défaut prod ; aucun hôte spark-dev inventé). data-app="agent"
|
* défaut prod ; aucun hôte spark-dev inventé). data-app="agent"
|
||||||
* l'intègre à refreshMenuHrefs/applyUrls/setCurrent comme les
|
* 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");
|
var sep = document.createElement("div");
|
||||||
sep.className = "draco-menu-sep";
|
sep.className = "draco-menu-sep";
|
||||||
sep.setAttribute("aria-hidden", "true");
|
sep.setAttribute("aria-hidden", "true");
|
||||||
@@ -379,10 +382,18 @@
|
|||||||
sparkItem.href = resolvedUrls.agent || "#";
|
sparkItem.href = resolvedUrls.agent || "#";
|
||||||
sparkItem.dataset.app = "agent";
|
sparkItem.dataset.app = "agent";
|
||||||
sparkItem.setAttribute("role", "menuitem");
|
sparkItem.setAttribute("role", "menuitem");
|
||||||
var sparkIco = document.createElement("span");
|
var sparkIco;
|
||||||
sparkIco.className = "draco-apptab-ico";
|
var agentTab = document.querySelector('.draco-apptab[data-app="agent"]');
|
||||||
sparkIco.setAttribute("style", "--app:#c2437e");
|
var agentIco = agentTab ? agentTab.querySelector(".draco-apptab-ico") : null;
|
||||||
sparkIco.textContent = "✦";
|
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);
|
sparkItem.appendChild(sparkIco);
|
||||||
var sparkTxt = document.createElement("span");
|
var sparkTxt = document.createElement("span");
|
||||||
sparkTxt.className = "draco-apps-item-txt";
|
sparkTxt.className = "draco-apps-item-txt";
|
||||||
|
|||||||
+8
-1
@@ -8,7 +8,14 @@
|
|||||||
═══════════════════════════════════════════════════════════ */
|
═══════════════════════════════════════════════════════════ */
|
||||||
'use strict';
|
'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';
|
const DATA_CACHE = 'flowdeck-data-v7';
|
||||||
|
|
||||||
// App shell (assets versionnés comme référencés dans les templates).
|
// App shell (assets versionnés comme référencés dans les templates).
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
"""Barre d'apps Flowdeck : aucun texte de chantier rendu hors commentaire.
|
||||||
|
|
||||||
|
Defaut 5 : les lignes « Chantier 5 (fusion #69/#71, sans bump) : ... »
|
||||||
|
etaient placees APRES la fermeture `#}` du commentaire Jinja d'en-tete
|
||||||
|
de `app/templates/_draco_bar.html` (lignes 9-11) : elles etaient donc
|
||||||
|
rendues telles quelles, juste au-dessus du <header> de la barre, sur
|
||||||
|
toutes les pages (la barre est incluse par base.html).
|
||||||
|
|
||||||
|
Test sans dependance (lecture du gabarit seul, comme
|
||||||
|
test_draco_alertes.py) : il tourne sans serveur ni base, donc aussi
|
||||||
|
en sandbox. La note documentaire peut vivre DANS le commentaire Jinja
|
||||||
|
(jamais rendu) ; elle ne doit jamais en sortir.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
|
||||||
|
def _hors_commentaires(texte):
|
||||||
|
"""Texte du gabarit sans les commentaires Jinja {# ... #}."""
|
||||||
|
return re.sub(r"\{#.*?#\}", "", texte, flags=re.S)
|
||||||
|
|
||||||
|
|
||||||
|
def test_aucun_texte_chantier_rendu():
|
||||||
|
gabarit = (ROOT / "app/templates/_draco_bar.html").read_text()
|
||||||
|
rendu = _hors_commentaires(gabarit)
|
||||||
|
assert "Chantier 5 (fusion" not in rendu
|
||||||
|
assert "sans bump" not in rendu
|
||||||
|
# Aucune ligne de texte libre entre le commentaire d'en-tete et <link> :
|
||||||
|
# le rendu doit commencer (apres commentaires) par <link> ou <header>.
|
||||||
|
assert rendu.lstrip().startswith("<link")
|
||||||
|
|
||||||
|
|
||||||
|
def test_barre_intacte_apres_suppression():
|
||||||
|
"""Le 5e onglet Console Spark et la marque survivent a la suppression."""
|
||||||
|
gabarit = (ROOT / "app/templates/_draco_bar.html").read_text()
|
||||||
|
rendu = _hors_commentaires(gabarit)
|
||||||
|
nav = rendu.split('<nav class="draco-apptabs"')[1].split("</nav>")[0]
|
||||||
|
assert nav.index('data-app="kanbante"') < nav.index('data-app="agent"')
|
||||||
|
assert "Console Spark" in nav
|
||||||
|
assert 'data-app="agent"' in nav
|
||||||
|
assert 'class="draco-brand"' in rendu
|
||||||
|
# L'entree « Console Spark » du menu de la marque est construite par
|
||||||
|
# draco-barre.js depuis les onglets : l'onglet agent suffit a l'alimenter.
|
||||||
|
js_barre = (ROOT / "static/js/vendor/draco-barre.js").read_text()
|
||||||
|
assert "Console Spark" in js_barre
|
||||||
+16
-1
@@ -38,7 +38,7 @@ def test_origin_of():
|
|||||||
|
|
||||||
|
|
||||||
def test_sister_origins_dev():
|
def test_sister_origins_dev():
|
||||||
# Correctif 2026-10-28 : 8 origines officielles, prod ET dev,
|
# Correctif 2026-10-28 : 10 origines officielles (8 sœurs + 2 agent), prod ET dev,
|
||||||
# quelle que soit la résolution.
|
# quelle que soit la résolution.
|
||||||
assert sister_origins(RESOLVED_DEV) == {
|
assert sister_origins(RESOLVED_DEV) == {
|
||||||
"https://og.dracodev.net",
|
"https://og.dracodev.net",
|
||||||
@@ -49,9 +49,24 @@ def test_sister_origins_dev():
|
|||||||
"https://flowdeck-dev.dracodev.net",
|
"https://flowdeck-dev.dracodev.net",
|
||||||
"https://tiktak-dev.dracodev.net",
|
"https://tiktak-dev.dracodev.net",
|
||||||
"https://kanbante-dev.dracodev.net",
|
"https://kanbante-dev.dracodev.net",
|
||||||
|
"https://agent.dracodev.net",
|
||||||
|
"https://agent-dev.dracodev.net",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def test_cors_headers_agent_console_autorise():
|
||||||
|
# Fédération depuis la console (2026-10-28) : les origines agent
|
||||||
|
# prod ET dev passent, avec credentials ; un tiers reste refusé.
|
||||||
|
for origine in ("https://agent.dracodev.net",
|
||||||
|
"https://agent-dev.dracodev.net"):
|
||||||
|
headers = cors_headers_for(origine, RESOLVED_DEV)
|
||||||
|
assert headers is not None
|
||||||
|
assert headers["Access-Control-Allow-Origin"] == origine
|
||||||
|
assert headers["Access-Control-Allow-Credentials"] == "true"
|
||||||
|
assert cors_headers_for("https://agent.dracodev.net.evil.example",
|
||||||
|
RESOLVED_DEV) is None
|
||||||
|
|
||||||
def test_sister_origins_ajoute_env_de_la_requete():
|
def test_sister_origins_ajoute_env_de_la_requete():
|
||||||
# Correctif 2026-10-28 : configuration muette, quel que soit
|
# Correctif 2026-10-28 : configuration muette, quel que soit
|
||||||
# l'hôte de la requête, prod ET dev passent — l'hôte ne
|
# 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,85 @@
|
|||||||
|
"""Page /recherche Flowdeck : onglets, base console, anti-cache (defaut 6).
|
||||||
|
|
||||||
|
Audit prealable (main 7.84.4, tag v7.84.4 = 984e92b) : la route
|
||||||
|
(app/routers/search.py:51), le gabarit (app/templates/recherche.html),
|
||||||
|
le JS (static/js/recherche.js) et le CSS (static/css/recherche.css)
|
||||||
|
contenaient DEJA la page a onglets des PR #33/#36 — aucune route
|
||||||
|
fantome, aucun bloc `scripts` manquant dans base.html:869, aucune
|
||||||
|
regle CSP ne bloquant le script (script-src 'self'). Le symptome
|
||||||
|
deploie (« pas d'onglets, resultats Flowdeck seuls ») ne s'explique
|
||||||
|
donc PAS par un mauvais gabarit dans main, mais par deux causes
|
||||||
|
reelles, corrigees ici :
|
||||||
|
|
||||||
|
1. Assets recherche servis CACHE-FIRST par le service worker
|
||||||
|
(static/sw.js) SANS version (?v= absent) sous un CACHE_NAME
|
||||||
|
inchange depuis A20 ('flowdeck-v8') : un navigateur ayant visite
|
||||||
|
Flowdeck avant #33 gardait le recherche.js pre-#33 (sans onglets,
|
||||||
|
sans compteurs, sans logique console, « Tout voir » naviguant
|
||||||
|
ailleurs) et le recherche.css pre-#33 (sans .rp-tabs) — la page
|
||||||
|
paraissait donc « non corrigee » malgre les deplois 7.84.3/7.84.4,
|
||||||
|
et le repli networkFirst du HTML vers le cache couvrait aussi le
|
||||||
|
cas reseau lent. Correctif : liens versionnes (?v=asset_version,
|
||||||
|
comme tous les autres assets de base.html) + bump CACHE_NAME
|
||||||
|
(purge a l'activation du nouveau worker).
|
||||||
|
2. Flowdeck etait le seul hote a ne pas rendre data-base-console
|
||||||
|
(son propre commentaire l'admettait) : la base console de l'onglet
|
||||||
|
Console Spark etait devine (derivation -dev / href d'onglet de
|
||||||
|
barre) au lieu d'etre explicite. Correctif : la route expose
|
||||||
|
`bases.agent` et le gabarit le rend en data-base-console.
|
||||||
|
|
||||||
|
La logique d'onglets elle-meme (focus, compteurs, « Tout voir » sur
|
||||||
|
place, etats par application, liens -dev, console sur
|
||||||
|
GET /api/v1/recherche) est deja couverte par tests/recherche.test.js
|
||||||
|
(suite Node, lancee en sandbox) ; ce fichier-ci verrouille le
|
||||||
|
branchement gabarit/route/service worker, sans serveur ni base.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
ONGLETS_ATTENDUS = ["all", "obsigate", "flowdeck", "tiktak", "kanbante",
|
||||||
|
"console"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_gabarit_recherche_onglets_complets():
|
||||||
|
gabarit = (ROOT / "app/templates/recherche.html").read_text()
|
||||||
|
trouves = re.findall(r'data-rp-tab="([^"]+)"', gabarit)
|
||||||
|
assert trouves == ONGLETS_ATTENDUS, trouves
|
||||||
|
for section in ("obsigate", "flowdeck", "tiktak", "kanbante", "console"):
|
||||||
|
assert f'data-rp-section="{section}"' in gabarit
|
||||||
|
assert 'id="rp-q"' in gabarit and "autofocus" in gabarit
|
||||||
|
|
||||||
|
|
||||||
|
def test_gabarit_recherche_base_console_explicite():
|
||||||
|
gabarit = (ROOT / "app/templates/recherche.html").read_text()
|
||||||
|
assert 'data-base-console="{{ bases.agent }}"' in gabarit
|
||||||
|
|
||||||
|
|
||||||
|
def test_gabarit_recherche_assets_versionnes():
|
||||||
|
"""Sans ?v=, le SW cache-first ressert les copies pre-#33."""
|
||||||
|
gabarit = (ROOT / "app/templates/recherche.html").read_text()
|
||||||
|
assert "/static/css/recherche.css?v={{ asset_version }}" in gabarit
|
||||||
|
assert "/static/js/recherche.js?v={{ asset_version }}" in gabarit
|
||||||
|
|
||||||
|
|
||||||
|
def test_route_recherche_expose_base_agent():
|
||||||
|
route = (ROOT / "app/routers/search.py").read_text()
|
||||||
|
assert '"agent"' in route or "'agent'" in route
|
||||||
|
assert "bases.agent" in (ROOT / "app/templates/recherche.html").read_text()
|
||||||
|
|
||||||
|
|
||||||
|
def test_service_worker_purge_anciens_caches():
|
||||||
|
"""Le bump de CACHE_NAME purge les caches pre-#33 a l'activation."""
|
||||||
|
sw = (ROOT / "static/sw.js").read_text()
|
||||||
|
assert "const CACHE_NAME = 'flowdeck-v9'" in sw
|
||||||
|
assert "flowdeck-v8" not in sw
|
||||||
|
assert "cache-first" in sw or "cacheFirst" in sw
|
||||||
|
|
||||||
|
|
||||||
|
def test_recherche_js_branche_console_v1():
|
||||||
|
"""Rappel : l'onglet console appelle GET /api/v1/recherche (cf. #36)."""
|
||||||
|
js = (ROOT / "static/js/recherche.js").read_text()
|
||||||
|
assert 'CONSOLE_SEARCH_PATH = "api/v1/recherche"' in js
|
||||||
|
assert "CONSOLE_SEARCH_LIMIT = 20" in js
|
||||||
Reference in New Issue
Block a user