Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8403b4fa08 | ||
|
|
757e32229d | ||
|
|
4c995f4c65 | ||
|
|
c950940e22 | ||
|
|
858dfd22e1 | ||
|
|
ab4a4dbd9d | ||
|
|
0bc1aca9bc | ||
|
|
515727ab9b | ||
|
|
3f375bca2a | ||
|
|
483af904af | ||
|
|
bfe0c75106 | ||
|
|
2a96ad3b1d | ||
|
|
12a29b29f6 | ||
|
|
8feab0188c | ||
|
|
fc44efabbb | ||
|
|
3ed09cbc0f | ||
|
|
a4f27bdbe7 | ||
|
|
05d1903045 | ||
|
|
b5b4a00b75 | ||
|
|
97f997840c | ||
|
|
474851b3bb | ||
|
|
4dfa8fd74c | ||
|
|
e709310e70 | ||
|
|
4d0c8ad934 | ||
|
|
b609e8913c | ||
|
|
f10e3d12e6 | ||
|
|
78c514c78b | ||
|
|
d20ae93b40 | ||
|
|
a885153a68 | ||
|
|
3edeb88c92 | ||
|
|
677b174f09 | ||
|
|
ab3d0798c7 | ||
|
|
332983c4d6 | ||
|
|
2c2f23dc57 | ||
|
|
097accc44c | ||
|
|
7ea5614093 | ||
|
|
e4da00cf17 | ||
|
|
03e8abf8c8 | ||
|
|
6750cdf81a | ||
|
|
ffc70e10b9 |
+15
-1
@@ -6,7 +6,7 @@ Format basé sur [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/),
|
|||||||
et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
> **En cours de développement** : les changements à venir sont listés dans la section
|
> **En cours de développement** : les changements à venir sont listés dans la section
|
||||||
> [Unreleased](#unreleased). La dernière version livrée est **2.82.1**.
|
> [Unreleased](#unreleased). La dernière version livrée est **2.82.4**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -14,6 +14,20 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## [2.82.4] — 2026-10-28
|
||||||
|
|
||||||
|
- **Recherche (PR #52)** : texte parasite du mécanisme de thème retiré du haut de la page `/recherche` (lignes 28-43 de `frontend/recherche.html`, hors commentaire).
|
||||||
|
- **Démarrage (PR #53)** : écran de chargement plein écran `#boot-splash` retiré au démarrage d'ObsiGate (CSS, div et script de pilotage), sans écran de remplacement.
|
||||||
|
|
||||||
|
## [2.82.3] — 2026-10-28
|
||||||
|
|
||||||
|
- **Recherche (PR #49)** : la page `/recherche` applique désormais le thème ObsiGate.
|
||||||
|
- **CORS recherche (PR #51)** : la recherche ObsiGate autorise désormais la console Spark (agent prod+dev) en inter-origines.
|
||||||
|
|
||||||
|
## [2.82.2] — 2026-10-28
|
||||||
|
|
||||||
|
- **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 ; barre de `/recherche` re-vendangée depuis `index.html` (5e onglet Console Spark) et `connect-src` CSP étendu à l'origine agent.
|
||||||
|
|
||||||
## [2.82.1] — 2026-10-28
|
## [2.82.1] — 2026-10-28
|
||||||
|
|
||||||
### Modifié
|
### Modifié
|
||||||
|
|||||||
+12
-7
@@ -966,12 +966,15 @@ if FRONTEND_DIR.exists():
|
|||||||
|
|
||||||
frontend/recherche.html est servi avec les jetons {{URL_*}}
|
frontend/recherche.html est servi avec les jetons {{URL_*}}
|
||||||
remplacés par urls_publiques() (même contrat que le fragment
|
remplacés par urls_publiques() (même contrat que le fragment
|
||||||
de barre) et le nonce CSP frais ; la réponse pose sa propre
|
de barre, jeton {{URL_AGENT}} inclus pour l'onglet Console
|
||||||
CSP (préservée par SecurityHeadersMiddleware, qui ne la pose
|
Spark de la barre re-vendangée) et le nonce CSP frais ; la
|
||||||
que si elle est absente) dont le connect-src autorise les
|
réponse pose sa propre CSP (préservée par
|
||||||
origines sœurs — sinon le navigateur bloquerait les appels
|
SecurityHeadersMiddleware, qui ne la pose que si elle est
|
||||||
sœurs avant même CORS. La CSP par défaut (same-origin) reste
|
absente) dont le connect-src autorise les origines sœurs ET
|
||||||
inchangée pour toutes les autres pages.
|
l'origine de l'agent — sinon le navigateur bloquerait les
|
||||||
|
appels sœurs et l'appel console (onglet Console Spark,
|
||||||
|
GET /api/v1/recherche) avant même CORS. La CSP par défaut
|
||||||
|
(same-origin) reste inchangée pour toutes les autres pages.
|
||||||
"""
|
"""
|
||||||
import json as _json
|
import json as _json
|
||||||
|
|
||||||
@@ -989,6 +992,7 @@ if FRONTEND_DIR.exists():
|
|||||||
("{{URL_FLOWDECK}}", "flowdeck"),
|
("{{URL_FLOWDECK}}", "flowdeck"),
|
||||||
("{{URL_TIKTAK}}", "tiktak"),
|
("{{URL_TIKTAK}}", "tiktak"),
|
||||||
("{{URL_KANBANTE}}", "kanbante"),
|
("{{URL_KANBANTE}}", "kanbante"),
|
||||||
|
("{{URL_AGENT}}", "agent"),
|
||||||
):
|
):
|
||||||
html = html.replace(jeton, urls.get(cle, ""))
|
html = html.replace(jeton, urls.get(cle, ""))
|
||||||
script = (
|
script = (
|
||||||
@@ -1003,7 +1007,8 @@ if FRONTEND_DIR.exists():
|
|||||||
html = inject_csp_nonce(html, request.state.csp_nonce)
|
html = inject_csp_nonce(html, request.state.csp_nonce)
|
||||||
soeurs = " ".join(sorted({
|
soeurs = " ".join(sorted({
|
||||||
origin_of(urls.get(cle, "")) or ""
|
origin_of(urls.get(cle, "")) or ""
|
||||||
for cle in ("obsigate", "flowdeck", "tiktak", "kanbante")
|
for cle in ("obsigate", "flowdeck", "tiktak", "kanbante",
|
||||||
|
"agent")
|
||||||
} - {""}))
|
} - {""}))
|
||||||
csp = (
|
csp = (
|
||||||
"default-src 'self'; "
|
"default-src 'self'; "
|
||||||
|
|||||||
@@ -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 de ``backend.draco_config`` (``urls_publiques()`` :
|
celles de ``backend.draco_config`` (``urls_publiques()`` :
|
||||||
explicite > dérivation par l'hôte de l'agent > défaut officiel)
|
explicite > dérivation par l'hôte de l'agent > défaut officiel)
|
||||||
— jamais ``*``. Correctif 2026-10-28 (fusion #68/#69, matrice
|
— jamais ``*``. Correctif 2026-10-28 (fusion #68/#69, matrice
|
||||||
@@ -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_config.urls_publiques() qui sont des applications sœurs.
|
# Clés de draco_config.urls_publiques() 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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Generated
+1
-1
@@ -2626,7 +2626,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "obsigate-desktop"
|
name = "obsigate-desktop"
|
||||||
version = "2.82.1"
|
version = "2.82.4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "obsigate-desktop"
|
name = "obsigate-desktop"
|
||||||
version = "2.82.1"
|
version = "2.82.4"
|
||||||
description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian"
|
description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian"
|
||||||
authors = ["Bruno Charest"]
|
authors = ["Bruno Charest"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json",
|
"$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json",
|
||||||
"productName": "ObsiGate",
|
"productName": "ObsiGate",
|
||||||
"version": "2.82.1",
|
"version": "2.82.4",
|
||||||
"identifier": "com.obsigate.desktop",
|
"identifier": "com.obsigate.desktop",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../frontend",
|
"frontendDist": "../frontend",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
# ObsiGate — Roadmap
|
# ObsiGate — Roadmap
|
||||||
|
|
||||||
> **Version :** 2.82.1 | **Dernière mise à jour :** 2026-10-28
|
> **Version :** 2.82.4 | **Dernière mise à jour :** 2026-10-28
|
||||||
> **Ce fichier ne contient que le travail à venir** (🔵 En cours + ⚪ Backlog) et un index compact
|
> **Ce fichier ne contient que le travail à venir** (🔵 En cours + ⚪ Backlog) et un index compact
|
||||||
> vers les fonctionnalités livrées.
|
> vers les fonctionnalités livrées.
|
||||||
> - **Méthode de livraison à appliquer pour toute tâche : [DELIVERY_WORKFLOW.md](./DELIVERY_WORKFLOW.md)**
|
> - **Méthode de livraison à appliquer pour toute tâche : [DELIVERY_WORKFLOW.md](./DELIVERY_WORKFLOW.md)**
|
||||||
|
|||||||
+1
-66
@@ -47,33 +47,6 @@
|
|||||||
href="/static/icons/icon-192x192.svg"
|
href="/static/icons/icon-192x192.svg"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Boot splash CSS — inline so it applies BEFORE /static/style.css is
|
|
||||||
available (desktop: embedded page served from tauri:// has no /static) -->
|
|
||||||
<style id="boot-splash-css">
|
|
||||||
body:not(.app-booted) > *:not(#boot-splash) { display: none !important; }
|
|
||||||
#boot-splash {
|
|
||||||
position: fixed; inset: 0; z-index: 2147483647;
|
|
||||||
display: flex; align-items: center; justify-content: center;
|
|
||||||
background: radial-gradient(1200px 600px at 50% -10%, #26272b 0%, #17181a 60%);
|
|
||||||
color: #eceeeb; font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
|
|
||||||
}
|
|
||||||
body.app-booted #boot-splash { display: none; }
|
|
||||||
.bs-card { display: flex; flex-direction: column; align-items: center; gap: 14px; }
|
|
||||||
.bs-logo {
|
|
||||||
width: 72px; height: 72px; display: flex; align-items: center; justify-content: center;
|
|
||||||
font-size: 38px; background: #1f2023; border: 1px solid #33353a; border-radius: 18px;
|
|
||||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.45);
|
|
||||||
}
|
|
||||||
.bs-title { font-size: 22px; font-weight: 600; letter-spacing: 0.3px; }
|
|
||||||
.bs-spinner {
|
|
||||||
width: 34px; height: 34px; border-radius: 50%;
|
|
||||||
border: 3px solid rgba(124, 58, 237, 0.25); border-top-color: #7c3aed;
|
|
||||||
animation: bs-spin 0.9s linear infinite;
|
|
||||||
}
|
|
||||||
@keyframes bs-spin { to { transform: rotate(360deg); } }
|
|
||||||
.bs-status { font-size: 13px; color: #9aa3b2; min-height: 18px; }
|
|
||||||
@media (prefers-reduced-motion: reduce) { .bs-spinner { animation-duration: 1.8s; } }
|
|
||||||
</style>
|
|
||||||
<link rel="stylesheet" href="/static/style.css" />
|
<link rel="stylesheet" href="/static/style.css" />
|
||||||
<link rel="stylesheet" href="/static/draco-barre.css" />
|
<link rel="stylesheet" href="/static/draco-barre.css" />
|
||||||
<link rel="stylesheet" href="/static/draco-alertes.css" />
|
<link rel="stylesheet" href="/static/draco-alertes.css" />
|
||||||
@@ -89,7 +62,7 @@
|
|||||||
id="hljs-theme-light"
|
id="hljs-theme-light"
|
||||||
disabled
|
disabled
|
||||||
/>
|
/>
|
||||||
<!-- defer: ne bloque pas le premier paint (le splash inline s'affiche immédiatement) -->
|
<!-- defer: ne bloque pas le premier paint -->
|
||||||
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||||||
<!-- Langues highlight.js hors bundle "common" (enregistrement auto apres le core) -->
|
<!-- Langues highlight.js hors bundle "common" (enregistrement auto apres le core) -->
|
||||||
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/dockerfile.min.js"></script>
|
<script defer src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/dockerfile.min.js"></script>
|
||||||
@@ -226,44 +199,6 @@
|
|||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body class="has-draco-bar">
|
<body class="has-draco-bar">
|
||||||
<!-- Boot splash : visible tant que l'app n'a pas booté avec son CSS.
|
|
||||||
Sur desktop, la page embarquée (tauri://) n'a pas /static/style.css →
|
|
||||||
sans ce splash, le squelette HTML brut serait visible pendant le
|
|
||||||
démarrage du backend (FOUC 3-10 s). Styles 100% inline. -->
|
|
||||||
<div id="boot-splash" role="status" aria-live="polite">
|
|
||||||
<div class="bs-card">
|
|
||||||
<div class="bs-logo">📖</div>
|
|
||||||
<div class="bs-title">ObsiGate</div>
|
|
||||||
<div class="bs-spinner" aria-hidden="true"></div>
|
|
||||||
<div class="bs-status" id="boot-splash-status">Démarrage…</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
(function () {
|
|
||||||
var done = false;
|
|
||||||
function bootReady() {
|
|
||||||
if (done) return;
|
|
||||||
done = true;
|
|
||||||
document.body.classList.add("app-booted");
|
|
||||||
var s = document.getElementById("boot-splash");
|
|
||||||
if (s && s.parentNode) s.parentNode.removeChild(s);
|
|
||||||
}
|
|
||||||
window.__obsigateBootReady = bootReady;
|
|
||||||
window.__setBootStatus = function (txt) {
|
|
||||||
var el = document.getElementById("boot-splash-status");
|
|
||||||
if (el) el.textContent = txt;
|
|
||||||
};
|
|
||||||
var poll = setInterval(function () {
|
|
||||||
if (window.__OBSIGATE_BOOTED) { clearInterval(poll); bootReady(); }
|
|
||||||
}, 80);
|
|
||||||
// Filet de sécurité : si le CSS principal a chargé (origine backend réelle)
|
|
||||||
// mais que le JS a échoué, révéler l'app après 10 s au lieu d'un splash infini.
|
|
||||||
setTimeout(function () {
|
|
||||||
var link = document.querySelector('link[href*="style.css"]');
|
|
||||||
if (link && link.sheet) { clearInterval(poll); bootReady(); }
|
|
||||||
}, 10000);
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
<div class="search-progress-bar" id="search-progress-bar">
|
<div class="search-progress-bar" id="search-progress-bar">
|
||||||
<div class="search-progress-bar__fill"></div>
|
<div class="search-progress-bar__fill"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+2
-10
@@ -120,16 +120,8 @@ async function init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", async () => {
|
document.addEventListener("DOMContentLoaded", async () => {
|
||||||
try {
|
await init();
|
||||||
await init();
|
Sync.init();
|
||||||
Sync.init();
|
|
||||||
} finally {
|
|
||||||
// Signale au splash de démarrage (index.html #boot-splash) que l'app est
|
|
||||||
// initialisée : le splash est retiré et le squelette stylé révélé.
|
|
||||||
// Set AFTER init() resolves so every handler (search, tabs, …) is bound.
|
|
||||||
window.__OBSIGATE_BOOTED = true;
|
|
||||||
if (typeof window.__obsigateBootReady === "function") window.__obsigateBootReady();
|
|
||||||
}
|
|
||||||
// Show version in header — always visible, fallback before the async fetch
|
// Show version in header — always visible, fallback before the async fetch
|
||||||
populateVersions();
|
populateVersions();
|
||||||
fetch('/api/health')
|
fetch('/api/health')
|
||||||
|
|||||||
+128
-31
@@ -46,23 +46,35 @@
|
|||||||
* -> { groups: [{ full_name, rows: [{ issue, column }] }] }
|
* -> { groups: [{ full_name, rows: [{ issue, column }] }] }
|
||||||
* Lien profond : {base}board/<full_name> (la carte n'a pas d'URL propre
|
* 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).
|
* adressable — même limite que la page /search native de Kanbante).
|
||||||
* - Console Spark : AUCUN endpoint de recherche mergé au moment de
|
* - Console Spark : GET {base-console}/api/v1/recherche?q=&limite=
|
||||||
* cette livraison (agent-commun : seule la PR #12 est ouverte, autre
|
* (agent-commun, authentifié par le cookie SSO draco_session) ->
|
||||||
* programme — ce dépôt n'est pas touché ici). Le code tente quand
|
* { q, domain, limite_par_type, resultats[], par_type,
|
||||||
* même l'appel conventionnel GET {base-console}/api/search?q= (même
|
* non_cherchables[] } ; chaque résultat porte type, id, titre,
|
||||||
* convention que les autres hôtes ; chemin À CONFIRMER avec le
|
* extrait, statut, domaine, section, objet et lien (relatif à
|
||||||
* travailleur console dès que son endpoint atterrit) et, en cas
|
* l'origine console). q < 2 caractères -> 422. Types cherchables :
|
||||||
* d'échec, de HTTP non-OK ou de format non reconnu, affiche
|
* discussion, mission, approbation, livrable, activite,
|
||||||
* honnêtement « Console Spark : recherche indisponible pour
|
* planification, objectif, identite (aucun secret). Liens profonds
|
||||||
* l'instant » — jamais de faux résultats, jamais de faux 0. La base
|
* (préfixés par la base console) : discussion
|
||||||
* console vient, dans l'ordre, de data-base-console rendu par l'hôte,
|
* /?section=conversation&disc=<id>, mission
|
||||||
* de window.DRACO_URLS.agent (ObsiGate) / window.DRACO_AGENT_URL
|
* /?section=missions&objet=<id>, approbation
|
||||||
* (TikTak), de la dérivation -dev si l'hôte courant est -dev, du
|
* /?section=approbations&objet=<id>, objectif
|
||||||
* href d'un onglet de barre data-app="console"/"spark" s'il existe,
|
* /?section=objectifs&objet=<id>, activite /?activite=<id> ;
|
||||||
* sinon du défaut prod agent.dracodev.net. L'onglet console n'est
|
* livrable /?section=livrables, planification /?section=planifie
|
||||||
* interrogé que lorsqu'il est affiché (ou quand une recherche a lieu
|
* et identite /?section=identites ouvrent la vue sans sélection
|
||||||
* pendant qu'il est affiché) : « Tout » ne le contient pas, pour ne
|
* d'objet. L'onglet appelle avec limite=20 (plafond par type) car
|
||||||
* pas polluer la vue groupée d'un message d'indisponibilité.
|
* 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) :
|
* 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 >
|
* 1. data-base-* rendus par l'hôte (son propre résolveur : explicite >
|
||||||
@@ -76,12 +88,14 @@
|
|||||||
* credentials:"include". Le navigateur envoie alors les cookies de
|
* credentials:"include". Le navigateur envoie alors les cookies de
|
||||||
* chaque origine, dont le cookie SSO partagé draco_session
|
* chaque origine, dont le cookie SSO partagé draco_session
|
||||||
* (Domain=.dracodev.net), accepté par les 4 API de recherche
|
* (Domain=.dracodev.net), accepté par les 4 API de recherche
|
||||||
* (ObsiGate et TikTak nativement, Flowdeck et Kanbante via #68).
|
* (ObsiGate et TikTak nativement, Flowdeck et Kanbante via #68)
|
||||||
* Chaque sœur n'autorise en CORS que les origines sœurs
|
* et par GET /api/v1/recherche de la console (identity() accepte
|
||||||
* (GET sur l'endpoint de recherche uniquement, pas de "*").
|
* 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 :
|
* Une sœur injoignable / non autorisée ne casse pas les autres :
|
||||||
* sa section affiche l'erreur + un lien vers sa recherche native.
|
* sa section affiche l'erreur + un lien vers sa recherche native.
|
||||||
* Le correctif CORS lui-même est chez un autre travailleur.
|
|
||||||
*/
|
*/
|
||||||
(function (global) {
|
(function (global) {
|
||||||
"use strict";
|
"use strict";
|
||||||
@@ -104,7 +118,8 @@
|
|||||||
};
|
};
|
||||||
var PROD_AGENT = "https://agent.dracodev.net/";
|
var PROD_AGENT = "https://agent.dracodev.net/";
|
||||||
var DEV_AGENT = "https://agent-dev.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 = [
|
var APPS = [
|
||||||
{ key: "obsigate", label: "ObsiGate" },
|
{ key: "obsigate", label: "ObsiGate" },
|
||||||
{ key: "flowdeck", label: "Flowdeck" },
|
{ key: "flowdeck", label: "Flowdeck" },
|
||||||
@@ -112,6 +127,24 @@
|
|||||||
{ key: "kanbante", label: "Kanbante" },
|
{ key: "kanbante", label: "Kanbante" },
|
||||||
];
|
];
|
||||||
var CONSOLE_APP = { key: "console", label: "Console Spark" };
|
var CONSOLE_APP = { key: "console", label: "Console Spark" };
|
||||||
|
var CONSOLE_TYPE_LABELS = {
|
||||||
|
discussion: "Discussion",
|
||||||
|
mission: "Mission",
|
||||||
|
approbation: "Approbation",
|
||||||
|
livrable: "Livrable",
|
||||||
|
activite: "Activité",
|
||||||
|
planification: "Planification",
|
||||||
|
objectif: "Objectif",
|
||||||
|
identite: "Identité",
|
||||||
|
};
|
||||||
|
/* Lien profond console (relatif à l'origine console) quand le
|
||||||
|
* résultat n'en porte pas : même règle que _lien_recherche
|
||||||
|
* d'agent-commun (section + objet, activite en section=null). */
|
||||||
|
var CONSOLE_VIEW_LINKS = {
|
||||||
|
livrable: "/?section=livrables",
|
||||||
|
planification: "/?section=planifie",
|
||||||
|
identite: "/?section=identites",
|
||||||
|
};
|
||||||
var TABS = ["all", "obsigate", "flowdeck", "tiktak", "kanbante", "console"];
|
var TABS = ["all", "obsigate", "flowdeck", "tiktak", "kanbante", "console"];
|
||||||
var COLUMN_LABELS = {
|
var COLUMN_LABELS = {
|
||||||
backlog: "Backlog",
|
backlog: "Backlog",
|
||||||
@@ -182,7 +215,8 @@
|
|||||||
if (!u && isDevHost(opts.host)) u = DEV_AGENT;
|
if (!u && isDevHost(opts.host)) u = DEV_AGENT;
|
||||||
if (!u)
|
if (!u)
|
||||||
u = cleanUrl(
|
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;
|
if (!u) u = PROD_AGENT;
|
||||||
return withSlash(u);
|
return withSlash(u);
|
||||||
@@ -336,19 +370,71 @@
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Normalisation console, défensive : accepte results/discussions/
|
/* Normalisation console : format réel GET /api/v1/recherche,
|
||||||
* items. Un élément sans URL exploitable (http(s), /relatif, #ancre)
|
* { resultats: [{ type, id, titre, extrait, statut, domaine,
|
||||||
* garde url=null et sera rendu en <span>, pas en faux lien profond.
|
* section, objet, lien }] }. Le lien relatif est préfixé par la
|
||||||
* Retourne { recognized, items } : recognized=false -> l'appelant
|
* base console ; sans lien, il est reconstruit depuis type/id
|
||||||
* affiche « indisponible », jamais un vide mensonger. */
|
* (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) {
|
function normalizeConsole(data, base) {
|
||||||
data = data || {};
|
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;
|
var list = null;
|
||||||
if (Array.isArray(data.results)) list = data.results;
|
if (Array.isArray(data.results)) list = data.results;
|
||||||
else if (Array.isArray(data.discussions)) list = data.discussions;
|
else if (Array.isArray(data.discussions)) list = data.discussions;
|
||||||
else if (Array.isArray(data.items)) list = data.items;
|
else if (Array.isArray(data.items)) list = data.items;
|
||||||
if (!list) return { recognized: false, items: [] };
|
if (!list) return { recognized: false, items: [] };
|
||||||
var items = list.map(function (r) {
|
var legacy = list.map(function (r) {
|
||||||
r = r || {};
|
r = r || {};
|
||||||
var raw = r.url || r.href || r.link || "";
|
var raw = r.url || r.href || r.link || "";
|
||||||
var url = null;
|
var url = null;
|
||||||
@@ -361,7 +447,7 @@
|
|||||||
url: url,
|
url: url,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
return { recognized: true, items: items };
|
return { recognized: true, items: legacy };
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Recherche native de l'app (repli quand la section est en erreur). */
|
/* Recherche native de l'app (repli quand la section est en erreur). */
|
||||||
@@ -408,6 +494,9 @@
|
|||||||
PROD_AGENT: PROD_AGENT,
|
PROD_AGENT: PROD_AGENT,
|
||||||
DEV_AGENT: DEV_AGENT,
|
DEV_AGENT: DEV_AGENT,
|
||||||
CONSOLE_SEARCH_PATH: CONSOLE_SEARCH_PATH,
|
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,
|
PER_APP_LIMIT: PER_APP_LIMIT,
|
||||||
FULL_LIMIT: FULL_LIMIT,
|
FULL_LIMIT: FULL_LIMIT,
|
||||||
};
|
};
|
||||||
@@ -671,12 +760,20 @@
|
|||||||
};
|
};
|
||||||
renderSection(sec, states.console, true);
|
renderSection(sec, states.console, true);
|
||||||
renderTabs();
|
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) {
|
fetchSection("console", url, function (err, data) {
|
||||||
if (myRun !== runId) return;
|
if (myRun !== runId) return;
|
||||||
if (err) {
|
if (err) {
|
||||||
var msg;
|
var msg;
|
||||||
if (err.name === "AbortError") msg = "délai dépassé (8 s)";
|
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 if (err.status) msg = "HTTP " + err.status;
|
||||||
else msg = "injoignable (CORS ou réseau)";
|
else msg = "injoignable (CORS ou réseau)";
|
||||||
states.console = {
|
states.console = {
|
||||||
|
|||||||
+29
-8
@@ -7,17 +7,17 @@
|
|||||||
.rp-form { display: flex; gap: 8px; margin-bottom: 24px; }
|
.rp-form { display: flex; gap: 8px; margin-bottom: 24px; }
|
||||||
.rp-input {
|
.rp-input {
|
||||||
flex: 1; font: inherit; font-size: 1.05rem; padding: 10px 12px;
|
flex: 1; font: inherit; font-size: 1.05rem; padding: 10px 12px;
|
||||||
border: 1px solid #888; border-radius: 8px; background: inherit; color: inherit;
|
border: 1px solid var(--border, #888); border-radius: 8px; background: inherit; color: inherit;
|
||||||
}
|
}
|
||||||
.rp-input:focus { outline: 2px solid #3b82f6; outline-offset: 1px; }
|
.rp-input:focus { outline: 2px solid var(--accent, #3b82f6); outline-offset: 1px; }
|
||||||
.rp-section { margin-bottom: 28px; }
|
.rp-section { margin-bottom: 28px; }
|
||||||
.rp-head {
|
.rp-head {
|
||||||
display: flex; align-items: baseline; gap: 10px;
|
display: flex; align-items: baseline; gap: 10px;
|
||||||
border-bottom: 1px solid #888; padding-bottom: 6px; margin-bottom: 8px;
|
border-bottom: 1px solid var(--border, #888); padding-bottom: 6px; margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
.rp-head h2 { font-size: 1.05rem; margin: 0; }
|
.rp-head h2 { font-size: 1.05rem; margin: 0; }
|
||||||
.rp-count {
|
.rp-count {
|
||||||
font-size: .8rem; font-weight: 700; background: #3b82f6; color: #fff;
|
font-size: .8rem; font-weight: 700; background: var(--accent, #3b82f6); color: #fff;
|
||||||
border-radius: 999px; padding: 1px 8px; min-width: 1.2em; text-align: center;
|
border-radius: 999px; padding: 1px 8px; min-width: 1.2em; text-align: center;
|
||||||
}
|
}
|
||||||
.rp-count:empty { display: none; }
|
.rp-count:empty { display: none; }
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
text-decoration: none; color: inherit;
|
text-decoration: none; color: inherit;
|
||||||
}
|
}
|
||||||
.rp-item:hover, .rp-item:focus { background: rgba(127, 127, 127, .15); outline: none; }
|
.rp-item:hover, .rp-item:focus { background: rgba(127, 127, 127, .15); outline: none; }
|
||||||
.rp-item:focus-visible { outline: 2px solid #3b82f6; }
|
.rp-item:focus-visible { outline: 2px solid var(--accent, #3b82f6); }
|
||||||
.rp-title { display: block; font-weight: 600; }
|
.rp-title { display: block; font-weight: 600; }
|
||||||
.rp-context { display: block; font-size: .8rem; opacity: .65; }
|
.rp-context { display: block; font-size: .8rem; opacity: .65; }
|
||||||
.rp-snippet { display: block; font-size: .88rem; opacity: .85; margin-top: 2px; }
|
.rp-snippet { display: block; font-size: .88rem; opacity: .85; margin-top: 2px; }
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
/* ---- Onglets (chantier 2+3) : mêmes onglets sur les 4 hôtes. ---- */
|
/* ---- Onglets (chantier 2+3) : mêmes onglets sur les 4 hôtes. ---- */
|
||||||
.rp-tabs {
|
.rp-tabs {
|
||||||
display: flex; gap: 4px; flex-wrap: wrap;
|
display: flex; gap: 4px; flex-wrap: wrap;
|
||||||
border-bottom: 1px solid #888; margin-bottom: 20px;
|
border-bottom: 1px solid var(--border, #888); margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
.rp-tab {
|
.rp-tab {
|
||||||
background: none; border: none; border-bottom: 2px solid transparent;
|
background: none; border: none; border-bottom: 2px solid transparent;
|
||||||
@@ -45,8 +45,8 @@
|
|||||||
color: inherit; opacity: .7; white-space: nowrap;
|
color: inherit; opacity: .7; white-space: nowrap;
|
||||||
}
|
}
|
||||||
.rp-tab:hover { opacity: 1; }
|
.rp-tab:hover { opacity: 1; }
|
||||||
.rp-tab.active { opacity: 1; border-bottom-color: #3b82f6; font-weight: 700; }
|
.rp-tab.active { opacity: 1; border-bottom-color: var(--accent, #3b82f6); font-weight: 700; }
|
||||||
.rp-tab:focus-visible { outline: 2px solid #3b82f6; outline-offset: 2px; }
|
.rp-tab:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: 2px; }
|
||||||
.rp-tab-count {
|
.rp-tab-count {
|
||||||
font-size: .8rem; font-weight: 700; background: rgba(127, 127, 127, .25);
|
font-size: .8rem; font-weight: 700; background: rgba(127, 127, 127, .25);
|
||||||
border-radius: 999px; padding: 1px 7px; margin-left: 2px;
|
border-radius: 999px; padding: 1px 7px; margin-left: 2px;
|
||||||
@@ -88,3 +88,24 @@
|
|||||||
background: rgba(127, 127, 127, .18); font-weight: 700;
|
background: rgba(127, 127, 127, .18); font-weight: 700;
|
||||||
}
|
}
|
||||||
.rp-og-note { opacity: .65; font-size: .85rem; padding: 8px 10px; margin: 8px 0 0; }
|
.rp-og-note { opacity: .65; font-size: .85rem; padding: 8px 10px; margin: 8px 0 0; }
|
||||||
|
|
||||||
|
/* ---- Thème (défaut 4) : la page suit le réglage ObsiGate. ----
|
||||||
|
* L'étape 1 (script synchrone en tête de recherche.html) pose
|
||||||
|
* data-theme = mode persisté ; l'étape 2 (themes.js) pose la palette
|
||||||
|
* complète en variables inline sur :root. Les règles .rp-* ci-dessus
|
||||||
|
* utilisent donc var(--border) / var(--accent) avec les anciennes
|
||||||
|
* valeurs en repli, et le reste hérite déjà du corps (style.css).
|
||||||
|
* color-scheme : les contrôles natifs (barres de défilement, champ
|
||||||
|
* de recherche) suivent le mode au lieu de celui de l'OS.
|
||||||
|
* Barre : draco-barre.css (fichier commun vendangé, NON modifié ici)
|
||||||
|
* ne connaît que [data-theme="dark"] ; sans les règles ci-dessous,
|
||||||
|
* sa media query prefers-color-scheme:dark imposerait une barre
|
||||||
|
* sombre sur une page claire (OS sombre + mode ObsiGate clair), et
|
||||||
|
* les modes high-contrast/sepia n'auraient aucune déclinaison.
|
||||||
|
* Les valeurs reprennent les défauts clairs de draco-barre.css et
|
||||||
|
* les générateurs _genHighContrast/_genSepia de themes.js. */
|
||||||
|
[data-theme="dark"], [data-theme="high-contrast"] { color-scheme: dark; }
|
||||||
|
[data-theme="light"], [data-theme="sepia"] { color-scheme: light; }
|
||||||
|
[data-theme="light"] .draco-bar { --bg: #f2f2f0; --surface: #ffffff; --surface-2: #e9eae6; --surface-3: #f6f6f3; --ink: #1b1d1a; --ink-2: #5c615a; --ink-3: #787d75; --line: #dedfd9; }
|
||||||
|
[data-theme="high-contrast"] .draco-bar { --bg: #000000; --surface: #0a0a0a; --surface-2: #141414; --surface-3: #000000; --ink: #ffffff; --ink-2: #cccccc; --ink-3: #888888; --line: #444444; }
|
||||||
|
[data-theme="sepia"] .draco-bar { --bg: #f5ecd5; --surface: #eee3c8; --surface-2: #e8dcc0; --surface-3: #f5ecd5; --ink: #3d2e1a; --ink-2: #6b5a3e; --ink-3: #9a8a6a; --line: #c8b890; }
|
||||||
|
|||||||
+64
-7
@@ -14,15 +14,41 @@
|
|||||||
retomberait sur les défauts prod (les jetons sont ignorés par
|
retomberait sur les défauts prod (les jetons sont ignorés par
|
||||||
le JS) — jamais d'URL inventée.
|
le JS) — jamais d'URL inventée.
|
||||||
La barre ci-dessous est une copie statique du snippet figé de
|
La barre ci-dessous est une copie statique du snippet figé de
|
||||||
index.html (commons v0.1.6) : les onglets sont des liens réels,
|
index.html (commons v0.1.6 + chantier 5 fusion #69/#71, sans
|
||||||
le bouton central replace le curseur dans la boîte (recherche.js,
|
bump) : les onglets sont des liens réels, le bouton central
|
||||||
clic data-act="palette"). Les menus, le panneau Spark, les
|
replace le curseur dans la boîte (recherche.js,
|
||||||
alertes et le domaine restent des comportements de la SPA et ne
|
clic data-act="palette"). Re-vendoring alignement recherche :
|
||||||
sont pas rejoués ici — l'autre travailleur (onglet Console Spark
|
5e onglet Console Spark (data-app="agent", jeton {{URL_AGENT}}
|
||||||
de la barre) devra reporter son ajout dans cette copie au
|
remplacé par la route /recherche comme les 4 autres) et
|
||||||
re-vendoring, comme pour tout hôte.
|
icône SVG du bouton Alertes, repris d'index.html. Les menus,
|
||||||
|
le panneau Spark, les alertes et le domaine restent des
|
||||||
|
comportements de la SPA et ne sont pas rejoués ici.
|
||||||
Sidebar statique (navigation de l'app) : les vaults, tags et
|
Sidebar statique (navigation de l'app) : les vaults, tags et
|
||||||
récents dynamiques vivent dans la SPA, pas sur cette page. -->
|
récents dynamiques vivent dans la SPA, pas sur cette page. -->
|
||||||
|
<script>
|
||||||
|
/* Thème ObsiGate, étape 1 (synchrone, avant les feuilles) : poser
|
||||||
|
data-theme = mode persisté AVANT la première peinture, sinon la
|
||||||
|
page flasherait en sombre puis basculerait. Sources lues :
|
||||||
|
'obsigate-theme-mode' (SPA modulaire, js/ui.js initTheme) puis,
|
||||||
|
à défaut, 'obsigate-theme' quand il vaut déjà un mode (valeur
|
||||||
|
historique de l'app monolithique) ; toute autre valeur — clé de
|
||||||
|
thème comme 'dracula', mode inconnu, stockage indisponible —
|
||||||
|
retombe sur 'dark', le défaut SPA. La palette complète (clé)
|
||||||
|
est appliquée à l'étape 2 en fin de page. */
|
||||||
|
(function () {
|
||||||
|
var MODES = ['dark', 'light', 'high-contrast', 'sepia'];
|
||||||
|
var mode = null;
|
||||||
|
try { mode = window.localStorage.getItem('obsigate-theme-mode'); } catch (e) { mode = null; }
|
||||||
|
if (MODES.indexOf(mode) === -1) {
|
||||||
|
try {
|
||||||
|
var legacy = window.localStorage.getItem('obsigate-theme');
|
||||||
|
if (MODES.indexOf(legacy) !== -1) { mode = legacy; }
|
||||||
|
} catch (e) { /* stockage indisponible : repli sombre */ }
|
||||||
|
}
|
||||||
|
if (MODES.indexOf(mode) === -1) { mode = 'dark'; }
|
||||||
|
document.documentElement.setAttribute('data-theme', mode);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
<link rel="stylesheet" href="/static/style.css">
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
<link rel="stylesheet" href="/static/draco-barre.css">
|
<link rel="stylesheet" href="/static/draco-barre.css">
|
||||||
<link rel="stylesheet" href="/static/recherche.css">
|
<link rel="stylesheet" href="/static/recherche.css">
|
||||||
@@ -53,6 +79,10 @@
|
|||||||
<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="{{URL_AGENT}}">
|
||||||
|
<span class="draco-apptab-ico" style="--app:#c2437e">✦</span>
|
||||||
|
<span class="draco-apptab-txt"><strong>Console Spark</strong><small>Agent commun</small></span>
|
||||||
|
</a>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
<div class="draco-bar-zone draco-bar-centre">
|
<div class="draco-bar-zone draco-bar-centre">
|
||||||
@@ -70,6 +100,7 @@
|
|||||||
<button type="button" data-act="domaine" data-dom="perso">Perso</button>
|
<button type="button" data-act="domaine" data-dom="perso">Perso</button>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="draco-alertes" data-act="alertes" aria-label="Alertes unifiées">
|
<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-alertes-txt">Alertes</span>
|
||||||
<span class="draco-badge" data-badge="alertes" hidden>0</span>
|
<span class="draco-badge" data-badge="alertes" hidden>0</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -142,5 +173,31 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="/static/js/recherche.js"></script>
|
<script src="/static/js/recherche.js"></script>
|
||||||
|
<script type="module">
|
||||||
|
/* Thème ObsiGate, étape 2 (différée) : appliquer la palette
|
||||||
|
complète via le moteur de thèmes de la SPA (themes.js), seule
|
||||||
|
source de vérité des 16 thèmes, des modes high-contrast/sepia
|
||||||
|
et des thèmes personnalisés — aucune table recopiée ici.
|
||||||
|
initThemes() relit localStorage et pose les variables inline
|
||||||
|
sur :root + data-theme = mode, comme au démarrage de la SPA
|
||||||
|
(js/sync.js init). Garde historique : si 'obsigate-theme'
|
||||||
|
vaut encore un mode ('dark'/'light', app monolithique) sans
|
||||||
|
'obsigate-theme-mode', on garde le mode de l'étape 1 —
|
||||||
|
initThemes() y verrait une clé inconnue et retomberait sur
|
||||||
|
'spark'/'dark'. Échec de chargement silencieux : la page
|
||||||
|
garde alors le mode de l'étape 1, recherche.js n'est pas
|
||||||
|
touché (fichier partagé avec les 3 autres hôtes). */
|
||||||
|
(function () {
|
||||||
|
var saved = null;
|
||||||
|
var savedMode = null;
|
||||||
|
try { saved = window.localStorage.getItem('obsigate-theme'); } catch (e) {}
|
||||||
|
try { savedMode = window.localStorage.getItem('obsigate-theme-mode'); } catch (e) {}
|
||||||
|
if (savedMode === null && (saved === 'dark' || saved === 'light')) { return; }
|
||||||
|
import('/static/js/themes.js').then(function (mod) {
|
||||||
|
if (mod && typeof mod.initThemes === 'function') { mod.initThemes(); }
|
||||||
|
}).catch(function () {});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "obsigate",
|
"name": "obsigate",
|
||||||
"version": "2.82.1",
|
"version": "2.82.4",
|
||||||
"description": "**Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive.",
|
"description": "**Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive.",
|
||||||
"main": "patch.js",
|
"main": "patch.js",
|
||||||
"directories": {
|
"directories": {
|
||||||
|
|||||||
@@ -141,4 +141,39 @@ 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({ 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");
|
ok(rp.resolveConsoleBase({}) === "https://agent.dracodev.net/", "console : défaut prod");
|
||||||
|
|
||||||
|
|
||||||
|
/* ---- Alignement console (agent-commun /api/v1/recherche) ---- */
|
||||||
|
let rc;
|
||||||
|
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)");
|
||||||
|
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.mjs : ${n} assertions OK`);
|
console.log(`recherche.test.mjs : ${n} assertions OK`);
|
||||||
|
|||||||
@@ -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,51 @@
|
|||||||
|
"""Tests — page /recherche sans texte parasite dans <head>.
|
||||||
|
|
||||||
|
Régression du texte de chantier resté hors commentaire dans
|
||||||
|
``frontend/recherche.html`` : le commentaire de tête se fermait, puis
|
||||||
|
un paragraphe de description du mécanisme de thème suivait en texte
|
||||||
|
brut dans ``<head>`` — le navigateur le rendait tout en haut de la
|
||||||
|
page, au-dessus de la barre header. Le correctif supprime ce texte
|
||||||
|
du rendu (il n'est pas remis en commentaire : un grep doit donner
|
||||||
|
0 occurrence dans les gabarits) sans toucher au mécanisme lui-même,
|
||||||
|
déjà documenté par les commentaires des scripts étape 1 / étape 2.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
FRONTEND = ROOT / "frontend"
|
||||||
|
|
||||||
|
|
||||||
|
def _html() -> str:
|
||||||
|
return (FRONTEND / "recherche.html").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def test_pas_de_texte_brut_dans_head():
|
||||||
|
"""Entre les balises et les scripts, <head> ne contient aucun
|
||||||
|
texte rendu : tout descriptif vit dans un commentaire ou dans un
|
||||||
|
commentaire de script, jamais en texte brut."""
|
||||||
|
head = _html().split("</head>")[0]
|
||||||
|
sans_commentaires = re.sub(r"<!--.*?-->", "", head, flags=re.DOTALL)
|
||||||
|
sans_scripts = re.sub(
|
||||||
|
r"<script.*?</script>", "", sans_commentaires, flags=re.DOTALL
|
||||||
|
)
|
||||||
|
sans_titre = re.sub(
|
||||||
|
r"<title>.*?</title>", "", sans_scripts, flags=re.DOTALL
|
||||||
|
)
|
||||||
|
texte = re.sub(r"<[^>]+>", "", sans_titre).strip()
|
||||||
|
assert texte == "", f"texte brut rendu dans <head> : {texte[:200]!r}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_mecanisme_theme_intact():
|
||||||
|
"""Le retrait du texte ne touche ni l'étape 1 (script synchrone
|
||||||
|
avant les feuilles) ni l'étape 2 (initThemes différé)."""
|
||||||
|
html = _html()
|
||||||
|
assert html.index("<script>") < html.index('<link rel="stylesheet"')
|
||||||
|
assert "obsigate-theme-mode" in html
|
||||||
|
assert "import('/static/js/themes.js')" in html
|
||||||
|
assert "initThemes" in html
|
||||||
|
assert '<html lang="fr" data-theme="dark">' in html
|
||||||
|
assert '<script src="/static/js/recherche.js"></script>' in html
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
"""Tests — thème de la page /recherche ObsiGate (défaut 4).
|
||||||
|
|
||||||
|
Mécanisme audité (ne pas réinventer) : le thème ObsiGate n'est PAS une
|
||||||
|
préférence serveur. La SPA le lit dans localStorage au démarrage —
|
||||||
|
``frontend/js/ui.js`` ``initTheme`` (mode seul, pour la première
|
||||||
|
peinture) puis ``frontend/js/sync.js`` ``init`` -> ``themes.js``
|
||||||
|
``initThemes`` (clé + mode, palette complète en variables inline sur
|
||||||
|
``:root``). Clés : ``obsigate-theme`` (clé, ex. ``'spark'``,
|
||||||
|
``'dracula'``, ou historiquement un mode) et ``obsigate-theme-mode``
|
||||||
|
(``'dark'`` | ``'light'`` | ``'high-contrast'`` | ``'sepia'``).
|
||||||
|
|
||||||
|
La page /recherche, servie hors SPA avec un ``data-theme="dark"`` en
|
||||||
|
dur et aucun script de thème, restait donc sombre quel que soit le
|
||||||
|
réglage. Correctif verrouillé ici, en deux étapes dans
|
||||||
|
``frontend/recherche.html`` (aucun fichier partagé touché :
|
||||||
|
``js/recherche.js`` reste identique sur les 4 hôtes, ``draco-barre.css``
|
||||||
|
reste le fichier commun vendangé, ``backend/main.py`` et sa CSP
|
||||||
|
strictes sont inchangés) :
|
||||||
|
|
||||||
|
- étape 1 : script synchrone en tête, avant les feuilles — pose
|
||||||
|
``data-theme`` = mode persisté avant la première peinture ;
|
||||||
|
- étape 2 : module différé — ``initThemes()`` de ``themes.js`` pose la
|
||||||
|
palette complète (16 thèmes, modes générés, thèmes personnalisés).
|
||||||
|
|
||||||
|
``frontend/recherche.css`` décline la barre pour les modes que
|
||||||
|
``draco-barre.css`` ne connaît pas et branche bordures/accents sur
|
||||||
|
``var(--border)`` / ``var(--accent)`` (anciennes valeurs en repli).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
FRONTEND = ROOT / "frontend"
|
||||||
|
MODES = ["dark", "light", "high-contrast", "sepia"]
|
||||||
|
NONCE = "TESTNONCE1234567890"
|
||||||
|
|
||||||
|
|
||||||
|
def _html() -> str:
|
||||||
|
return (FRONTEND / "recherche.html").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def _css() -> str:
|
||||||
|
return (FRONTEND / "recherche.css").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def _inline_scripts(html: str) -> list[str]:
|
||||||
|
"""Contenu des scripts inline exécutables (sans src)."""
|
||||||
|
return re.findall(
|
||||||
|
r"<script(?: type=\"module\")?>(.*?)</script>", html, re.DOTALL
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_etape1_avant_les_feuilles_et_mode_valide():
|
||||||
|
html = _html()
|
||||||
|
scripts = _inline_scripts(html)
|
||||||
|
assert scripts, "aucun script inline dans recherche.html"
|
||||||
|
etape1 = scripts[0]
|
||||||
|
assert "obsigate-theme-mode" in etape1
|
||||||
|
assert "obsigate-theme" in etape1
|
||||||
|
assert "setAttribute('data-theme'" in etape1 or 'setAttribute("data-theme"' in etape1
|
||||||
|
for mode in MODES:
|
||||||
|
assert f"'{mode}'" in etape1, f"mode {mode} absent de l'étape 1"
|
||||||
|
# Le script étape 1 précède la première feuille de style.
|
||||||
|
assert html.index("<script>") < html.index('<link rel="stylesheet"')
|
||||||
|
# Repli sans JS : sombre, le défaut SPA.
|
||||||
|
assert '<html lang="fr" data-theme="dark">' in html
|
||||||
|
|
||||||
|
|
||||||
|
def test_etape1_ignore_les_cles_de_theme():
|
||||||
|
"""Une clé ('dracula', 'spark', …) ne doit jamais devenir un mode."""
|
||||||
|
etape1 = _inline_scripts(_html())[0]
|
||||||
|
# La validation passe par une liste fermée de modes, jamais par la
|
||||||
|
# valeur brute de 'obsigate-theme'.
|
||||||
|
assert "MODES.indexOf(mode)" in etape1
|
||||||
|
assert "MODES.indexOf(legacy)" in etape1
|
||||||
|
assert "'dark'" in etape1 # repli final
|
||||||
|
|
||||||
|
|
||||||
|
def test_etape2_importe_themes_js_avec_garde_historique():
|
||||||
|
html = _html()
|
||||||
|
modules = re.findall(
|
||||||
|
r'<script type="module">(.*?)</script>', html, re.DOTALL
|
||||||
|
)
|
||||||
|
assert len(modules) == 1, "exactement un module inline attendu"
|
||||||
|
etape2 = modules[0]
|
||||||
|
assert "import('/static/js/themes.js')" in etape2
|
||||||
|
assert "initThemes" in etape2
|
||||||
|
# Garde historique : 'obsigate-theme' valant encore un mode sans
|
||||||
|
# 'obsigate-theme-mode' -> on garde le mode de l'étape 1.
|
||||||
|
assert "savedMode === null" in etape2
|
||||||
|
assert "saved === 'dark'" in etape2 and "saved === 'light'" in etape2
|
||||||
|
# Échec de chargement silencieux : la page garde le mode étape 1.
|
||||||
|
assert ".catch(function () {})" in etape2
|
||||||
|
# Le module est après recherche.js (différé : il tourne avant
|
||||||
|
# l'init DOMContentLoaded de recherche.js, sans le modifier).
|
||||||
|
assert html.index("/static/js/recherche.js") < html.index(
|
||||||
|
'<script type="module">'
|
||||||
|
)
|
||||||
|
# themes.js existe bien à l'URL importée et exporte initThemes.
|
||||||
|
themes = (FRONTEND / "js" / "themes.js").read_text(encoding="utf-8")
|
||||||
|
assert "initThemes" in themes
|
||||||
|
assert "AVAILABLE_MODES = ['dark', 'light', 'high-contrast', 'sepia']" in themes
|
||||||
|
|
||||||
|
|
||||||
|
def test_nonce_csp_pour_les_deux_etapes():
|
||||||
|
"""Comme /recherche injecte window.DRACO_URLS avant </head>, les
|
||||||
|
trois scripts inline doivent tous recevoir le nonce frais."""
|
||||||
|
from backend.csp import inject_csp_nonce
|
||||||
|
|
||||||
|
html = _html().replace(
|
||||||
|
"</head>",
|
||||||
|
'<script>window.DRACO_URLS = {};</script></head>',
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
out = inject_csp_nonce(html, NONCE)
|
||||||
|
assert not re.findall(r"<script>", out), "script classique sans nonce"
|
||||||
|
assert not re.findall(
|
||||||
|
r'<script type="module">', out
|
||||||
|
), "module inline sans nonce"
|
||||||
|
assert out.count(f'nonce="{NONCE}"') == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_barre_decline_tous_les_modes():
|
||||||
|
"""draco-barre.css ne connaît que [data-theme="dark"] : les autres
|
||||||
|
modes sont déclinés dans recherche.css (fichier de la page), sans
|
||||||
|
toucher au fichier commun vendangé."""
|
||||||
|
css = _css()
|
||||||
|
barre = (FRONTEND / "draco-barre.css").read_text(encoding="utf-8")
|
||||||
|
assert '[data-theme="light"] .draco-bar' not in barre
|
||||||
|
for mode in ("light", "high-contrast", "sepia"):
|
||||||
|
assert f'[data-theme="{mode}"] .draco-bar' in css, mode
|
||||||
|
# Valeurs reprises des générateurs themes.js (_genHighContrast /
|
||||||
|
# _genSepia) : fond noir / sépia, encre blanche / brune.
|
||||||
|
assert "--surface: #0a0a0a" in css and "--ink: #ffffff" in css
|
||||||
|
assert "--surface: #eee3c8" in css and "--ink: #3d2e1a" in css
|
||||||
|
assert "color-scheme: dark" in css and "color-scheme: light" in css
|
||||||
|
|
||||||
|
|
||||||
|
def test_champs_onglets_resultats_suivent_accent_et_bordure():
|
||||||
|
css = _css()
|
||||||
|
assert "border: 1px solid var(--border, #888)" in css
|
||||||
|
assert "background: var(--accent, #3b82f6)" in css
|
||||||
|
assert "border-bottom-color: var(--accent, #3b82f6)" in css
|
||||||
|
assert "outline: 2px solid var(--accent, #3b82f6)" in css
|
||||||
|
|
||||||
|
|
||||||
|
def test_invariants_page_intacts():
|
||||||
|
"""Barre (5e onglet Console Spark), onglets, focus, 'Tout voir',
|
||||||
|
repli typographique : rien ne régresse."""
|
||||||
|
html = _html()
|
||||||
|
for app in ("obsigate", "flowdeck", "tiktak", "kanbante", "agent"):
|
||||||
|
assert f'data-app="{app}"' in html, app
|
||||||
|
assert "{{URL_AGENT}}" in html
|
||||||
|
for tab in ("all", "obsigate", "flowdeck", "tiktak", "kanbante", "console"):
|
||||||
|
assert f'data-rp-tab="{tab}"' in html, tab
|
||||||
|
assert "Tout voir" in html
|
||||||
|
assert 'id="rp-q"' in html and "autofocus" in html
|
||||||
|
assert '<script src="/static/js/recherche.js"></script>' in html
|
||||||
|
# Aucune police externe ajoutée : le repli système imposé par la
|
||||||
|
# CSP stricte de /recherche (font-src 'self' data:) est inchangé.
|
||||||
|
assert "fonts.googleapis.com" not in html
|
||||||
|
assert "fonts.gstatic.com" not in html
|
||||||
|
# Ordre des feuilles inchangé (recherche.css après la barre).
|
||||||
|
assert html.index("draco-barre.css") < html.index("recherche.css")
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
"""Tests — démarrage ObsiGate sans écran de chargement plein écran.
|
||||||
|
|
||||||
|
Le splash (``#boot-splash`` dans ``frontend/index.html`` : icône
|
||||||
|
livre, titre, spinner, « Démarrage… ») masquait tout le reste de la
|
||||||
|
page via ``body:not(.app-booted) > * { display: none !important }``
|
||||||
|
jusqu'au signal de ``frontend/js/app.js`` (ou un repli à 10 s).
|
||||||
|
Les autres applications (TikTak, Kanbante, Flowdeck, console Spark)
|
||||||
|
n'affichent aucun splash de ce type au démarrage : le contenu y
|
||||||
|
apparaît dès qu'il est prêt. Le retrait supprime donc l'écran, son
|
||||||
|
CSS de masquage, son script de pilotage et le signal côté ``app.js``,
|
||||||
|
sans toucher aux indicateurs internes aux vues ni à l'écran de
|
||||||
|
connexion.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
FRONTEND = ROOT / "frontend"
|
||||||
|
|
||||||
|
MARQUEURS_SPLASH = (
|
||||||
|
"boot-splash",
|
||||||
|
"app-booted",
|
||||||
|
"__OBSIGATE_BOOTED",
|
||||||
|
"__obsigateBootReady",
|
||||||
|
"__setBootStatus",
|
||||||
|
"bs-card",
|
||||||
|
"bs-logo",
|
||||||
|
"bs-title",
|
||||||
|
"bs-spinner",
|
||||||
|
"bs-status",
|
||||||
|
"bs-spin",
|
||||||
|
"Démarrage…",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _index() -> str:
|
||||||
|
return (FRONTEND / "index.html").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def _app_js() -> str:
|
||||||
|
return (FRONTEND / "js" / "app.js").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def test_aucun_marqueur_splash():
|
||||||
|
index = _index()
|
||||||
|
app_js = _app_js()
|
||||||
|
for marqueur in MARQUEURS_SPLASH:
|
||||||
|
assert marqueur not in index, f"{marqueur} encore dans index.html"
|
||||||
|
assert marqueur not in app_js, f"{marqueur} encore dans js/app.js"
|
||||||
|
|
||||||
|
|
||||||
|
def test_pas_de_masquage_global_au_demarrage():
|
||||||
|
"""Aucune règle ne cache tout le <body> en attendant un signal :
|
||||||
|
sans elle, un chargement lent ne peut plus donner un écran blanc
|
||||||
|
bloqué."""
|
||||||
|
index = _index()
|
||||||
|
assert "body:not(.app-booted)" not in index
|
||||||
|
assert "display: none !important" not in index
|
||||||
|
|
||||||
|
|
||||||
|
def test_boot_js_conserve_et_indicateurs_internes_intacts():
|
||||||
|
"""Le démarrage SPA reste branché ; seuls le splash et son signal
|
||||||
|
disparaissent. L'écran de connexion et la barre de progression
|
||||||
|
de recherche (indicateurs internes aux vues) ne sont PAS visés."""
|
||||||
|
app_js = _app_js()
|
||||||
|
assert 'document.addEventListener("DOMContentLoaded"' in app_js
|
||||||
|
assert "await init();" in app_js
|
||||||
|
assert "Sync.init();" in app_js
|
||||||
|
assert "populateVersions" in app_js
|
||||||
|
index = _index()
|
||||||
|
assert 'id="login-screen"' in index
|
||||||
|
assert 'id="search-progress-bar"' in index
|
||||||
|
assert 'id="app"' in index
|
||||||
Reference in New Issue
Block a user