Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4dfa8fd74c | ||
|
|
e709310e70 | ||
|
|
4d0c8ad934 | ||
|
|
b609e8913c | ||
|
|
f10e3d12e6 | ||
|
|
78c514c78b | ||
|
|
d20ae93b40 | ||
|
|
a885153a68 | ||
|
|
3edeb88c92 | ||
|
|
677b174f09 | ||
|
|
ab3d0798c7 | ||
|
|
332983c4d6 | ||
|
|
2c2f23dc57 | ||
|
|
097accc44c | ||
|
|
7ea5614093 |
@@ -14,6 +14,10 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
---
|
||||
|
||||
## [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
|
||||
|
||||
### Modifié
|
||||
|
||||
@@ -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 de ``backend.draco_config`` (``urls_publiques()`` :
|
||||
explicite > dérivation par l'hôte de l'agent > défaut officiel)
|
||||
— jamais ``*``. Correctif 2026-10-28 (fusion #68/#69, matrice
|
||||
@@ -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_config.urls_publiques() 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)
|
||||
|
||||
|
||||
|
||||
Generated
+1
-1
@@ -2626,7 +2626,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "obsigate-desktop"
|
||||
version = "2.82.1"
|
||||
version = "2.82.2"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"env_logger",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "obsigate-desktop"
|
||||
version = "2.82.1"
|
||||
version = "2.82.2"
|
||||
description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian"
|
||||
authors = ["Bruno Charest"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json",
|
||||
"productName": "ObsiGate",
|
||||
"version": "2.82.1",
|
||||
"version": "2.82.2",
|
||||
"identifier": "com.obsigate.desktop",
|
||||
"build": {
|
||||
"frontendDist": "../frontend",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# ObsiGate — Roadmap
|
||||
|
||||
> **Version :** 2.82.1 | **Dernière mise à jour :** 2026-10-28
|
||||
> **Version :** 2.82.2 | **Dernière mise à jour :** 2026-10-28
|
||||
> **Ce fichier ne contient que le travail à venir** (🔵 En cours + ⚪ Backlog) et un index compact
|
||||
> vers les fonctionnalités livrées.
|
||||
> - **Méthode de livraison à appliquer pour toute tâche : [DELIVERY_WORKFLOW.md](./DELIVERY_WORKFLOW.md)**
|
||||
|
||||
+29
-8
@@ -7,17 +7,17 @@
|
||||
.rp-form { display: flex; gap: 8px; margin-bottom: 24px; }
|
||||
.rp-input {
|
||||
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-head {
|
||||
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-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;
|
||||
}
|
||||
.rp-count:empty { display: none; }
|
||||
@@ -29,7 +29,7 @@
|
||||
text-decoration: none; color: inherit;
|
||||
}
|
||||
.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-context { display: block; font-size: .8rem; opacity: .65; }
|
||||
.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. ---- */
|
||||
.rp-tabs {
|
||||
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 {
|
||||
background: none; border: none; border-bottom: 2px solid transparent;
|
||||
@@ -45,8 +45,8 @@
|
||||
color: inherit; opacity: .7; white-space: nowrap;
|
||||
}
|
||||
.rp-tab:hover { opacity: 1; }
|
||||
.rp-tab.active { opacity: 1; border-bottom-color: #3b82f6; font-weight: 700; }
|
||||
.rp-tab:focus-visible { outline: 2px solid #3b82f6; outline-offset: 2px; }
|
||||
.rp-tab.active { opacity: 1; border-bottom-color: var(--accent, #3b82f6); font-weight: 700; }
|
||||
.rp-tab:focus-visible { outline: 2px solid var(--accent, #3b82f6); outline-offset: 2px; }
|
||||
.rp-tab-count {
|
||||
font-size: .8rem; font-weight: 700; background: rgba(127, 127, 127, .25);
|
||||
border-radius: 999px; padding: 1px 7px; margin-left: 2px;
|
||||
@@ -88,3 +88,24 @@
|
||||
background: rgba(127, 127, 127, .18); font-weight: 700;
|
||||
}
|
||||
.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; }
|
||||
|
||||
@@ -25,6 +25,46 @@
|
||||
comportements de la SPA et ne sont pas rejoués ici.
|
||||
Sidebar statique (navigation de l'app) : les vaults, tags et
|
||||
récents dynamiques vivent dans la SPA, pas sur cette page. -->
|
||||
Thème (défaut 4) : le thème ObsiGate n'est PAS une préférence
|
||||
serveur — il vit dans localStorage, lu par la SPA au démarrage
|
||||
(frontend/js/ui.js initTheme : mode ; frontend/js/sync.js
|
||||
init() -> themes.js initThemes : clé + mode). Étape 1 ci-dessous
|
||||
(script synchrone en tête, avant les feuilles) : pose
|
||||
data-theme = mode persisté (obsigate-theme-mode, repli
|
||||
obsigate-theme historique quand il valait déjà un mode, sinon
|
||||
'dark', le défaut SPA) pour que la première peinture soit déjà
|
||||
dans le bon mode clair/sombre. Étape 2 en fin de page (module
|
||||
différé) : initThemes() de themes.js applique la palette
|
||||
complète — 16 thèmes intégrés, modes high-contrast/sepia
|
||||
générés, thèmes personnalisés — via variables inline sur :root,
|
||||
exactement comme la SPA. data-theme="dark" sur <html> n'est
|
||||
plus que le repli sans JS. Aucun endpoint serveur, aucune
|
||||
police externe ajoutée (la CSP stricte de /recherche et le
|
||||
repli typographique système sont inchangés).
|
||||
<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/draco-barre.css">
|
||||
<link rel="stylesheet" href="/static/recherche.css">
|
||||
@@ -149,5 +189,31 @@
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
</html>
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "obsigate",
|
||||
"version": "2.82.1",
|
||||
"version": "2.82.2",
|
||||
"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",
|
||||
"directories": {
|
||||
|
||||
@@ -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,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")
|
||||
Reference in New Issue
Block a user