Compare commits

..
Author SHA1 Message Date
bruno 8403b4fa08 chore(release): v2.82.4 — CHANGELOG.md
CI / lint (push) Failing after 2s
CI / security (push) Failing after 1s
CI / test (push) Skipped
CI / build (push) Skipped
CI / e2e (push) Skipped
2026-10-28 18:25:02 -04:00
bruno 757e32229d chore(release): v2.82.4 — docs/ROADMAP.md
CI / lint (push) Failing after 3s
CI / security (push) Failing after 0s
CI / test (push) Skipped
CI / build (push) Skipped
CI / e2e (push) Skipped
2026-10-28 18:24:54 -04:00
bruno 4c995f4c65 chore(release): v2.82.4 — desktop/Cargo.lock
CI / lint (push) Failing after 1s
CI / test (push) Skipped
CI / build (push) Skipped
CI / e2e (push) Skipped
CI / security (push) Successful in 2m4s
2026-10-28 18:24:49 -04:00
bruno c950940e22 chore(release): v2.82.4 — desktop/Cargo.toml
CI / security (push) Failing after 2s
CI / lint (push) Failing after 1s
CI / test (push) Skipped
CI / build (push) Skipped
CI / e2e (push) Skipped
2026-10-28 18:24:41 -04:00
bruno 858dfd22e1 chore(release): v2.82.4 — desktop/tauri.conf.json
CI / lint (push) Failing after 2s
CI / security (push) Failing after 1s
CI / test (push) Skipped
CI / build (push) Skipped
CI / e2e (push) Skipped
2026-10-28 18:24:36 -04:00
bruno ab4a4dbd9d chore(release): v2.82.4 — package.json
CI / lint (push) Failing after 3s
CI / test (push) Skipped
CI / build (push) Skipped
CI / e2e (push) Skipped
CI / security (push) Successful in 2m5s
2026-10-28 18:24:29 -04:00
bruno 0bc1aca9bc chore(release): v2.82.4 — VERSION
CI / security (push) Failing after 2s
CI / lint (push) Failing after 2s
CI / test (push) Skipped
CI / build (push) Skipped
CI / e2e (push) Skipped
2026-10-28 18:24:24 -04:00
bruno 515727ab9b Merge pull request 'Retire l'ecran de chargement plein ecran au demarrage d'ObsiGate' (#53) from fix/retrait-splash-demarrage into main
CI / security (push) Failing after 0s
CI / lint (push) Failing after 1m51s
CI / test (push) Skipped
CI / build (push) Skipped
CI / e2e (push) Skipped
2026-10-28 18:22:24 -04:00
bruno 3f375bca2a Merge pull request 'Retire le texte de chantier theme rendu en haut de /recherche' (#52) from fix/recherche-texte-theme-parasite into main
CI / security (push) Failing after 1s
CI / lint (push) Failing after 1m53s
CI / test (push) Skipped
CI / build (push) Skipped
CI / e2e (push) Skipped
2026-10-28 18:22:09 -04:00
bruno 483af904af Test non-regression : aucun splash au demarrage, boot JS et indicateurs internes intacts
CI / lint (pull_request) Failing after 0s
CI / test (pull_request) Skipped
CI / build (pull_request) Skipped
CI / e2e (pull_request) Skipped
CI / security (pull_request) Failing after 0s
2026-10-28 16:44:13 -04:00
bruno bfe0c75106 Retire le signal de boot vers le splash supprime (DOMContentLoaded simplifie) 2026-10-28 16:44:11 -04:00
bruno 2a96ad3b1d Retire l'ecran de chargement plein ecran au demarrage (splash, CSS de masquage, script de pilotage) 2026-10-28 16:44:08 -04:00
bruno 12a29b29f6 Test non-regression : aucun texte brut rendu dans le head de /recherche
CI / security (pull_request) Successful in 1m57s
CI / lint (pull_request) Failing after 2m17s
CI / test (pull_request) Skipped
CI / build (pull_request) Skipped
CI / e2e (pull_request) Skipped
2026-10-28 16:43:35 -04:00
bruno 8feab0188c Retire le texte de chantier theme rendu en haut de /recherche (lignes hors commentaire) 2026-10-28 16:43:31 -04:00
bruno fc44efabbb Changelog 2.82.3 : thème /recherche, CORS console (PR #49/#51)
CI / lint (push) Failing after 1s
CI / test (push) Skipped
CI / build (push) Skipped
CI / e2e (push) Skipped
CI / security (push) Failing after 1s
2026-10-28 16:22:51 -04:00
bruno 3ed09cbc0f Bump 2.82.3
CI / lint (push) Failing after 0s
CI / test (push) Skipped
CI / build (push) Skipped
CI / security (push) Failing after 0s
CI / e2e (push) Skipped
2026-10-28 16:22:48 -04:00
bruno a4f27bdbe7 Bump 2.82.3
CI / lint (push) Failing after 1s
CI / security (push) Failing after 0s
CI / test (push) Skipped
CI / build (push) Skipped
CI / e2e (push) Skipped
2026-10-28 16:22:44 -04:00
bruno 05d1903045 Bump 2.82.3
CI / lint (push) Failing after 0s
CI / test (push) Skipped
CI / build (push) Skipped
CI / e2e (push) Skipped
CI / security (push) Failing after 1s
2026-10-28 16:22:39 -04:00
bruno b5b4a00b75 Bump 2.82.3
CI / lint (push) Failing after 0s
CI / security (push) Failing after 0s
CI / test (push) Skipped
CI / build (push) Skipped
CI / e2e (push) Skipped
2026-10-28 16:22:35 -04:00
bruno 97f997840c Bump 2.82.3
CI / lint (push) Failing after 2s
CI / test (push) Skipped
CI / build (push) Skipped
CI / security (push) Failing after 0s
CI / e2e (push) Skipped
2026-10-28 16:22:31 -04:00
bruno 474851b3bb Bump 2.82.3
CI / lint (push) Failing after 2s
CI / security (push) Failing after 2s
CI / test (push) Skipped
CI / build (push) Skipped
CI / e2e (push) Skipped
2026-10-28 16:22:26 -04:00
bruno 4dfa8fd74c Merge pull request 'CORS recherche : autoriser la console Spark (agent prod+dev)' (#51) from federation-recherche-console into main
CI / lint (push) Failing after 0s
CI / test (push) Skipped
CI / build (push) Skipped
CI / e2e (push) Skipped
CI / security (push) Failing after 1s
2026-10-28 16:17:07 -04:00
bruno e709310e70 Merge pull request 'recherche: appliquer le thème ObsiGate à la page /recherche (défaut 4)' (#49) from obsigate-recherche-theme into main
CI / lint (push) Failing after 0s
CI / security (push) Failing after 1s
CI / test (push) Skipped
CI / build (push) Skipped
CI / e2e (push) Skipped
2026-10-28 16:17:05 -04:00
bruno f10e3d12e6 recherche: tests du theme de la page (defaut 4)
CI / lint (pull_request) Failing after 1s
CI / test (pull_request) Skipped
CI / build (pull_request) Skipped
CI / e2e (pull_request) Skipped
CI / security (pull_request) Successful in 1m57s
2026-10-28 15:59:18 -04:00
bruno 78c514c78b recherche: decliner la barre et les accents selon le theme (defaut 4) 2026-10-28 15:59:13 -04:00
bruno d20ae93b40 recherche: appliquer le theme ObsiGate (defaut 4) 2026-10-28 15:59:08 -04:00
14 changed files with 392 additions and 91 deletions
+11 -1
View File
@@ -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).
> **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,16 @@ 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.
+1 -1
View File
@@ -1 +1 @@
2.82.2
2.82.4
+1 -1
View File
@@ -2626,7 +2626,7 @@ dependencies = [
[[package]]
name = "obsigate-desktop"
version = "2.82.2"
version = "2.82.4"
dependencies = [
"chrono",
"env_logger",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "obsigate-desktop"
version = "2.82.2"
version = "2.82.4"
description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian"
authors = ["Bruno Charest"]
edition = "2021"
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json",
"productName": "ObsiGate",
"version": "2.82.2",
"version": "2.82.4",
"identifier": "com.obsigate.desktop",
"build": {
"frontendDist": "../frontend",
+1 -1
View File
@@ -1,6 +1,6 @@
# ObsiGate — Roadmap
> **Version :** 2.82.2 | **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
> vers les fonctionnalités livrées.
> - **Méthode de livraison à appliquer pour toute tâche : [DELIVERY_WORKFLOW.md](./DELIVERY_WORKFLOW.md)**
+1 -66
View File
@@ -47,33 +47,6 @@
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/draco-barre.css" />
<link rel="stylesheet" href="/static/draco-alertes.css" />
@@ -89,7 +62,7 @@
id="hljs-theme-light"
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>
<!-- 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>
@@ -226,44 +199,6 @@
</script>
</head>
<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__fill"></div>
</div>
+2 -10
View File
@@ -120,16 +120,8 @@ async function init() {
}
document.addEventListener("DOMContentLoaded", async () => {
try {
await 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();
}
await init();
Sync.init();
// Show version in header — always visible, fallback before the async fetch
populateVersions();
fetch('/api/health')
+29 -8
View File
@@ -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; }
+50
View File
@@ -25,6 +25,30 @@
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. -->
<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 +173,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
View File
@@ -1,6 +1,6 @@
{
"name": "obsigate",
"version": "2.82.2",
"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.",
"main": "patch.js",
"directories": {
@@ -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
+167
View File
@@ -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")
+75
View File
@@ -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