Compare commits
33
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -1,3 +1,27 @@
|
||||
## 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.
|
||||
|
||||
@@ -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
|
||||
@@ -49,6 +52,12 @@ DEV_ORIGINS = frozenset({
|
||||
"https://kanbante-dev.dracodev.net",
|
||||
})
|
||||
|
||||
AGENT_ORIGINS = frozenset({
|
||||
"https://agent.dracodev.net",
|
||||
"https://agent-dev.dracodev.net",
|
||||
})
|
||||
|
||||
|
||||
# Clés de draco_urls.toutes() qui sont des applications sœurs.
|
||||
_SOEUR_KEYS = ("obsigate", "flowdeck", "tiktak", "kanbante")
|
||||
|
||||
@@ -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).
|
||||
# ``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 +193,32 @@ class RechercheCorsMiddleware:
|
||||
if message["type"] == "http.response.start":
|
||||
cors = cors_headers_for(origin, request_host=request_host)
|
||||
if cors is not None:
|
||||
message["headers"] = list(message.get("headers", [])) + [
|
||||
# Remplacer, pas empiler (défaut 3b, 2026-10-28) :
|
||||
# le CORSMiddleware global de l'application pose
|
||||
# déjà Access-Control-Allow-Credentials: true sur
|
||||
# TOUTES les réponses (comportement Starlette avec
|
||||
# allow_credentials=True, même pour une origine
|
||||
# hors allowlist et hors /api/search) ; empiler les
|
||||
# en-têtes de recherche donnait, sur /api/search
|
||||
# depuis agent-dev, Allow-Credentials en double
|
||||
# (« true, true » ≠ « true » pour le contrôle CORS
|
||||
# du navigateur → fetch rejeté → « injoignable
|
||||
# (CORS ou réseau) ») et, depuis une sœur
|
||||
# (origine déjà dans la allowlist globale),
|
||||
# Allow-Origin en double (rejet certain). Les
|
||||
# hôtes TikTak/Kanbante, sans ce cumul, répondent
|
||||
# avec des en-têtes uniques — d'où l'échec
|
||||
# Flowdeck seul. Une origine non autorisée
|
||||
# traverse sans modification (comportement
|
||||
# antérieur préservé : le seul en-tête global
|
||||
# reste, sans Allow-Origin, donc bloqué).
|
||||
restants = [
|
||||
(k, v) for k, v in message.get("headers", [])
|
||||
if k.decode("latin-1").lower() not in (
|
||||
"access-control-allow-origin",
|
||||
"access-control-allow-credentials",
|
||||
"vary")]
|
||||
message["headers"] = restants + [
|
||||
(k.lower().encode("latin-1"), v.encode("latin-1"))
|
||||
for k, v in cors.items()
|
||||
]
|
||||
|
||||
@@ -63,7 +63,12 @@ def recherche_page(request: Request, q: str = Query(default="")):
|
||||
user = SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
|
||||
bases = draco_urls.toutes()
|
||||
template = ENV.get_template("recherche.html")
|
||||
# Defaut 6 : la base agent est exposee explicitement (data-base-console
|
||||
# dans recherche.html), comme les 3 autres hotes — recherche.js ne doit
|
||||
# plus la deviner (derivation -dev / href d'onglet de barre) pour
|
||||
# l'onglet Console Spark (GET /api/v1/recherche, alignement #36).
|
||||
return HTMLResponse(template.render(
|
||||
request=request, q=q.strip(), user=user,
|
||||
bases={k: bases[k] for k in ("obsigate", "flowdeck", "tiktak", "kanbante")},
|
||||
bases={k: bases[k] for k in ("obsigate", "flowdeck", "tiktak",
|
||||
"kanbante", "agent")},
|
||||
))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{# 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.10
|
||||
(ui/barre-apps/) : ne pas modifier la structure ici ; toute évolution
|
||||
passe par une nouvelle version de commons.
|
||||
CSS/JS : /static/css/draco-barre.css, /static/js/vendor/draco-barre.js.
|
||||
@@ -8,35 +8,45 @@
|
||||
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). #}
|
||||
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. #}
|
||||
<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/logos/spark.png" alt="" width="26" height="26"></span><span class="draco-brand-name">dracodev</span>
|
||||
</a>
|
||||
<nav class="draco-apptabs" aria-label="Applications">
|
||||
<a class="draco-apptab" data-app="obsigate" href="{{ draco_urls().obsigate }}/">
|
||||
<span class="draco-apptab-ico" style="--app:#7a5af5">▤</span>
|
||||
<span class="draco-apptab-ico" style="--app:#7a5af5" data-icon="obsigate"><img src="/static/img/logos/obsigate.png" alt="ObsiGate" width="30" height="30"></span>
|
||||
<span class="draco-apptab-txt"><strong>ObsiGate</strong><small>Notes</small></span>
|
||||
</a>
|
||||
<a class="draco-apptab" data-app="flowdeck" href="{{ draco_urls().flowdeck }}/">
|
||||
<span class="draco-apptab-ico" style="--app:#2f6fed">▦</span>
|
||||
<span class="draco-apptab-ico" style="--app:#2f6fed" data-icon="flowdeck"><img src="/static/img/logos/flowdeck.png" alt="Flowdeck" width="30" height="30"></span>
|
||||
<span class="draco-apptab-txt"><strong>Flowdeck</strong><small>Projets</small></span>
|
||||
</a>
|
||||
<a class="draco-apptab" data-app="tiktak" href="{{ draco_urls().tiktak }}/">
|
||||
<span class="draco-apptab-ico" style="--app:#e8762c">✓</span>
|
||||
<span class="draco-apptab-ico" style="--app:#e8762c" data-icon="tiktak"><img src="/static/img/logos/tiktak.svg" alt="TikTak" width="30" height="30"></span>
|
||||
<span class="draco-apptab-txt"><strong>TikTak</strong><small>Tâches</small></span>
|
||||
<span class="draco-badge" data-badge="tiktak" hidden>0</span>
|
||||
</a>
|
||||
<a class="draco-apptab" data-app="kanbante" href="{{ draco_urls().kanbante }}/">
|
||||
<span class="draco-apptab-ico" style="--app:#0d9488">▥</span>
|
||||
<span class="draco-apptab-ico" style="--app:#0d9488" data-icon="kanbante"><img src="/static/img/logos/kanbante.png" alt="Kanbante" width="30" height="30"></span>
|
||||
<span class="draco-apptab-txt"><strong>Kanbante</strong><small>Kanban</small></span>
|
||||
<span class="draco-badge" data-badge="kanbante" hidden>0</span>
|
||||
</a>
|
||||
<a class="draco-apptab" data-app="agent" href="{{ draco_urls().agent }}/">
|
||||
<span class="draco-apptab-ico" style="--app:#c2437e">✦</span>
|
||||
<span class="draco-apptab-ico" style="--app:#c00000" data-icon="agent"><img src="/static/img/logos/spark.png" alt="Console Spark" width="30" height="30"></span>
|
||||
<span class="draco-apptab-txt"><strong>Console Spark</strong><small>Agent commun</small></span>
|
||||
</a>
|
||||
</nav>
|
||||
@@ -57,16 +67,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).
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
{% 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-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>
|
||||
<div class="rp-form">
|
||||
@@ -49,5 +54,5 @@
|
||||
</main>
|
||||
{% endblock %}
|
||||
{% block scripts %}
|
||||
<script src="/static/js/recherche.js"></script>
|
||||
<script src="/static/js/recherche.js?v={{ asset_version }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
+27
-11
@@ -7,6 +7,9 @@
|
||||
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.
|
||||
Jetons repris du prototype validé Console v7.2.
|
||||
Thème : clair par défaut, sombre via [data-theme="dark"] sur <html>
|
||||
ou prefers-color-scheme si l'app intégratrice ne force rien. */
|
||||
@@ -14,7 +17,7 @@
|
||||
.draco-bar {
|
||||
--bg: #f2f2f0; --surface: #ffffff; --surface-2: #e9eae6; --surface-3: #f6f6f3;
|
||||
--ink: #1b1d1a; --ink-2: #5c615a; --ink-3: #787d75; --line: #dedfd9;
|
||||
--agent: #c2437e; --danger: #cf4444;
|
||||
--agent: #c00000; --danger: #cf4444;
|
||||
position: relative;
|
||||
height: 60px; box-sizing: border-box;
|
||||
display: grid; grid-template-columns: auto minmax(200px, 520px) auto;
|
||||
@@ -37,7 +40,8 @@
|
||||
.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; }
|
||||
.draco-brand-mark img { width: 26px; height: 26px; border-radius: 8px; object-fit: cover; display: block; }
|
||||
|
||||
/* Menu des applications — construit par le JS depuis les onglets */
|
||||
.draco-apps-menu {
|
||||
@@ -94,6 +98,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 +142,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; }
|
||||
.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 +185,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; }
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 91 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
@@ -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 >
|
||||
* défaut prod ; aucun hôte spark-dev inventé). data-app="agent"
|
||||
* l'intègre à refreshMenuHrefs/applyUrls/setCurrent comme les
|
||||
* autres items ; l'icône ✦ reprend la couleur agent du CSS. */
|
||||
* autres items ; son icône CLONE celle de l'onglet agent
|
||||
* (2026-10-29, logos barre-apps, corrigé le même jour) : le logo
|
||||
* fourni, tel quel, jamais redessiné — le CSS --agent (rose) des
|
||||
* autres éléments de la barre n'est pas changé ici. */
|
||||
var sep = document.createElement("div");
|
||||
sep.className = "draco-menu-sep";
|
||||
sep.setAttribute("aria-hidden", "true");
|
||||
@@ -379,10 +382,18 @@
|
||||
sparkItem.href = resolvedUrls.agent || "#";
|
||||
sparkItem.dataset.app = "agent";
|
||||
sparkItem.setAttribute("role", "menuitem");
|
||||
var sparkIco = document.createElement("span");
|
||||
sparkIco.className = "draco-apptab-ico";
|
||||
sparkIco.setAttribute("style", "--app:#c2437e");
|
||||
sparkIco.textContent = "✦";
|
||||
var sparkIco;
|
||||
var agentTab = document.querySelector('.draco-apptab[data-app="agent"]');
|
||||
var agentIco = agentTab ? agentTab.querySelector(".draco-apptab-ico") : null;
|
||||
if (agentIco) {
|
||||
sparkIco = agentIco.cloneNode(true);
|
||||
} else {
|
||||
sparkIco = document.createElement("span");
|
||||
sparkIco.className = "draco-apptab-ico";
|
||||
sparkIco.setAttribute("style", "--app:#d21f2c");
|
||||
sparkIco.setAttribute("data-icon", "agent");
|
||||
sparkIco.textContent = "✦";
|
||||
}
|
||||
sparkItem.appendChild(sparkIco);
|
||||
var sparkTxt = document.createElement("span");
|
||||
sparkTxt.className = "draco-apps-item-txt";
|
||||
|
||||
+8
-1
@@ -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).
|
||||
|
||||
+16
-1
@@ -38,7 +38,7 @@ def test_origin_of():
|
||||
|
||||
|
||||
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.
|
||||
assert sister_origins(RESOLVED_DEV) == {
|
||||
"https://og.dracodev.net",
|
||||
@@ -49,9 +49,24 @@ def test_sister_origins_dev():
|
||||
"https://flowdeck-dev.dracodev.net",
|
||||
"https://tiktak-dev.dracodev.net",
|
||||
"https://kanbante-dev.dracodev.net",
|
||||
"https://agent.dracodev.net",
|
||||
"https://agent-dev.dracodev.net",
|
||||
}
|
||||
|
||||
|
||||
|
||||
def test_cors_headers_agent_console_autorise():
|
||||
# Fédération depuis la console (2026-10-28) : les origines agent
|
||||
# prod ET dev passent, avec credentials ; un tiers reste refusé.
|
||||
for origine in ("https://agent.dracodev.net",
|
||||
"https://agent-dev.dracodev.net"):
|
||||
headers = cors_headers_for(origine, RESOLVED_DEV)
|
||||
assert headers is not None
|
||||
assert headers["Access-Control-Allow-Origin"] == origine
|
||||
assert headers["Access-Control-Allow-Credentials"] == "true"
|
||||
assert cors_headers_for("https://agent.dracodev.net.evil.example",
|
||||
RESOLVED_DEV) is None
|
||||
|
||||
def test_sister_origins_ajoute_env_de_la_requete():
|
||||
# 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,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