Compare commits

...
Author SHA1 Message Date
bruno e0e0f9af0f Merge pull request 'ObsiGate 2.83.4 — barre v0.4.0 : icônes monoline dans le menu compte (HUB-11)' (#64) from hub11-barre-v040 into main
CI / lint (push) Successful in 1m24s
CI / security (push) Successful in 17s
CI / test (push) Successful in 3m35s
CI / build (push) Successful in 10s
CI / e2e (push) Successful in 16m38s
2026-11-05 00:33:34 -05:00
Spark 35b828a4c5 ObsiGate 2.83.4 : version et changelog (barre v0.4.0, HUB-11) — VERSION + fichiers dérivés synchronisés par scripts/bump_version.py
CI / lint (pull_request) Successful in 1m19s
CI / security (pull_request) Successful in 21s
CI / test (pull_request) Successful in 3m37s
CI / build (pull_request) Successful in 12s
CI / e2e (pull_request) Successful in 16m51s
2026-11-05 05:03:01 +00:00
Spark 6ebb3b3eae ObsiGate 2.83.4 : barre v0.4.0 — icônes monoline du menu compte, vendange verbatim (HUB-11) + garde 2026-11-05 05:02:10 +00:00
bruno 569d0e33a6 Merge pull request 'ObsiGate 2.83.3 : barre v0.3.0 — liens Hub du menu compte (HUB-1)' (#63) from hub1-barre-v030 into main
CI / lint (push) Successful in 1m31s
CI / security (push) Successful in 22s
CI / test (push) Successful in 3m55s
CI / build (push) Successful in 11s
CI / e2e (push) Successful in 17m28s
2026-11-04 19:48:18 -05:00
Spark 1d1ce5a848 ObsiGate 2.83.3 : barre v0.3.0, liens Hub du menu compte (VERSION + fichiers dérivés synchronisés)
CI / lint (pull_request) Successful in 1m18s
CI / security (pull_request) Successful in 18s
CI / test (pull_request) Successful in 3m45s
CI / build (pull_request) Successful in 12s
CI / e2e (pull_request) Successful in 16m20s
2026-11-04 21:57:41 +00:00
Spark f04369343d ObsiGate 2.83.3 : barre v0.3.0, liens Hub du menu compte (tests) 2026-11-04 21:57:41 +00:00
Spark 8290abd675 ObsiGate 2.83.3 : barre v0.3.0, liens Hub du menu compte (frontend + draco_config) 2026-11-04 21:57:41 +00:00
bruno ea60d7e172 Merge pull request 'ObsiGate 2.83.2 : recherche fédérée — ShaarIt, 5e source de « Rechercher partout »' (#62) from recherche-shaarit into main
CI / lint (push) Successful in 1m29s
CI / security (push) Successful in 18s
CI / test (push) Successful in 3m48s
CI / build (push) Successful in 46s
CI / e2e (push) Successful in 17m17s
2026-11-04 12:23:55 -05:00
Spark 606d59f483 ObsiGate 2.83.2 : recherche fédérée — ShaarIt, 5e source de « Rechercher partout »
CI / lint (pull_request) Successful in 1m25s
CI / security (pull_request) Successful in 20s
CI / test (pull_request) Successful in 4m4s
CI / build (pull_request) Failing after 10m20s
CI / e2e (pull_request) Skipped
- frontend/js/recherche.js re-vendangé verbatim depuis commons
  v0.2.1 (source canonique ui/recherche/) : source ShaarIt (bases,
  onglet, normaliseur — lien profond {base}shaare/<shorturl>).
- frontend/recherche.html : data-base-shaarit (jeton URL_SHAARIT),
  onglet et section « ShaarIt — bookmarks ».
- backend/recherche_cors.py : origines ShaarIt prod/dev + clé
  sœur ; backend/main.py : origine ShaarIt dans le connect-src
  CSP de la page /recherche.
- Version synchronisée par scripts/bump_version.py --set 2.83.2
  (VERSION, package.json, desktop, README, ROADMAP, CHANGELOG).
- Tests : recherche.test.mjs 67 assertions (5 sources),
  test_recherche_cors allowlist 12 origines ; suite backend
  complète 1916 passés / 0 échec, ruff et mypy verts.
2026-11-04 17:23:38 +00:00
bruno a046f286e6 Merge pull request 'ObsiGate 2.83.1 : barre v0.2.0 — onglet ShaarIt + garde data-theme' (#61) from barre-v020-2-83-1 into main
CI / lint (push) Successful in 1m59s
CI / security (push) Successful in 15s
CI / test (push) Successful in 3m55s
CI / build (push) Successful in 10s
CI / e2e (push) Successful in 22m11s
2026-11-04 11:16:29 -05:00
bruno 0a92a55efa ObsiGate 2.83.1 : barre v0.2.0, onglet ShaarIt + garde data-theme (VERSION + fichiers dérivés synchronisés)
CI / lint (pull_request) Successful in 1m19s
CI / security (pull_request) Successful in 22s
CI / test (pull_request) Successful in 3m29s
CI / build (pull_request) Successful in 15s
CI / e2e (pull_request) Successful in 16m49s
2026-11-04 03:38:18 +00:00
bruno 3c383559f4 ObsiGate 2.83.1 : barre v0.2.0, onglet ShaarIt + garde data-theme (tests/test_draco_config.py) 2026-11-04 03:38:18 +00:00
bruno 6253c7558a ObsiGate 2.83.1 : barre v0.2.0, onglet ShaarIt + garde data-theme (tests/test_draco_barre.py) 2026-11-04 03:38:18 +00:00
bruno 6835607f66 ObsiGate 2.83.1 : barre v0.2.0, onglet ShaarIt + garde data-theme (.env.example) 2026-11-04 03:38:18 +00:00
bruno 478749566a ObsiGate 2.83.1 : barre v0.2.0, onglet ShaarIt + garde data-theme (backend/main.py) 2026-11-04 03:38:18 +00:00
bruno d00c261ff1 ObsiGate 2.83.1 : barre v0.2.0, onglet ShaarIt + garde data-theme (backend/draco_config.py) 2026-11-04 03:38:18 +00:00
bruno 646a4977f5 ObsiGate 2.83.1 : barre v0.2.0, onglet ShaarIt + garde data-theme (frontend/recherche.html) 2026-11-04 03:38:18 +00:00
bruno 8683e1d724 ObsiGate 2.83.1 : barre v0.2.0, onglet ShaarIt + garde data-theme (frontend/index.html) 2026-11-04 03:38:18 +00:00
bruno 5a7f1b9bcf ObsiGate 2.83.1 : barre v0.2.0, onglet ShaarIt + garde data-theme (frontend/img/logos/shaarit.svg) 2026-11-04 03:38:18 +00:00
bruno 14089ff59e ObsiGate 2.83.1 : barre v0.2.0, onglet ShaarIt + garde data-theme (frontend/draco-barre.js) 2026-11-04 03:38:18 +00:00
bruno 0b0639ce61 ObsiGate 2.83.1 : barre v0.2.0, onglet ShaarIt + garde data-theme (frontend/draco-barre.css) 2026-11-04 03:38:18 +00:00
bruno 603ba976e1 Merge pull request 'feat: vault DIR — exclusions par vault et lecture seule déclarable (vault Spark)' (#58) from spark-vault-dir into main
CI / lint (push) Successful in 1m36s
CI / security (push) Successful in 22s
CI / test (push) Successful in 3m52s
CI / build (push) Successful in 18s
CI / e2e (push) Successful in 16m36s
2026-11-03 16:24:37 -05:00
Spark ad1cea23ab release: version 2.83.0 (synchronisation complète)
CI / lint (pull_request) Successful in 1m24s
CI / security (pull_request) Successful in 21s
CI / test (pull_request) Successful in 3m50s
CI / build (pull_request) Successful in 14s
CI / e2e (pull_request) Successful in 16m45s
2026-11-03 21:00:59 +00:00
bruno b5b5a0e820 chore: version 2.83.0 (vault DIR exclusions + fix CI e2e)
CI / lint (pull_request) Successful in 1m35s
CI / security (pull_request) Successful in 37s
CI / test (pull_request) Failing after 3m32s
CI / build (pull_request) Skipped
CI / e2e (pull_request) Skipped
2026-11-03 14:09:13 -05:00
Spark 60666bf79d chore: relance CI après nettoyage du runner 2026-11-03 14:09:13 -05:00
Spark 099ffe7e8f chore: relance CI (runner dégradé, run 3161 gelé) 2026-11-03 14:09:12 -05:00
Spark bd5507d6ef feat: vault DIR — garde d'exclusion sur les lectures xlsx (vault Spark) 2026-11-03 14:09:12 -05:00
Spark 3ba8b7b667 feat: vault DIR — exclusions par vault et lecture seule déclarable (vault Spark) 2026-11-03 14:09:12 -05:00
bruno 52dcbf3607 Merge pull request 'fix(ci): e2e DinD par DNS réseau — fin du blocage 'Start ObsiGate'' (#60) from fix/ci-e2e-resseau-dns into main
CI / lint (push) Successful in 1m40s
CI / security (push) Successful in 35s
CI / test (push) Successful in 4m25s
CI / build (push) Successful in 23s
CI / e2e (push) Successful in 16m57s
2026-11-03 14:08:36 -05:00
bruno e28a537f79 fix(e2e): z-index barre résultats au-dessus de la barre DRACO + signal de boot du spec .ts
CI / lint (pull_request) Successful in 1m19s
CI / security (pull_request) Successful in 20s
CI / test (pull_request) Successful in 3m35s
CI / build (pull_request) Successful in 22s
CI / e2e (pull_request) Successful in 16m22s
- obsigate.spec.ts attendait encore le flag __OBSIGATE_BOOTED retiré (même
  cause que les 8 suites js, corrigée ici) ;
- .results-bar (z 50) était masquée par .draco-spark-flottant (z 65, même
  position bas-centre) → « subtree intercepts pointer events » sur
  #rb-next/#rb-case : 2 tests en timeout de clic. Bug d'UI réel (l'utilisateur
  ne peut pas cliquer les flèches pendant qu'Spark flotte), corrigé par
  z-index 66. Validé local : 2 passed en 10 s (au lieu de 2×60 s de timeout).
2026-11-03 11:17:35 -05:00
bruno b87ac72d78 ci(wip): runs-on ci-deepin — contournement régression extraction moby 29.x du runner VM (à retirer quand daemon VM réparé)
CI / security (pull_request) Successful in 24s
CI / lint (pull_request) Successful in 1m39s
CI / test (pull_request) Successful in 3m36s
CI / build (pull_request) Successful in 14s
CI / e2e (pull_request) Failing after 30m53s
2026-11-03 09:59:18 -05:00
bruno ca367b6918 test(e2e): helpers ne dépendent plus du signal __OBSIGATE_BOOTED retiré (bfe0c75)
CI / security (pull_request) Failing after 2s
CI / lint (pull_request) Successful in 1m18s
CI / test (pull_request) Successful in 3m35s
CI / build (pull_request) Successful in 12s
CI / e2e (pull_request) Failing after 31m42s
bfe0c75 a retiré le splash et son signal de boot côté app sans adapter les
helpers des specs ; login() attendait window.__OBSIGATE_BOOTED → timeout
60 s × chaque test des 8 suites. Signal de rechange sans dépendance au
flag interne : document prêt ET premier .tree-item rendu (preuve que
l'init a lié données+handlers). Validé local : 14 passed sur les 3 suites
qui échouaient en CI (obsigate:ci v2.82.8, auth off, mêmes fixtures).
2026-11-03 09:26:42 -05:00
bruno 9037f2dd69 fix(ci): e2e joint l'app par DNS de réseau (DinD), plus de port publié ni curls sans timeout
CI / security (pull_request) Failing after 2s
CI / lint (pull_request) Successful in 1m35s
CI / test (pull_request) Successful in 3m47s
CI / build (pull_request) Successful in 15s
CI / e2e (pull_request) Failing after 30m52s
Le step 'Start ObsiGate' pendait : boucle de health curls sans --max-time
vers l'IP passerelle:2029 (injoignable/port fuité par un run antérieur) =
150 s de blocage par tentative. L'app est désormais rattachée au réseau du
job et jointe par nom (obsigate-e2e:8080), avec timeout partout et dump
des logs container à l'échec.
2026-11-03 08:44:08 -05:00
bruno 3a27fde176 Merge pull request 'fix: CI préexistante — imports coffres (ruff I001) + planchers pyasn1/pypdf' (#59) from fix-ci-main into main
CI / security (push) Failing after 1s
CI / lint (push) Successful in 1m37s
CI / test (push) Successful in 3m37s
CI / build (push) Successful in 10s
CI / e2e (push) Failing after 1h10m34s
2026-11-03 01:21:25 -05:00
Spark d3acae2bd8 fix: tests draco_barre — assertions alignées sur le code actuel (#222, #218)
CI / security (pull_request) Successful in 23s
CI / lint (pull_request) Successful in 1m16s
CI / test (pull_request) Successful in 3m58s
CI / build (pull_request) Successful in 1m4s
CI / e2e (pull_request) Successful in 17m19s
2026-11-03 05:56:41 +00:00
Spark ad0a715a4c fix: resynchroniser les fichiers dérivés sur la version 2.82.8 (dérive préexistante) 2026-11-03 05:56:41 +00:00
Spark f7095f8057 fix: test unitaire frontend — recherche.js exempté d'import/export (fichier vendangé #68)
CI / security (pull_request) Failing after 1s
CI / lint (pull_request) Successful in 1m36s
CI / test (pull_request) Failing after 3m47s
CI / build (pull_request) Skipped
CI / e2e (pull_request) Skipped
2026-11-03 05:48:53 +00:00
Spark 67fbda60d3 fix: planchers de sécurité pip-audit — pypdf>=6.21.4 et pyasn1>=0.6.5
CI / security (pull_request) Successful in 19s
CI / lint (pull_request) Failing after 25s
CI / test (pull_request) Skipped
CI / build (pull_request) Skipped
CI / e2e (pull_request) Skipped
2026-11-03 05:44:13 +00:00
Spark 5280b84d12 fix: tri des imports coffre_store dans routers/coffres (ruff I001) 2026-11-03 05:44:13 +00:00
bruno b42a81018c release: changelog 2.82.8
CI / security (push) Failing after 0s
CI / lint (push) Failing after 1m59s
CI / test (push) Skipped
CI / build (push) Skipped
CI / e2e (push) Skipped
2026-10-30 18:36:43 -04:00
bruno 5c85763de1 release: version 2.82.8
CI / security (push) Failing after 0s
CI / lint (push) Failing after 1m56s
CI / test (push) Skipped
CI / build (push) Skipped
CI / e2e (push) Skipped
2026-10-30 18:36:39 -04:00
bruno 2aacb60102 Merge pull request 'secrets: afficher/masquer + copier la valeur dans le panneau détail' (#57) from secrets-afficher-copier into main
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-30 18:36:07 -04:00
bruno c4247b8bbc secrets: afficher/masquer + copier la valeur, revelation gestionnaire auditee (og/COFFRE-contrat-distribution.md)
CI / lint (pull_request) Failing after 1s
CI / security (pull_request) Failing after 0s
CI / test (pull_request) Skipped
CI / build (pull_request) Skipped
CI / e2e (pull_request) Skipped
2026-10-30 18:34:21 -04:00
bruno c29bafeccc secrets: afficher/masquer + copier la valeur, revelation gestionnaire auditee (og/test_coffre_secrets.py) 2026-10-30 18:34:18 -04:00
bruno fe4bed91d7 secrets: afficher/masquer + copier la valeur, revelation gestionnaire auditee (og/en.json) 2026-10-30 18:34:15 -04:00
bruno 3abf908ca4 secrets: afficher/masquer + copier la valeur, revelation gestionnaire auditee (og/fr.json) 2026-10-30 18:34:12 -04:00
bruno 9c8e8f54dc secrets: afficher/masquer + copier la valeur, revelation gestionnaire auditee (og/coffres.js) 2026-10-30 18:34:09 -04:00
bruno 1e59781d77 secrets: afficher/masquer + copier la valeur, revelation gestionnaire auditee (og/coffres.py) 2026-10-30 18:34:07 -04:00
bruno ead3393211 Merge pull request 'ObsiGate 2.82.7 : barre v0.1.11 — marque monochrome, anneau avatar' (#56) from barre-v0111-2-82-7 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-29 23:10:14 -04:00
bruno 48beb692dc ObsiGate 2.82.7 : étoile Spark monochrome pour la marque de la barre
CI / lint (pull_request) Failing after 0s
CI / security (pull_request) Failing after 0s
CI / test (pull_request) Skipped
CI / build (pull_request) Skipped
CI / e2e (pull_request) Skipped
2026-10-29 23:10:00 -04:00
bruno fe4b189697 ObsiGate 2.82.7 : barre v0.1.11, marque monochrome + anneau avatar (tests/test_draco_barre.py) 2026-10-29 23:09:59 -04:00
bruno c1e79d07e4 ObsiGate 2.82.7 : barre v0.1.11, marque monochrome + anneau avatar (CHANGELOG.md) 2026-10-29 23:09:57 -04:00
bruno 5de962b2c6 ObsiGate 2.82.7 : barre v0.1.11, marque monochrome + anneau avatar (VERSION) 2026-10-29 23:09:55 -04:00
bruno bc150bf225 ObsiGate 2.82.7 : barre v0.1.11, marque monochrome + anneau avatar (frontend/draco-barre.css) 2026-10-29 23:09:53 -04:00
bruno 31b8a86861 ObsiGate 2.82.7 : barre v0.1.11, marque monochrome + anneau avatar (frontend/recherche.html) 2026-10-29 23:09:52 -04:00
bruno dda60313f0 ObsiGate 2.82.7 : barre v0.1.11, marque monochrome + anneau avatar (frontend/index.html) 2026-10-29 23:09:50 -04:00
bruno fdf33c0f54 Merge pull request 'ObsiGate 2.82.6 : barre v0.1.10 — rouge Spark, marque logo, avatar flottant, Alertes icône' (#55) from barre-logos-2-82-6 into main
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-29 22:56:31 -04:00
bruno 92bfd8f0ef ObsiGate 2.82.6 : avatar Spark flottant (commons v0.1.10)
CI / lint (pull_request) Failing after 0s
CI / security (pull_request) Failing after 0s
CI / test (pull_request) Skipped
CI / build (pull_request) Skipped
CI / e2e (pull_request) Skipped
2026-10-29 22:56:13 -04:00
bruno 64973a578b ObsiGate 2.82.6 : barre v0.1.10 — rouge Spark, marque logo, avatar flottant, Alertes icône (tests/test_draco_barre.py) 2026-10-29 22:56:11 -04:00
bruno fb5414efbb ObsiGate 2.82.6 : barre v0.1.10 — rouge Spark, marque logo, avatar flottant, Alertes icône (CHANGELOG.md) 2026-10-29 22:56:09 -04:00
bruno 174cd1c3e7 ObsiGate 2.82.6 : barre v0.1.10 — rouge Spark, marque logo, avatar flottant, Alertes icône (VERSION) 2026-10-29 22:56:07 -04:00
bruno ee6748c261 ObsiGate 2.82.6 : barre v0.1.10 — rouge Spark, marque logo, avatar flottant, Alertes icône (frontend/draco-barre.css) 2026-10-29 22:56:06 -04:00
bruno dd0b12213f ObsiGate 2.82.6 : barre v0.1.10 — rouge Spark, marque logo, avatar flottant, Alertes icône (frontend/recherche.html) 2026-10-29 22:56:04 -04:00
bruno 6195f252d9 ObsiGate 2.82.6 : barre v0.1.10 — rouge Spark, marque logo, avatar flottant, Alertes icône (frontend/index.html) 2026-10-29 22:56:02 -04:00
bruno 397a60663c Merge pull request 'ObsiGate 2.82.5 : les 5 logos dans le menu et la barre commune' (#54) from barre-logos-2-82-5 into main
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-29 22:22:58 -04:00
bruno d792714f4f ObsiGate 2.82.5 : logo fourni spark.png pour la barre et le menu
CI / lint (pull_request) Failing after 1s
CI / security (pull_request) Failing after 0s
CI / test (pull_request) Skipped
CI / build (pull_request) Skipped
CI / e2e (pull_request) Skipped
2026-10-29 22:22:41 -04:00
bruno 65edbc9e6c ObsiGate 2.82.5 : logo fourni kanbante.png pour la barre et le menu 2026-10-29 22:22:39 -04:00
bruno 95df928035 ObsiGate 2.82.5 : logo fourni tiktak.svg pour la barre et le menu 2026-10-29 22:22:38 -04:00
bruno 78ac84e1cc ObsiGate 2.82.5 : logo fourni flowdeck.png pour la barre et le menu 2026-10-29 22:22:37 -04:00
bruno 550bcb5cc8 ObsiGate 2.82.5 : logo fourni obsigate.png pour la barre et le menu 2026-10-29 22:22:35 -04:00
bruno 44baa0a273 ObsiGate 2.82.5 : logos barre-apps v0.1.9 dans le menu et la barre (CHANGELOG.md) 2026-10-29 22:22:34 -04:00
bruno 78021d6549 ObsiGate 2.82.5 : logos barre-apps v0.1.9 dans le menu et la barre (VERSION) 2026-10-29 22:22:32 -04:00
bruno 72d8ce39d6 ObsiGate 2.82.5 : logos barre-apps v0.1.9 dans le menu et la barre (frontend/draco-barre.css) 2026-10-29 22:22:30 -04:00
bruno 14dd77094d ObsiGate 2.82.5 : logos barre-apps v0.1.9 dans le menu et la barre (frontend/draco-barre.js) 2026-10-29 22:22:29 -04:00
bruno 031879889c ObsiGate 2.82.5 : logos barre-apps v0.1.9 dans le menu et la barre (frontend/recherche.html) 2026-10-29 22:22:27 -04:00
bruno a5d2e0d085 ObsiGate 2.82.5 : logos barre-apps v0.1.9 dans le menu et la barre (frontend/index.html) 2026-10-29 22:22:25 -04:00
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
62 changed files with 1499 additions and 320 deletions
+12
View File
@@ -47,6 +47,16 @@ OBSIGATE_ADMIN_PASSWORD=chab30
# Ignored directories (séparés par des virgules)
# OBSIGATE_IGNORED_DIRS=.obsidian,.trash,.git,__pycache__,node_modules,.obsigate-backup
# Vaults DIR (DIR_N_NAME / DIR_N_PATH dans le compose) — options par vault :
# DIR_N_READONLY=true : lecture seule déclarée (mutations refusées
# en 403, actions d'écriture masquées dans l'UI)
# DIR_N_IGNORED_DIRS=.home,.venv : noms de dossiers cachés pour CE vault
# (toute profondeur, en plus du global)
# DIR_N_IGNORED_PATHS=Executeurs/*/data,Executeurs/*/.env
# : motifs fnmatch sur le chemin relatif dans
# le vault (un dossier qui matche cache son
# sous-arbre) — navigation, lecture, index
# Audit
# OBSIGATE_AUDIT_MAX_SIZE=10485760
@@ -119,6 +129,8 @@ DEEPSEEK_MODEL=deepseek-chat
# DRACO_FLOWDECK_URL=https://flowdeck.dracodev.net
# DRACO_TIKTAK_URL=https://tiktak.dracodev.net
# DRACO_KANBANTE_URL=https://kanbante.dracodev.net
# DRACO_SHAARIT_URL=https://shaarit.dracodev.net
# DRACO_HUB_URL=https://hub.dracodev.net
# DRACO_GITEA_URL=https://git.dracodev.net
# ── Fusion — émission d'événements vers le service d'agents (#220) ──
+29 -17
View File
@@ -12,7 +12,7 @@ on:
jobs:
# ── Lint ──────────────────────────────────────────────────────────
lint:
runs-on: ubuntu-latest
runs-on: ci-deepin
steps:
- uses: actions/checkout@v4
@@ -100,7 +100,7 @@ jobs:
# ── Tests ─────────────────────────────────────────────────────────
test:
needs: lint
runs-on: ubuntu-latest
runs-on: ci-deepin
steps:
- uses: actions/checkout@v4
@@ -127,7 +127,7 @@ jobs:
# ── Security scan ─────────────────────────────────────────────────
security:
runs-on: ubuntu-latest
runs-on: ci-deepin
steps:
- uses: actions/checkout@v4
@@ -209,7 +209,7 @@ jobs:
# ── Docker build ──────────────────────────────────────────────────
build:
needs: test
runs-on: ubuntu-latest
runs-on: ci-deepin
steps:
- uses: actions/checkout@v4
@@ -252,7 +252,7 @@ jobs:
# ── E2E Tests (Playwright) ─────────────────────────────────────────
e2e:
needs: build
runs-on: ubuntu-latest
runs-on: ci-deepin
steps:
- uses: actions/checkout@v4
@@ -272,7 +272,13 @@ jobs:
- name: Start ObsiGate
run: |
docker rm -f obsigate-e2e 2>/dev/null || true
docker create --name obsigate-e2e -p 2029:8080 \
# DinD robuste : PAS de port publié (les ports 2029 fuités par des
# runs antérieurs font échouer « docker start », et l'IP passerelle
# du job n'est pas toujours joignable → boucle de health curls sans
# timeout qui pendaient 150 s chacun). À la place, l'app est
# rattachée au réseau du job et jointable par nom DNS embarqué.
JOBNET=$(docker inspect -f '{{range $k, $v := .NetworkSettings.Networks}}{{$k}} {{end}}' "$(hostname)" 2>/dev/null | awk '{print $1}')
docker create --name obsigate-e2e \
-v $(pwd)/test_vault:/vaults/TestVault \
-v $(pwd)/test_dir:/vaults/TestDir \
-e VAULT_1_NAME=TestVault \
@@ -288,17 +294,22 @@ jobs:
# fixtures avant le démarrage (l'indexeur scanne au démarrage).
docker cp test_vault/. obsigate-e2e:/vaults/TestVault
docker cp test_dir/. obsigate-e2e:/vaults/TestDir
if [ -n "$JOBNET" ]; then docker network connect "$JOBNET" obsigate-e2e || true; fi
docker start obsigate-e2e
# Le port publié est joignable via l'IP de la passerelle (localhost
# du job container ne voit pas le port publié sur l'hôte).
GW=$(ip route show default | awk '{print $3}' || echo 172.17.0.1)
echo "Gateway IP: $GW"
# Wait for health check
for i in $(seq 1 30); do
if curl -sf "http://$GW:2029/api/health"; then echo "Health OK"; break; fi
if [ -n "$JOBNET" ]; then
APP_URL="http://obsigate-e2e:8080"
else
echo "Réseau du job introuvable (JOBNET vide) — impossible de localiser l'app sans port publié"
exit 1
fi
echo "APP_URL: $APP_URL"
# --max-time : ne JAMAIS pendre sans timeout (bug d'origine : curls
# sans timeout = 150 s × 31 tentatives, job « bloqué »).
for i in $(seq 1 40); do
if curl -sf --max-time 3 "$APP_URL/api/health"; then echo "Health OK"; break; fi
sleep 1
done
curl -sf "http://$GW:2029/api/health" >/dev/null || { echo "App not reachable at $GW:2029"; exit 1; }
curl -sf --max-time 5 "$APP_URL/api/health" >/dev/null || { echo "App not reachable at $APP_URL"; docker logs --tail 40 obsigate-e2e; exit 1; }
# Les fixtures sont copiées via `docker cp` en root → rendre le vault
# inscriptible par l'utilisateur non-root de l'app (UID 1000), sinon
# toute création/édition de fichier renvoie 403 « Vault is read-only ».
@@ -306,9 +317,10 @@ jobs:
- name: Run E2E tests
run: |
GW=$(ip route show default | awk '{print $3}' || echo 172.17.0.1)
echo "Using BASE_URL=http://$GW:2029"
BASE_URL="http://$GW:2029" npx playwright test --project=chromium-desktop --reporter=list
BASE_URL="http://obsigate-e2e:8080"
docker exec obsigate-e2e true >/dev/null 2>&1 || { echo "obsigate-e2e n'est pas en marche"; exit 1; }
echo "Using BASE_URL=$BASE_URL"
BASE_URL="$BASE_URL" npx playwright test --project=chromium-desktop --reporter=list
- name: Upload test results
if: always()
+62 -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.3**.
> [Unreleased](#unreleased). La dernière version livrée est **2.83.4**.
---
@@ -14,6 +14,67 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
---
## [2.83.4] — 2026-11-05
- **Barre commune v0.4.0 — icônes monoline du menu compte (HUB-11, fusion #125)** : `frontend/draco-barre.js` et `frontend/draco-barre.css` re-vendangés **verbatim** depuis dracodev-commons v0.4.0 (branche `menu-compte-icones-v0-4-0`, PR commons #10). Les items du menu compte — **Télécharger**, **À Propos**, **Aides**, **Profil**, **Déconnexion** — reçoivent des **icônes SVG monoline** (contour, trait fin, `currentColor`, ~18 px) dans le style des icônes du rail de la console Spark ; aucun emoji. L'en-tête d'identité reste sans icône ; les liens Hub et le relais « Déconnexion » sont inchangés. Garde ajoutée à `tests/test_draco_barre.py`.
---
## [2.83.3] — 2026-11-04
- **Barre commune v0.3.0 — liens Hub du menu compte (HUB-1, fusion #115)** : `frontend/draco-barre.js` et `frontend/draco-barre.css` re-vendangés **verbatim** depuis dracodev-commons v0.3.0 (branche `hub-menu-compte-v0-3-0`, PR commons #9). Le menu compte gagne les liens **Télécharger**, **À Propos**, **Aides** et **Profil** (devenu un lien, plus un relais) vers le Hub. Branchement hôte : `backend/draco_config.py` gagne la clé `hub` (défaut officiel `https://hub.dracodev.net`, variable `DRACO_HUB_URL`, dérivation `-dev` → `hub-dev` depuis l'hôte de l'agent), servie dans `window.DRACO_URLS` ; `frontend/index.html` la passe en `urls.hub` de `DRACO_BAR_CONFIG`. « Déconnexion » reste relayée, inchangée. Aucun nouvel onglet : le Hub n'apparaît que dans le menu compte.
---
## [2.83.2] — 2026-11-04
- **Recherche fédérée — ShaarIt, 5e source de « Rechercher partout »** (demande de Bruno, 2026-11-04 ; contrat dracodev-commons v0.2.1, `contrats/recherche-federee.md`) : `frontend/js/recherche.js` **re-vendangé verbatim** depuis la source canonique de commons (`ui/recherche/`) — ShaarIt devient une source de la page `/recherche` comme les autres (bases prod/dev, onglet, normaliseur : lien profond canonique `{base}shaare/<shorturl>`, contexte « Bookmark · privé · tags », repli recherche native `/?q=`). Le fichier reste strictement identique chez les cinq hôtes.
- **Page `/recherche`** : onglet et section « ShaarIt — bookmarks », `data-base-shaarit` rendu par le jeton `{{URL_SHAARIT}}` (déjà résolu par la route depuis la 2.83.1) ; la CSP `connect-src` de la page ajoute l'origine ShaarIt résolue (`backend/main.py`).
- **CORS de recherche** (`backend/recherche_cors.py`) : origines officielles ShaarIt prod et dev ajoutées aux listes explicites, `shaarit` ajoutée aux clés sœurs résolues — `GET /api/search` d'ObsiGate devient lisible depuis les pages de ShaarIt, et réciproquement.
- Tests : `tests/frontend/recherche.test.mjs` porté à 5 sections (normaliseur ShaarIt inclus) ; `tests/test_recherche_cors.py` — allowlist attendue à 12 origines.
---
## [2.83.1] — 2026-11-03
- **Barre commune (dracodev-commons v0.2.0 vendangée, @70c837a)** : nouvel onglet **ShaarIt** (bookmarks, réécriture Rust de Shaarli) après Kanbante et avant l'onglet Console Spark — `frontend/draco-barre.css` et `frontend/draco-barre.js` remplacés **verbatim** par la v0.2.0 (hachages vérifiés), logo `frontend/img/logos/shaarit.svg` ajouté tel quel ; le fragment porté dans `frontend/index.html` et `frontend/recherche.html` (jeton `{{URL_SHAARIT}}` remplacé par la route `/recherche`). L'URL ShaarIt suit le **même mécanisme** que les autres apps : `backend/draco_config.py` gagne la clé `shaarit` (défaut officiel `https://shaarit.dracodev.net`, variable `DRACO_SHAARIT_URL`, dérivation `-dev` → `shaarit-dev` depuis l'hôte de l'agent), servie dans `window.DRACO_URLS` et réécrite sur l'onglet comme les autres.
- **⚠️ Changement de comportement visible — garde `data-theme` (CSS v0.2.0)** : les blocs `prefers-color-scheme` du CSS vendangé sont désormais préfixés `html:not([data-theme])`. La copie précédente n'avait **pas** cette garde. ObsiGate pose toujours `data-theme` sur `<html>`, donc sa barre ne change pas en pratique ; la garde ne s'exercerait que sur une page hôte sans `data-theme` (elle ne forcerait plus le sombre système contre un choix clair). Signalé pour arbitrage à la fusion, comme chez Flowdeck où le piège était actif.
---
## [2.83.0] — 2026-11-03
- **Vaults DIR : exclusions par vault et lecture seule déclarable** (prépare le vault « Spark ») : trois nouvelles variables par vault DIR — `DIR_N_READONLY=true` (les mutations sont refusées en 403 « read_only » et l'arborescence n'affiche plus les actions d'écriture pour ce vault), `DIR_N_IGNORED_DIRS=.home,.venv,…` (noms de dossiers cachés à toute profondeur, en plus du global `IGNORED_DIRS`) et `DIR_N_IGNORED_PATHS=Executeurs/*/data,…` (motifs fnmatch sur le chemin relatif ; un dossier qui matche cache tout son sous-arbre). Les exclusions s'appliquent désormais **partout** — navigation (`/api/browse`), lecture (`/api/file`, `raw`, téléchargement : 404), liste des chemins et indexation — et plus seulement à l'indexeur. `/api/vaults` expose `readonly` pour chaque vault. Sans ces variables, comportement inchangé. Fiche : `docs/features/spark-vault-dir.md`. Tests : `tests/test_spark_vault_dir.py`.
---
## [2.82.8] — 2026-10-30
- **Panneau détail d'un secret : Afficher/Masquer + Copier la valeur** (PR #57) : la valeur reste masquée (••••••) par défaut ; Afficher la révèle en place, Masquer l'efface (effacée aussi dès que le détail change). « Copier la valeur » copie au presse-papiers sans afficher. Le bouton « Copier » existant ne copiait que la référence.
- **Nouvel endpoint `POST /api/coffres/{slug}/secrets/{nom}/reveler`** : session + gestion du coffre exigées (propriétaire, ou admin sur coffre d'instance) ; un lecteur par octroi utilisateur reçoit 403. Chaque révélation est auditée (`revelation_manuelle`) et met à jour la dernière utilisation. Ce n'est pas une distribution : aucun octroi créé, aucun consommateur ne reçoit la valeur.
- **Suppression de la « Copie manuelle à usage unique »** par consommateur temporaire : elle échouait systématiquement (aucun octroi pour le consommateur temporaire, donc 403 octroi_absent) ; « Copier la valeur » la remplace.
- **Tests** : `tests/test_coffre_secrets.py` 44/44 (5 nouveaux TestRevelation).
## [2.82.7] — 2026-10-29
- **Barre commune (dracodev-commons v0.1.11 vendangée)** : la marque de la barre n'est plus le logo rouge — c'est l'étoile Spark monochrome (`frontend/img/brand-spark.png`, servi en `/static/img/brand-spark.png`, 26 px), blanche sur barre foncée et noire sur barre claire : ObsiGate pose déjà `data-theme` sur `<html>` (sombre par défaut, mode persisté), donc la règle vendangée `[data-theme="dark"] .draco-bar .draco-brand-mark img { filter: none; }` s'applique directement et aucune surcharge propre n'est nécessaire. L'onglet Console Spark (et son entrée de menu, qui le clone) GARDE le logo rouge. Le bouton avatar Spark flottant bas-centre gagne un anneau rouge `#c00000` (bordure 3 px, `var(--agent)`), via le CSS vendangé.
- **Pages concernées** : la barre est rendue en ligne dans `frontend/index.html` et dans sa copie de `frontend/recherche.html` — les deux sont portées à la v0.1.11 ; `frontend/draco-barre.css` remplacé verbatim par la v0.1.11, `frontend/draco-barre.js` inchangé (identique à commons, lui-même inchangé en v0.1.11). Le reste de l'intégration est inchangé. Précision : sur `/recherche`, copie statique, le bouton flottant y est visuel seulement, comme au lot précédent — l'ouverture du panneau vit dans la SPA.
## [2.82.6] — 2026-10-29
- **Barre commune (dracodev-commons v0.1.10 vendangée)** : le rose Spark `#c2437e` disparaît de la barre — `--agent` passe au rouge Spark `#c00000` (échantillonné dans le logo Spark), l'onglet Console Spark passe à `--app #c00000`, et la marque ✦ devient le logo Spark (26 px). Le bouton « Spark » rose de la barre est supprimé, remplacé par le bouton avatar Spark flottant bas-centre (cercle personnage, `frontend/img/avatar-spark.png`, servi en `/static/img/avatar-spark.png`, `data-act="spark"` → panneau Spark de l'hôte, comme l'ancien bouton). Le bouton Alertes n'affiche plus que la cloche (pastille conservée), donc une barre plus étroite.
- **Pages concernées** : la barre est rendue en ligne dans `frontend/index.html` et dans sa copie de `frontend/recherche.html` — les deux sont portées à la v0.1.10 ; `frontend/draco-barre.css` remplacé verbatim par la v0.1.10, `frontend/draco-barre.js` inchangé (identique à commons v0.1.9, lui-même inchangé en v0.1.10). Le reste de l'intégration (URL, `DRACO_URLS`, actions, compteurs) est inchangé. Précision : sur `/recherche`, copie statique, le panneau Spark n'est pas rejoué (comme avant) — le bouton flottant y est visuel, l'ouverture du panneau vit dans la SPA.
## [2.82.5] — 2026-10-29
- **Barre commune (dracodev-commons v0.1.9 vendangée)** : les 5 tuiles des onglets — et donc du menu de la marque, qui les clone — sont désormais les logos fournis, tels quels, jamais redessinés : ObsiGate (arbre), Flowdeck (cartes), TikTak (logo officiel SVG), Kanbante (cycle vert), Console Spark (étoile rouge validée). Fichiers dans `frontend/img/logos/` (copie de `ui/barre-apps/logos/` de commons, PNG 180 px), servis en `/static/img/logos/` ; l'entrée « Console Spark » du menu clone l'icône de l'onglet agent (`draco-barre.js`) ; onglet agent passé à `--app #d21f2c`. Les glyphes texte ▤/▦/✓/▥/✦ disparaissent des onglets (marque ✦, bouton Spark et rappel d'en-tête inchangés).
- **Pages concernées** : la barre est rendue en ligne dans `frontend/index.html` et dans sa copie de `frontend/recherche.html` — les deux sont portées à la v0.1.9, `draco-barre.js` et `draco-barre.css` remplacés verbatim (les fichiers précédents étaient identiques à l'octet près à ceux de commons avant la v0.1.9, aucune adaptation locale perdue). Le reste de l'intégration (URL, `DRACO_URLS`, actions, compteurs) est inchangé.
## [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.
+3 -3
View File
@@ -4,7 +4,7 @@
**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.
[![Version](https://img.shields.io/badge/Version-2.81.1-blue.svg)]()
[![Version](https://img.shields.io/badge/Version-2.83.4-blue.svg)]()
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Docker](https://img.shields.io/badge/Docker-Ready-blue.svg)](https://www.docker.com/)
[![Python](https://img.shields.io/badge/Python-3.11+-green.svg)](https://www.python.org/)
@@ -977,8 +977,8 @@ Ce projet est sous licence **MIT** — voir le fichier [LICENSE](LICENSE) pour l
## 📝 Changelog
Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.81.1).
Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.83.4).
---
*Projet : ObsiGate | Version : 2.81.1 | Dernière mise à jour : Septembre 2026*
*Projet : ObsiGate | Version : 2.83.4 | Dernière mise à jour : Septembre 2026*
+3 -3
View File
@@ -2,7 +2,7 @@
**Ultra-light web gateway for your Obsidian vaults** — Access, browse, and search all your Obsidian notes from any device via a modern, responsive web interface.
[![Version](https://img.shields.io/badge/Version-2.81.1-blue.svg)]()
[![Version](https://img.shields.io/badge/Version-2.83.4-blue.svg)]()
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Docker](https://img.shields.io/badge/Docker-Ready-blue.svg)](https://www.docker.com/)
[![Python](https://img.shields.io/badge/Python-3.11+-green.svg)](https://www.python.org/)
@@ -1152,8 +1152,8 @@ This project is licensed under the **MIT License** - see the [LICENSE](LICENSE)
## 📝 Changelog
See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.81.1).
See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.83.4).
---
*Project: ObsiGate | Version: 2.81.1 | Last updated: September 2026*
*Project: ObsiGate | Version: 2.83.4 | Last updated: September 2026*
+1 -1
View File
@@ -1 +1 @@
2.82.3
2.83.4
+10
View File
@@ -17,6 +17,10 @@ Variables (voir ``.env.example``) :
- ``DRACO_FLOWDECK_URL`` — base publique de Flowdeck.
- ``DRACO_TIKTAK_URL`` — base publique de TikTak.
- ``DRACO_KANBANTE_URL`` — base publique de Kanbante.
- ``DRACO_SHAARIT_URL`` — base publique de ShaarIt (barre v0.2.0).
- ``DRACO_HUB_URL`` — base publique du Hub (barre v0.3.0,
HUB-1 : liens Télécharger / À Propos / Aides / Profil du menu
compte ; le Hub n'a pas d'onglet dans la barre).
- ``DRACO_GITEA_URL`` — base publique de Gitea (les cartes
Kanbante *sont* des issues Gitea, contrat liens-profonds) ;
distincte de ``OBSIGATE_GITEA_URL`` (source connectée de
@@ -50,6 +54,8 @@ DEFAUTS: dict[str, str] = {
"flowdeck": "https://flowdeck.dracodev.net",
"tiktak": "https://tiktak.dracodev.net",
"kanbante": "https://kanbante.dracodev.net",
"shaarit": "https://shaarit.dracodev.net",
"hub": "https://hub.dracodev.net",
"gitea": "https://git.dracodev.net",
}
@@ -59,6 +65,8 @@ _VARIABLES: dict[str, str] = {
"flowdeck": "DRACO_FLOWDECK_URL",
"tiktak": "DRACO_TIKTAK_URL",
"kanbante": "DRACO_KANBANTE_URL",
"shaarit": "DRACO_SHAARIT_URL",
"hub": "DRACO_HUB_URL",
"gitea": "DRACO_GITEA_URL",
}
@@ -71,6 +79,8 @@ _SLUGS_HOTE: dict[str, str] = {
"flowdeck": "flowdeck",
"tiktak": "tiktak",
"kanbante": "kanbante",
"shaarit": "shaarit",
"hub": "hub",
}
# Premier label de l'hôte de l'agent : ``agent`` suivi d'un suffixe
+44 -4
View File
@@ -97,6 +97,13 @@ def load_vault_config() -> dict[str, dict[str, Any]]:
Also reads optional configuration:
- VAULT_N_ATTACHMENTS_PATH: relative path to attachments folder
- VAULT_N_SCAN_ATTACHMENTS: "true"/"false" to enable/disable scanning
- DIR_N_READONLY: "true"/"false" — vault DIR déclaré en lecture seule
(les mutations sont refusées en 403, l'UI masque les actions d'écriture)
- DIR_N_IGNORED_DIRS: noms de dossiers ignorés pour ce vault, séparés
par des virgules (toute profondeur, en plus du global IGNORED_DIRS)
- DIR_N_IGNORED_PATHS: motifs fnmatch sur le chemin relatif POSIX dans
le vault, séparés par des virgules (un dossier qui matche cache tout
son sous-arbre) — navigation, lecture et indexation les respectent
Returns:
Dict mapping vault names to configuration dicts with keys:
@@ -104,6 +111,9 @@ def load_vault_config() -> dict[str, dict[str, Any]]:
- attachmentsPath: relative attachments folder (optional)
- scanAttachmentsOnStartup: boolean (default True)
- type: "VAULT" or "DIR"
- readonly: boolean (DIR only, default False)
- ignoredDirs: set of directory names (DIR only, default empty)
- ignoredPaths: list of fnmatch patterns (DIR only, default empty)
"""
vaults: dict[str, dict[str, Any]] = {}
n = 1
@@ -131,12 +141,23 @@ def load_vault_config() -> dict[str, dict[str, Any]]:
path = os.environ.get(f"DIR_{n}_PATH")
if not name or not path:
break
readonly = os.environ.get(f"DIR_{n}_READONLY", "false").lower() == "true"
ignored_dirs = {
d.strip() for d in os.environ.get(f"DIR_{n}_IGNORED_DIRS", "").split(",") if d.strip()
}
ignored_paths = [
p.strip() for p in os.environ.get(f"DIR_{n}_IGNORED_PATHS", "").split(",") if p.strip()
]
vaults[name] = {
"path": path,
"attachmentsPath": None,
"scanAttachmentsOnStartup": False,
"type": "DIR"
"type": "DIR",
"readonly": readonly,
"ignoredDirs": ignored_dirs,
"ignoredPaths": ignored_paths,
}
n += 1
@@ -509,7 +530,8 @@ def _scan_vault(
Args:
vault_name: Display name of the vault.
vault_path: Absolute filesystem path to the vault root.
vault_cfg: Optional vault configuration dict (unused for indexing, kept for compatibility).
vault_cfg: Optional vault configuration dict — ses exclusions
(``ignoredDirs`` / ``ignoredPaths``) sont appliquées au scan.
previous_files: Optional ``{relative_path: file_info}`` snapshot from a
previous scan used for differential reuse.
@@ -529,6 +551,16 @@ def _scan_vault(
root_resolved = vault_root.resolve(strict=False)
# Exclusions propres au vault (DIR_N_IGNORED_DIRS / DIR_N_IGNORED_PATHS) :
# elles s'ajoutent au global IGNORED_DIRS, sans le modifier. Le cfg est
# celui passé par l'appelant — l'index global ne contient pas encore ce
# vault pendant son premier scan, chemin_exclu() (qui lit l'index) ne
# peut donc pas servir ici.
from backend.services.vaults import chemin_exclu_dans_config
cfg = vault_cfg or {}
ignored_names = IGNORED_DIRS | set(cfg.get("ignoredDirs") or [])
# BUG-032: walk without following symlinks and refuse any symlink that
# escapes the vault root, so external data can never be indexed/exposed.
for dirpath, dirnames, filenames in os.walk(vault_root, followlinks=False):
@@ -537,7 +569,11 @@ def _scan_vault(
# Prune ignored and symlinked directories in place (no recursion).
dirnames[:] = [
d for d in dirnames
if d not in IGNORED_DIRS and not (current_dir / d).is_symlink()
if d not in ignored_names
and not (current_dir / d).is_symlink()
and not chemin_exclu_dans_config(
cfg, str((current_dir / d).relative_to(vault_root)).replace("\\", "/")
)
]
for d in dirnames:
@@ -565,6 +601,10 @@ def _scan_vault(
rel_path_str = str(fpath.relative_to(vault_root)).replace("\\", "/")
# Exclusion propre au vault (motif de chemin, ex. un .env ciblé).
if chemin_exclu_dans_config(cfg, rel_path_str):
continue
ext = fpath.suffix.lower()
# Also match extensionless files named like Dockerfile, Makefile
basename_lower = fpath.name.lower()
+4 -2
View File
@@ -966,7 +966,8 @@ if FRONTEND_DIR.exists():
frontend/recherche.html est servi avec les jetons {{URL_*}}
remplacés par urls_publiques() (même contrat que le fragment
de barre, jeton {{URL_AGENT}} inclus pour l'onglet Console
de barre, jetons {{URL_SHAARIT}} et {{URL_AGENT}} inclus pour
les onglets ShaarIt (barre v0.2.0) et Console
Spark de la barre re-vendangée) et le nonce CSP frais ; la
réponse pose sa propre CSP (préservée par
SecurityHeadersMiddleware, qui ne la pose que si elle est
@@ -992,6 +993,7 @@ if FRONTEND_DIR.exists():
("{{URL_FLOWDECK}}", "flowdeck"),
("{{URL_TIKTAK}}", "tiktak"),
("{{URL_KANBANTE}}", "kanbante"),
("{{URL_SHAARIT}}", "shaarit"),
("{{URL_AGENT}}", "agent"),
):
html = html.replace(jeton, urls.get(cle, ""))
@@ -1008,7 +1010,7 @@ if FRONTEND_DIR.exists():
soeurs = " ".join(sorted({
origin_of(urls.get(cle, "")) or ""
for cle in ("obsigate", "flowdeck", "tiktak", "kanbante",
"agent")
"shaarit", "agent")
} - {""}))
csp = (
"default-src 'self'; "
+3 -1
View File
@@ -43,6 +43,7 @@ PROD_ORIGINS = frozenset({
"https://flowdeck.dracodev.net",
"https://tiktak.dracodev.net",
"https://kanbante.dracodev.net",
"https://shaarit.dracodev.net",
})
DEV_ORIGINS = frozenset({
@@ -50,6 +51,7 @@ DEV_ORIGINS = frozenset({
"https://flowdeck-dev.dracodev.net",
"https://tiktak-dev.dracodev.net",
"https://kanbante-dev.dracodev.net",
"https://shaarit-dev.dracodev.net",
})
AGENT_ORIGINS = frozenset({
@@ -59,7 +61,7 @@ AGENT_ORIGINS = frozenset({
# 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", "shaarit")
def is_dev_host(hostname: str | None) -> bool:
+10 -2
View File
@@ -22,10 +22,12 @@ httpx>=0.27.0
# l'utilisateur). #224 : la version préinstallée dans la toolcache du
# runner (6.19.0) est frappée par 11 CVE supplémentaires (dont
# CVE-2026-113786, CVE-2026-115572, CVE-2026-115585, CVE-2026-115580),
# corrigées au plus tard en 6.21.2 — le plancher doit être >= 6.21.2 :
# corrigées au plus tard en 6.21.2. La 6.21.2 est à son tour frappée
# (CVE-2026-119203 et suivantes, relevées par le job security sur main),
# corrigées en 6.21.4 — le plancher doit être >= 6.21.4 :
# en dessous, pip répond « already satisfied » pour la toolcache et
# n'aligne jamais (même piège que BUG-093).
pypdf>=6.21.2
pypdf>=6.21.4
pyotp>=2.10.0
segno>=1.5.0
webauthn==2.6.0
@@ -79,3 +81,9 @@ cryptography>=44.0
# Plancher de securite : msgpack est transitif, version 1.2.1 relevee par le job
# security (pip-audit, bloquant) — CVE-2026-114613, corrigee en 1.2.3.
msgpack>=1.2.3
# Plancher de sécurité : pyasn1 est transitive (python-jose/rsa), version
# 0.6.4 relevée par le job security (pip-audit, bloquant) —
# CVE-2026-119194 à CVE-2026-119197, corrigées en 0.6.5. Sans plancher
# explicite, pip répond « already satisfied » pour la toolcache et
# n'aligne jamais (même piège que BUG-093).
pyasn1>=0.6.5
+53 -1
View File
@@ -12,6 +12,14 @@ Règles d'or, appliquées côté serveur à chaque requête :
- Seul ``POST /api/coffres/distribuer`` rend une valeur, et uniquement à
un consommateur authentifié par jeton (hash + jti, jamais la session
utilisateur), avec un octroi actif et un contexte obligatoire.
- Seule exception, voulue : ``POST /api/coffres/{slug}/secrets/{nom}/reveler``
rend la valeur au gestionnaire du coffre authentifié par sa session
(propriétaire, ou admin sur coffre d'instance — jamais un simple
lecteur par octroi utilisateur), pour l'affichage et la copie manuelle
dans le panneau détail. Chaque révélation est auditée
(``revelation_manuelle``) et met à jour la dernière utilisation ; la
valeur n'est jamais journalisée et le frontend l'efface dès qu'elle
est masquée ou que le détail change.
- Autorisations (spec §6) : le propriétaire seul sur son coffre
personnel ; l'admin gère les coffres d'instance, les consommateurs et
la KEK, jamais le coffre personnel d'autrui ; l'ACL « * » des vaults
@@ -57,7 +65,6 @@ from backend.coffre_store import (
SecretExpireError,
ValidationError,
compter_octrois_actifs,
est_expire,
compter_secrets,
create_coffre,
create_consommateur,
@@ -65,6 +72,7 @@ from backend.coffre_store import (
create_secret,
delete_secret,
distribuer_valeur,
est_expire,
get_coffre,
get_coffre_by_id,
get_coffre_or_404,
@@ -622,6 +630,50 @@ async def api_rotate_secret(
return _metadata_secret(coffre, secret)
@router.post("/api/coffres/{slug}/secrets/{nom}/reveler")
async def api_reveler_secret(
slug: str, nom: str, current_user=Depends(require_auth)
):
"""Révélation manuelle pour le panneau détail (afficher / copier).
Session utilisateur + gestion du coffre exigées : un lecteur par
octroi utilisateur voit les métadonnées mais ne révèle jamais la
valeur. Chaque appel est audité et touche la dernière utilisation.
"""
_exiger_actives()
coffre = _coffre_or_404(slug)
_exiger_gerer(current_user, coffre)
secret = _secret_or_404(coffre, nom)
try:
valeur = distribuer_valeur(coffre, secret)
except SecretExpireError:
_audit(_username(current_user), "revelation_refusee",
coffre=coffre["slug"],
secret_ref=f"coffre:{coffre['slug']}/{secret['nom']}",
resultat="secret_expire")
raise HTTPException(status_code=410, detail="secret_expire") from None
except KEKAbsenteError:
_audit(_username(current_user), "revelation_refusee",
coffre=coffre["slug"],
secret_ref=f"coffre:{coffre['slug']}/{secret['nom']}",
resultat="kek_absente")
raise HTTPException(status_code=503, detail="kek_absente") from None
except CryptoIndisponibleError:
_audit(_username(current_user), "revelation_refusee",
coffre=coffre["slug"],
secret_ref=f"coffre:{coffre['slug']}/{secret['nom']}",
resultat="crypto_indisponible")
raise HTTPException(status_code=503,
detail="crypto_indisponible") from None
toucher_utilisation(secret["id"])
_audit(_username(current_user), "revelation_manuelle",
coffre=coffre["slug"],
secret_ref=f"coffre:{coffre['slug']}/{secret['nom']}",
resultat="ok")
return {"reference": f"coffre:{coffre['slug']}/{secret['nom']}",
"valeur": valeur, "version": secret["version_courante"]}
@router.delete("/api/coffres/{slug}/secrets/{nom}")
async def api_delete_secret(
slug: str, nom: str, current_user=Depends(require_auth)
+12 -1
View File
@@ -44,7 +44,7 @@ from backend.schemas import (
from backend.services.files import read_raw_file
from backend.services.mutations import file_revision
from backend.services.paths import resolve_safe_path
from backend.services.vaults import browse_directory, get_vault_root
from backend.services.vaults import browse_directory, chemin_exclu, get_vault_root
from backend.share import list_shares
logger = logging.getLogger("obsigate")
@@ -171,6 +171,9 @@ async def api_file_download(vault_name: str, path: str = Query(..., description=
if not vault_data:
raise HTTPException(status_code=404, detail=f"Vault '{vault_name}' not found")
if chemin_exclu(vault_name, path):
raise HTTPException(status_code=404, detail=f"File not found: {path}")
vault_root = Path(vault_data["path"])
file_path = resolve_safe_path(vault_root, path)
@@ -243,6 +246,8 @@ def api_file_xlsx_dashboard(
"""
if not check_vault_access(vault_name, current_user):
raise HTTPException(status_code=403, detail=f"Accès refusé à la vault '{vault_name}'")
if chemin_exclu(vault_name, path):
raise HTTPException(status_code=404, detail=f"File not found: {path}")
_vault_root = get_vault_root(vault_name)
file_path = resolve_safe_path(_vault_root, path)
if not file_path.is_file():
@@ -311,6 +316,9 @@ def api_file_xlsx_sheet(
if not vault_data:
raise HTTPException(status_code=404, detail=f"Vault '{vault_name}' not found")
if chemin_exclu(vault_name, path):
raise HTTPException(status_code=404, detail=f"File not found: {path}")
file_path = resolve_safe_path(Path(vault_data["path"]), path)
if not file_path.is_file():
raise HTTPException(status_code=404, detail=f"File not found: {path}")
@@ -366,6 +374,9 @@ async def api_file(vault_name: str, path: str = Query(..., description="Relative
if not vault_data:
raise HTTPException(status_code=404, detail=f"Vault '{vault_name}' not found")
if chemin_exclu(vault_name, path):
raise HTTPException(status_code=404, detail=f"File not found: {path}")
vault_root = Path(vault_data["path"])
file_path = resolve_safe_path(vault_root, path)
+1
View File
@@ -609,6 +609,7 @@ class VaultInfo(BaseModel):
file_count: int = Field(description="Number of indexed files")
tag_count: int = Field(description="Number of unique tags")
type: str = Field(default="VAULT", description="Type of the vault mapping (VAULT or DIR)")
readonly: bool = Field(default=False, description="True when the vault is declared read-only (DIR_N_READONLY) — mutations are refused and the UI hides write actions")
class BookmarkToggleRequest(BaseModel):
+19 -1
View File
@@ -7,13 +7,30 @@ from typing import Any
from backend.services.errors import ServiceError
from backend.services.paths import resolve_safe_path
from backend.services.vaults import get_vault_root
from backend.services.vaults import chemin_exclu, get_vault_root
logger = logging.getLogger("obsigate.services.files")
def _ensure_not_excluded(vault_name: str, path: str) -> None:
"""Raise ``not_found`` (404) when *path* is excluded in this vault.
Un chemin exclu (DIR_N_IGNORED_DIRS / DIR_N_IGNORED_PATHS) est
indiscernable d'un chemin inexistant : même erreur, aucun détail
sur la règle qui l'a caché.
"""
if chemin_exclu(vault_name, path):
raise ServiceError(
f"File not found: {path}",
code="not_found",
status=404,
details={"vault": vault_name, "path": path},
)
def read_raw_file(vault_name: str, path: str) -> dict[str, Any]:
"""Return the raw text content of a vault file (no redaction)."""
_ensure_not_excluded(vault_name, path)
root = get_vault_root(vault_name)
file_path = resolve_safe_path(root, path)
@@ -56,6 +73,7 @@ def read_file_text(
ServiceError: ``not_found`` (404), ``file_too_large`` (413) or a read
error (500).
"""
_ensure_not_excluded(vault_name, path)
root = get_vault_root(vault_name)
target = resolve_safe_path(root, path)
+29 -19
View File
@@ -69,8 +69,18 @@ def _rel(root: Path, path: Path) -> str:
return str(path.relative_to(root)).replace("\\", "/")
def _ensure_writable(root: Path) -> None:
"""Raise ``read_only`` (403) when the vault root is not writable."""
def _ensure_writable(root: Path, vault_name: str | None = None) -> None:
"""Raise ``read_only`` (403) when the vault must not be mutated.
Two sources, same error: the vault is **declared** read-only
(``DIR_N_READONLY``, via :func:`vault_readonly`) or its root is
not writable on the filesystem (``:ro`` mount).
"""
if vault_name is not None:
from backend.services.vaults import vault_readonly
if vault_readonly(vault_name):
raise ServiceError("Vault is read-only", code="read_only", status=403)
if not os.access(root, os.W_OK):
raise ServiceError("Vault is read-only", code="read_only", status=403)
@@ -139,7 +149,7 @@ def create_file(
``unsupported_extension`` (400) or ``already_exists`` (409).
"""
root = get_vault_root(vault_name)
_ensure_writable(root)
_ensure_writable(root, vault_name)
file_path = resolve_safe_path(root, path)
_validate_extension(file_path)
@@ -190,7 +200,7 @@ def create_directory(vault_name: str, path: str, *, exist_ok: bool = False) -> d
``already_exists`` (409) when *exist_ok* is False.
"""
root = get_vault_root(vault_name)
_ensure_writable(root)
_ensure_writable(root, vault_name)
dir_path = resolve_safe_path(root, path)
if dir_path.exists():
@@ -234,7 +244,7 @@ def edit_file(
ServiceError: ``not_found`` (404) or ``read_only`` (403).
"""
root = get_vault_root(vault_name)
_ensure_writable(root)
_ensure_writable(root, vault_name)
file_path = resolve_safe_path(root, path)
if not file_path.exists() or not file_path.is_file():
@@ -470,7 +480,7 @@ def edit_xlsx_cells(
reference or value.
"""
root = get_vault_root(vault_name)
_ensure_writable(root)
_ensure_writable(root, vault_name)
file_path = resolve_safe_path(root, path)
if not file_path.exists() or not file_path.is_file():
@@ -592,7 +602,7 @@ def mutate_xlsx_structure(
— deletions are NOT recoverable from the viewer (only via the ``.bak``).
"""
root = get_vault_root(vault_name)
_ensure_writable(root)
_ensure_writable(root, vault_name)
file_path = resolve_safe_path(root, path)
if not file_path.exists() or not file_path.is_file():
@@ -1000,7 +1010,7 @@ def mutate_xlsx_style(
from openpyxl.utils.cell import range_boundaries
root = get_vault_root(vault_name)
_ensure_writable(root)
_ensure_writable(root, vault_name)
file_path = resolve_safe_path(root, path)
if not file_path.exists() or not file_path.is_file():
@@ -1255,7 +1265,7 @@ def save_csv_cells(
from backend.xlsx_reader import sniff_csv_delimiter
root = get_vault_root(vault_name)
_ensure_writable(root)
_ensure_writable(root, vault_name)
file_path = resolve_safe_path(root, path)
if not file_path.exists() or not file_path.is_file():
@@ -1341,7 +1351,7 @@ def append_to_file(
ServiceError: ``not_found`` (404) or ``read_only`` (403).
"""
root = get_vault_root(vault_name)
_ensure_writable(root)
_ensure_writable(root, vault_name)
file_path = resolve_safe_path(root, path)
if not file_path.exists() or not file_path.is_file():
@@ -1382,7 +1392,7 @@ def rename_file(vault_name: str, path: str, new_name: str) -> dict[str, Any]:
``unsupported_extension`` (400) or ``already_exists`` (409).
"""
root = get_vault_root(vault_name)
_ensure_writable(root)
_ensure_writable(root, vault_name)
old_path = resolve_safe_path(root, path)
if not old_path.exists() or not old_path.is_file():
@@ -1424,7 +1434,7 @@ def rename_directory(vault_name: str, path: str, new_name: str) -> dict[str, Any
``already_exists`` (409).
"""
root = get_vault_root(vault_name)
_ensure_writable(root)
_ensure_writable(root, vault_name)
old_path = resolve_safe_path(root, path)
if not old_path.exists() or not old_path.is_dir():
@@ -1467,7 +1477,7 @@ def move_path(vault_name: str, source_path: str, destination_dir: str = "") -> d
``unsupported_extension`` (400) or ``already_exists`` (409).
"""
root = get_vault_root(vault_name)
_ensure_writable(root)
_ensure_writable(root, vault_name)
source = resolve_safe_path(root, source_path)
if not source.exists():
@@ -1546,7 +1556,7 @@ def duplicate_path(vault_name: str, source_path: str, destination_dir: str = "")
destination is inside the source directory.
"""
root = get_vault_root(vault_name)
_ensure_writable(root)
_ensure_writable(root, vault_name)
source = resolve_safe_path(root, source_path)
if not source.exists():
@@ -1764,7 +1774,7 @@ def delete_file(vault_name: str, path: str, *, backup: bool = True) -> dict[str,
ServiceError: ``not_found`` (404) or ``read_only`` (403).
"""
root = get_vault_root(vault_name)
_ensure_writable(root)
_ensure_writable(root, vault_name)
file_path = resolve_safe_path(root, path)
if not file_path.exists() or not file_path.is_file():
@@ -1797,7 +1807,7 @@ def delete_directory(vault_name: str, path: str, *, recursive: bool = True) -> d
``invalid_arguments`` (400) when targeting the vault root.
"""
root = get_vault_root(vault_name)
_ensure_writable(root)
_ensure_writable(root, vault_name)
dir_path = resolve_safe_path(root, path)
if not dir_path.exists() or not dir_path.is_dir():
@@ -1855,7 +1865,7 @@ def restore_backup(
or ``read_only`` (403).
"""
root = get_vault_root(vault_name)
_ensure_writable(root)
_ensure_writable(root, vault_name)
file_path = resolve_safe_path(root, path)
backup_dir = get_backup_dir(vault_name, path)
@@ -1931,7 +1941,7 @@ def save_raw_file(
Dict with ``success``, ``vault``, ``path``, and ``size``.
"""
root = get_vault_root(vault_name)
_ensure_writable(root)
_ensure_writable(root, vault_name)
file_path = resolve_safe_path(root, path)
_validate_extension(file_path, allow_images=True, allow_docs=allow_docs)
@@ -1980,7 +1990,7 @@ def batch_upload_files(
and ``errors`` (list of error dicts).
"""
root = get_vault_root(vault_name)
_ensure_writable(root)
_ensure_writable(root, vault_name)
clean_target = (target_dir or "").strip().strip("/\\")
uploaded: list[str] = []
+2 -1
View File
@@ -215,8 +215,9 @@ def list_paths(vault: str, limit: int = 5000) -> dict[str, Any]:
from datetime import datetime
from backend.indexer import get_vault_data, path_index
from backend.services.vaults import chemin_exclu
entries = path_index.get(vault, [])
entries = [e for e in path_index.get(vault, []) if not chemin_exclu(vault, e["path"])]
meta: dict[str, dict[str, Any]] = {}
data = get_vault_data(vault)
if data:
+71 -1
View File
@@ -7,6 +7,7 @@ Single source of truth consumed by both the REST routes (``/api/vaults``,
from __future__ import annotations
import fnmatch
from datetime import UTC
from pathlib import Path
from typing import Any
@@ -15,6 +16,68 @@ from backend.services.errors import ServiceError
from backend.services.paths import resolve_safe_path
def chemin_exclu_dans_config(config: dict[str, Any] | None, rel_path: str) -> bool:
"""True si *rel_path* est exclu par la config d'exclusions d'un vault.
Deux mécanismes, lus dans le dict ``config`` du vault (alimenté par
``DIR_N_IGNORED_DIRS`` / ``DIR_N_IGNORED_PATHS``, vaults DIR seulement) :
- ``ignoredDirs`` : le nom d'un segment du chemin (à toute profondeur)
figure dans l'ensemble — ex. ``.home`` cache ``a/.home/x`` ;
- ``ignoredPaths`` : motifs fnmatch testés contre le chemin relatif
POSIX **et chacun de ses préfixes** — un dossier qui matche cache
donc tout son sous-arbre, ex. ``Executeurs/*/data`` cache aussi
``Executeurs/Hermes/data/config.md``.
Tolérant : config absente/vide ou chemin vide → False.
"""
if not config or not rel_path:
return False
rel = str(rel_path).replace("\\", "/").strip("/")
if not rel:
return False
segments = rel.split("/")
ignored_dirs = config.get("ignoredDirs") or []
if any(seg in ignored_dirs for seg in segments):
return True
prefixes = ["/".join(segments[:i]) for i in range(1, len(segments) + 1)]
for pattern in config.get("ignoredPaths") or []:
pat = str(pattern).replace("\\", "/").strip("/")
if pat and any(fnmatch.fnmatchcase(prefix, pat) for prefix in prefixes):
return True
return False
def chemin_exclu(vault_name: str, rel_path: str) -> bool:
"""True si *rel_path* est exclu dans le vault *vault_name*.
Lit la config du vault via l'index (``get_vault_data``). Tolérant :
vault inconnu ou sans config d'exclusions → False.
"""
try:
from backend.indexer import get_vault_data
data = get_vault_data(vault_name)
except Exception:
return False
if not data:
return False
return chemin_exclu_dans_config(data.get("config") or {}, rel_path)
def vault_readonly(vault_name: str) -> bool:
"""True si le vault est déclaré en lecture seule (DIR_N_READONLY)."""
try:
from backend.indexer import get_vault_data
data = get_vault_data(vault_name)
except Exception:
return False
if not data:
return False
return bool((data.get("config") or {}).get("readonly", False))
def list_accessible_vaults(user: dict[str, Any]) -> list[dict[str, Any]]:
"""Return the vaults *user* may access, with summary metadata."""
from backend.auth.middleware import check_vault_access
@@ -29,6 +92,7 @@ def list_accessible_vaults(user: dict[str, Any]) -> list[dict[str, Any]]:
"file_count": len(data.get("files", [])),
"tag_count": len(data.get("tags", {})),
"type": data.get("config", {}).get("type", "VAULT"),
"readonly": bool(data.get("config", {}).get("readonly", False)),
})
return result
@@ -58,7 +122,7 @@ def browse_directory(vault_name: str, path: str = "") -> dict[str, Any]:
root = get_vault_root(vault_name)
target = resolve_safe_path(root, path) if path else root.resolve()
if not target.exists():
if not target.exists() or (path and chemin_exclu(vault_name, path)):
raise ServiceError(
f"Path not found: {path}",
code="not_found",
@@ -74,12 +138,15 @@ def browse_directory(vault_name: str, path: str = "") -> dict[str, Any]:
if hide_hidden and entry.name.startswith("."):
continue
rel = str(entry.relative_to(root)).replace("\\", "/")
if chemin_exclu(vault_name, rel):
continue
if entry.is_dir():
# Count only direct children (files and subdirs) for performance.
try:
file_count = sum(
1 for child in entry.iterdir()
if (not hide_hidden or not child.name.startswith("."))
and not chemin_exclu(vault_name, f"{rel}/{child.name}")
and (child.is_file() and (child.suffix.lower() in SUPPORTED_EXTENSIONS or child.name.lower() in ("dockerfile", "makefile"))
or child.is_dir())
)
@@ -200,6 +267,9 @@ def list_all_files(
stat = entry.stat()
rel_path = str(entry.relative_to(root)).replace("\\", "/")
if chemin_exclu(vault_name, rel_path):
continue
if recursive and dir_prefix:
try:
rel_to_dir = str(entry.parent.relative_to(dir_path)).replace("\\", "/")
+1 -1
View File
@@ -2626,7 +2626,7 @@ dependencies = [
[[package]]
name = "obsigate-desktop"
version = "2.82.3"
version = "2.83.4"
dependencies = [
"chrono",
"env_logger",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "obsigate-desktop"
version = "2.82.3"
version = "2.83.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.3",
"version": "2.83.4",
"identifier": "com.obsigate.desktop",
"build": {
"frontendDist": "../frontend",
+10
View File
@@ -73,6 +73,16 @@ configurée (sinon 503 `kek_absente`).
SSO : une session utilisateur, même admin, ne distribue jamais
(401 `consommateur_invalide`). Il n'existe pas d'endpoint de
distribution par session (voir §6).
- Révélation manuelle (panneau détail, hors distribution) : `POST
/api/coffres/{slug}/secrets/{nom}/reveler` rend la valeur au
gestionnaire du coffre authentifié par sa session (propriétaire,
ou admin sur coffre d'instance), pour l'afficher ou la copier.
Ce n'est pas une distribution : aucun octroi n'est créé, aucun
consommateur ne reçoit la valeur, chaque révélation est auditée
(`revelation_manuelle`) et met à jour la dernière utilisation.
Un lecteur par octroi utilisateur (métadonnées visibles) reçoit
403. L'ancienne copie manuelle par consommateur temporaire a
été supprimée (elle échouait : 403 octroi_absent).
- Ajouter une app = enregistrer un consommateur + créer des octrois,
**sans modification du coffre** et sans toucher à ses secrets.
+1 -1
View File
@@ -1,6 +1,6 @@
# ObsiGate — Roadmap
> **Version :** 2.82.3 | **Dernière mise à jour :** 2026-10-28
> **Version :** 2.83.4 | **Dernière mise à jour :** 2026-11-05
> **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)**
+79
View File
@@ -0,0 +1,79 @@
# Vault DIR « Spark » — exclusions par vault et lecture seule déclarable
## Objectif
Permettre d'exposer dans ObsiGate des répertoires techniques (code du
service d'agents, workspaces du compagnon, coquilles d'exécuteurs) sous un
vault `DIR` monté en lecture seule — le vault « Spark » — sans jamais
rendre visibles leurs zones sensibles (profils et clés des exécuteurs,
`.home` des workspaces, `.venv`, etc.).
Avant ce chantier, deux manques le bloquaient proprement :
1. Les exclusions n'existaient qu'au niveau de l'indexeur (`IGNORED_DIRS`,
global) : la navigation et la lecture montraient le disque tel quel.
2. La lecture seule d'un vault n'était ni déclarable ni affichée : les
menus contextuels proposaient renommer / déplacer / supprimer, qui
échouaient ensuite côté serveur (montage `:ro`).
## Contrat de configuration (vaults DIR seulement)
Lues par `load_vault_config()` (`backend/indexer.py`, boucle `DIR_N`),
rangées dans le dict `config` du vault ; les vaults `VAULT` ne sont pas
touchés et les défauts (vide / faux) conservent le comportement actuel :
| Variable | Forme | Effet |
|---|---|---|
| `DIR_N_READONLY` | `true` / `false` | Mutations refusées en 403 `read_only` ; `readonly: true` exposé par `/api/vaults` ; l'UI masque les actions d'écriture |
| `DIR_N_IGNORED_DIRS` | `.home,.venv` | Noms de dossiers cachés à toute profondeur, en plus du global |
| `DIR_N_IGNORED_PATHS` | `Executeurs/*/data,Executeurs/*/.env` | Motifs fnmatch sur le chemin relatif POSIX ; un dossier qui matche cache tout son sous-arbre |
Exemple visé pour le vault Spark :
```
DIR_1_NAME=Spark
DIR_1_PATH=/vaults/Spark
DIR_1_READONLY=true
DIR_1_IGNORED_DIRS=.home,.venv,__pycache__,node_modules,.git
DIR_1_IGNORED_PATHS=Executeurs/*/data,Executeurs/*/.env,*/compagnon.json
```
(le montage lui-même — volume `:ro`, arborescence assemblée sur l'hôte —
relève de la configuration de déploiement, pas d'ObsiGate.)
## Implémentation
- **Helper central** : `chemin_exclu(vault_name, rel_path)` et
`chemin_exclu_dans_config(config, rel_path)` dans
`backend/services/vaults.py` — segment présent dans `ignoredDirs`, ou
fnmatch du chemin ou d'un de ses préfixes contre `ignoredPaths`.
Tolérant si vault / config absents. `vault_readonly(vault_name)` au
même endroit.
- **Navigation** : `browse_directory` filtre les items exclus (et leurs
compteurs d'enfants) ; naviguer directement dans un chemin exclu
répond 404. `list_all_files` et `list_paths` filtrent aussi.
- **Lecture** : un chemin exclu est indiscernable d'un chemin inexistant
(404, sans détail sur la règle) — `read_raw_file` / `read_file_text`
(`backend/services/files.py`), route `GET /api/file/{vault}` et route
de téléchargement (`backend/routers/files_read.py`).
- **Indexation** : le scan (`_scan_vault`, qui reçoit le `vault_cfg`)
élague les dossiers exclus et saute les fichiers exclus, en plus du
global `IGNORED_DIRS` inchangé.
- **Lecture seule** : `_ensure_writable(root, vault_name)` dans
`backend/services/mutations.py` refuse d'abord sur la déclaration
(`DIR_N_READONLY`), avant le garde filesystem existant — toutes les
mutations du service (créer, éditer, renommer, déplacer, supprimer,
upload, xlsx/csv) passent par ce point.
- **API / UI** : `VaultInfo.readonly` (`backend/schemas.py`) rempli par
`list_accessible_vaults` ; côté frontend, `vaultIsReadonly(nom)` dans
`frontend/js/sidebar.js` alimente `ContextMenuManager`,
`attachTreeItemActionButton` et `attachTreeItemLongPress` pour le
vault et ses dossiers / fichiers (aucun autre fichier JS touché).
## Tests
`tests/test_spark_vault_dir.py` : parsing des trois variables, helper
`chemin_exclu` (noms et motifs), navigation filtrée, lectures exclues en
404 (vue fichier, raw, téléchargement), fichier non exclu lisible,
indexation filtrée, mutation refusée en 403 sur vault readonly et
`/api/vaults` exposant `readonly=true`.
+71 -13
View File
@@ -7,14 +7,36 @@
menu des applications) ; l'avatar est un vrai <button>.
Correctif fusion #70 (SANS bump) : bouton trois points et menu par
défaut supprimés ; le menu de la marque gagne « Console Spark ».
v0.1.10 : rose --agent → rouge Spark #c00000 ; marque = logo Spark ;
bouton Spark supprimé (remplacé par l'avatar flottant bas-centre) ;
Alertes icône seule.
v0.1.11 : marque = étoile Spark monochrome (blanche barre foncée,
noire barre claire) ; anneau rouge #c00000 autour de l'avatar flottant.
v0.2.0 : nouvel onglet « ShaarIt » (data-app="shaarit", --app émeraude
#059669, logo logos/shaarit.svg via {{LOGO_SHAARIT}}) avant l'onglet
agent ; GARDE data-theme : les blocs prefers-color-scheme ne
s'appliquent que si <html> n'a AUCUN attribut data-theme
(html:not([data-theme])) — avant ce correctif, un hôte ayant choisi
le clair via data-theme="light" héritait quand même du sombre
système (piège constaté dans les copies vendangées de
Kanbante/Flowdeck ; les re-vendanges de la v0.2.0 le corrigent).
v0.3.0 (HUB-1) : aucun style nouveau — les liens du Hub du menu
compte (Télécharger, À Propos, Aides, Profil) réutilisent
.draco-menu-item (déjà ancre-compatible : text-decoration none)
et le séparateur .draco-menu-sep du menu des applications.
v0.4.0 (HUB-11) : .draco-menu-icone — les icônes monoline des
items du menu compte (SVG inline construit par le JS) : 18 px,
replacées à gauche du libellé par order: -1 (l'item est en
flex), couleur atténuée --ink-2, pleine --ink au survol/focus.
Jetons repris du prototype validé Console v7.2.
Thème : clair par défaut, sombre via [data-theme="dark"] sur <html>
ou prefers-color-scheme si l'app intégratrice ne force rien. */
ou prefers-color-scheme si l'app intégratrice ne force rien
(aucun attribut data-theme sur <html>). */
.draco-bar {
--bg: #f2f2f0; --surface: #ffffff; --surface-2: #e9eae6; --surface-3: #f6f6f3;
--ink: #1b1d1a; --ink-2: #5c615a; --ink-3: #787d75; --line: #dedfd9;
--agent: #c2437e; --danger: #cf4444;
--agent: #c00000; --danger: #cf4444;
position: relative;
height: 60px; box-sizing: border-box;
display: grid; grid-template-columns: auto minmax(200px, 520px) auto;
@@ -22,8 +44,12 @@
background: var(--surface); border-bottom: 1px solid var(--line);
font: 500 14px/1.3 system-ui, "Segoe UI", sans-serif; color: var(--ink);
}
/* Garde data-theme (v0.2.0) : le sombre système ne s'applique QUE si
* l'hôte n'a posé aucun data-theme sur <html>. Sans cette garde, un
* hôte en data-theme="light" sur un système sombre affichait la barre
* sombre malgré son choix explicite. */
@media (prefers-color-scheme: dark) {
.draco-bar { --bg:#101211; --surface:#1a1c1a; --surface-2:#242624; --surface-3:#20221f;
html:not([data-theme]) .draco-bar { --bg:#101211; --surface:#1a1c1a; --surface-2:#242624; --surface-3:#20221f;
--ink:#eceee9; --ink-2:#a6aba2; --ink-3:#8b9089; --line:#2e302c; }
}
[data-theme="dark"] .draco-bar { --bg:#101211; --surface:#1a1c1a; --surface-2:#242624; --surface-3:#20221f;
@@ -37,7 +63,15 @@
.draco-brand { display: flex; align-items: center; gap: 7px; color: var(--ink); text-decoration: none; font-weight: 800; cursor: pointer; border-radius: 10px; padding: 4px 6px; margin: -4px -6px; }
.draco-brand:hover { background: var(--surface-2); }
.draco-brand:focus-visible { outline: 2px solid var(--agent); outline-offset: 2px; }
.draco-brand-mark { color: var(--agent); font-size: 24px; line-height: 1; }
.draco-brand-mark { line-height: 0; }
/* Marque monochrome v0.1.11 : étoile blanche extraite du logo rouge,
fond transparent — noire sur barre claire (filtre), blanche sur
barre foncée (filtre annulé par les règles thème ci-dessous). */
.draco-brand-mark img { width: 26px; height: 26px; object-fit: cover; display: block; filter: brightness(0); }
@media (prefers-color-scheme: dark) {
html:not([data-theme]) .draco-bar .draco-brand-mark img { filter: none; }
}
[data-theme="dark"] .draco-bar .draco-brand-mark img { filter: none; }
/* Menu des applications — construit par le JS depuis les onglets */
.draco-apps-menu {
@@ -64,6 +98,15 @@
text-decoration: none; cursor: pointer;
}
.draco-menu-item:hover, .draco-menu-item:focus-visible { background: var(--surface-2); }
/* Icônes monoline du menu compte (v0.4.0) : appendues après le
libellé dans le DOM, replacées à gauche par order: -1. */
.draco-menu-icone {
order: -1; flex: none; display: inline-flex; align-items: center;
width: 18px; height: 18px; color: var(--ink-2);
}
.draco-menu-icone svg { display: block; width: 18px; height: 18px; }
.draco-menu-item:hover .draco-menu-icone,
.draco-menu-item:focus-visible .draco-menu-icone { color: var(--ink); }
.draco-menu-sep { height: 1px; margin: 6px 10px; background: var(--line); }
.draco-compte-nom {
padding: 8px 10px 10px; font-weight: 700; font-size: 14px;
@@ -94,6 +137,13 @@
width: 30px; height: 30px; flex: none; display: grid; place-items: center;
border-radius: 9px; background: var(--app); color: #fff; font-size: 15px;
}
/* Icônes logos (2026-10-29, corrigé le même jour) : l'image du logo
* fourni, telle quelle, remplit la tuile 30 px — jamais de glyphe
* texte, jamais de redessin. Le fond de la tuile est transparent :
* les logos PNG portent déjà leur fond (dégradé), le SVG TikTak
* reste sur le fond de la barre. */
.draco-apptab-ico { background: transparent; }
.draco-apptab-ico img { width: 30px; height: 30px; display: block; border-radius: 9px; object-fit: cover; }
.draco-apptab-txt { display: none; flex-direction: column; line-height: 1.15; }
.draco-apptab-txt strong { font-size: 14px; font-weight: 700; }
.draco-apptab-txt small { font-size: 11.5px; color: var(--ink-2); }
@@ -131,18 +181,23 @@
/* Alertes — la cloche ne disparaît JAMAIS (correctif v7.2) */
.draco-alertes {
position: relative; display: flex; align-items: center; gap: 7px; height: 38px;
padding: 0 11px; border-radius: 10px; border: 1px solid var(--line);
position: relative; display: flex; align-items: center; height: 38px;
padding: 0 9px; border-radius: 10px; border: 1px solid var(--line);
background: transparent; color: var(--ink); font: inherit; cursor: pointer;
}
/* Spark */
.draco-spark {
position: relative; display: flex; align-items: center; gap: 7px; height: 38px;
padding: 0 14px; border: 0; border-radius: 10px; cursor: pointer;
background: var(--agent); color: #fff; font: inherit; font-weight: 700;
/* Bouton avatar Spark flottant — bas-centre, ouvre le panneau Spark
de l'hôte via data-act="spark" (délégation globale existante).
La console Spark ne l'inclut pas : son fragment est propre. */
.draco-spark-flottant {
position: fixed; left: 50%; bottom: 20px; transform: translateX(-50%);
z-index: 65; width: 64px; height: 64px; padding: 0; border: 0;
border-radius: 50%; background: transparent; cursor: pointer;
box-shadow: 0 8px 28px rgb(0 0 0 / .45);
}
.draco-spark .draco-badge { border-color: var(--agent); }
.draco-spark-flottant img { width: 64px; height: 64px; border-radius: 50%; object-fit: cover; display: block; box-sizing: border-box; border: 3px solid var(--agent); }
.draco-spark-flottant:hover { transform: translateX(-50%) scale(1.05); }
.draco-spark-flottant:focus-visible { outline: 2px solid var(--agent); outline-offset: 3px; }
.draco-icon-btn {
width: 38px; height: 38px; border-radius: 10px; border: 1px solid transparent;
@@ -169,7 +224,7 @@
.draco-bar { grid-template-columns: auto minmax(200px, 440px) auto; }
}
@media (max-width: 1200px) {
.draco-search-txt, .draco-search kbd, .draco-alertes-txt, .draco-spark-txt { display: none; }
.draco-search-txt, .draco-search kbd { display: none; }
.draco-search { width: 38px; padding: 0; justify-content: center; }
.draco-bar { grid-template-columns: auto auto 1fr; }
.draco-bar-droite { justify-content: flex-end; }
@@ -193,6 +248,9 @@
.draco-bar[data-current="flowdeck"] .draco-apptab[data-app="flowdeck"],
.draco-bar[data-current="tiktak"] .draco-apptab[data-app="tiktak"],
.draco-bar[data-current="kanbante"] .draco-apptab[data-app="kanbante"],
/* v0.2.0 : sur ShaarIt elle-même (data-current="shaarit"), c'est
l'onglet ShaarIt qui reste visible en mobile. */
.draco-bar[data-current="shaarit"] .draco-apptab[data-app="shaarit"],
/* Chantier 5 : sur la console elle-même (data-current="agent",
programme console #71), c'est l'onglet Console Spark qui reste
visible en mobile — les 4 hôtes gardent leur propre courant. */
+189 -26
View File
@@ -8,7 +8,34 @@
* correctif fusion #70 (SANS bump de version, publication séparée) :
* bouton trois points et son menu par défaut SUPPRIMÉS, « Console Spark »
* ajoutée au menu de la marque, fermeture/focus corrigés (voir
* « Menu des applications », « Focus » et « Écouteurs globaux »).
* « Menu des applications », « Focus » et « Écouteurs globaux ») ;
* v0.2.0 : 5e application ShaarIt — clé urls.shaarit, défaut officiel
* https://shaarit.dracodev.net/, dérivation -dev shaarit-dev (même
* contrat que les autres apps), entrée APP_LABELS ; l'onglet
* (data-app="shaarit", après Kanbante, avant agent) est repris
* automatiquement par la construction du menu de la marque.
* v0.3.0 (carte HUB-1, fusion #115) : le Hub, point central de la
* suite — nouvelle clé d'URL « hub » (jeton {{URL_HUB}} côté hôte,
* DRACO_BAR_CONFIG.urls.hub), défaut officiel
* https://hub.dracodev.net/, dérivation -dev hub-dev comme les
* sœurs. Le menu compte gagne trois vrais liens (patron
* buildAppsMenu : <a href>, classe draco-menu-item) — Télécharger
* → <hub>/telecharger, À Propos → <hub>/a-propos, Aides →
* <hub>/aide — et « Profil » devient lui aussi un lien, vers
* <hub>/profil (il n'est plus relayé aux hôtes : un seul Profil,
* celui du Hub). « Déconnexion » reste l'action relayée
* (data-compte-act="deconnexion" / onAction), inchangée.
* refreshMenuHrefs() resynchronise aussi les liens du Hub à
* l'ouverture des menus. Le Hub n'a pas d'onglet dans la barre :
* comme spark, sa clé est résolue et exposée via DRACO_BAR.urls,
* sans étape « href rendu » (voir « URL » ci-dessous).
* v0.4.0 (carte HUB-11, fusion #125) : icônes monoline dans le
* menu compte — chaque item (les 4 liens du Hub et « Déconnexion »)
* reçoit un glyphe SVG inline dans le style des icônes du rail de
* la console (contour, trait fin régulier, currentColor, aucun
* remplissage), aligné à gauche du libellé via .draco-menu-icone.
* L'en-tête d'identité ne reçoit pas d'icône. Aucun changement de
* comportement : liens résolus et Déconnexion relayée inchangés.
*
* Aucun gestionnaire inline : un seul écouteur délégué sur le document
* (patron validé par le prototype — les onclick du HTML généré sont
@@ -26,8 +53,10 @@
* flowdeck: "https://flowdeck.dracodev.net/",
* tiktak: "https://tiktak.dracodev.net/",
* kanbante: "https://kanbante.dracodev.net/",
* shaarit: "https://shaarit.dracodev.net/",
* agent: "https://agent.dracodev.net/",
* spark: "https://spark.dracodev.net/"
* spark: "https://spark.dracodev.net/",
* hub: "https://hub.dracodev.net/"
* },
* onAction: function (act, el) { … } // l'app relaie les actions
* } // qu'elle ne connaît pas
@@ -54,10 +83,19 @@
* La clé spark ne correspond à aucun onglet de la barre : elle est
* résolue et exposée via DRACO_BAR.urls pour le panneau Spark
* (panneau-agent), qui suit le même contrat d'injection. La clé agent,
* elle, a un onglet depuis le chantier 5 (ordre : ObsiGate, Flowdeck,
* TikTak, Kanbante, Console Spark) : elle suit donc l'étape « href
* elle, a un onglet depuis le chantier 5 (ordre, v0.2.0 : ObsiGate,
* Flowdeck, TikTak, Kanbante, ShaarIt, Console Spark) : elle suit
* donc l'étape « href
* rendu » comme les autres onglets, en plus d'alimenter le menu de
* la marque et DRACO_BAR.urls.
* la marque et DRACO_BAR.urls. Même résolution pour shaarit (v0.2.0) :
* explicite > dérivation -dev shaarit-dev > href rendu
* ({{URL_SHAARIT}}) > défaut officiel.
* Cas hub (v0.3.0) : PAS d'onglet, donc pas d'étape « href rendu » —
* explicite (DRACO_BAR_CONFIG.urls.hub, alimenté par le jeton
* {{URL_HUB}} remplacé côté serveur par l'hôte) > dérivation -dev
* hub-dev sur hôte -dev > défaut officiel
* https://hub.dracodev.net/. La base résolue sert les quatre liens
* du menu compte (telecharger, a-propos, aide, profil).
* Actions émises (data-act) : rail, palette, domaine (data-dom),
* alertes, spark, rappel-entete. L'app intégratrice décide de chacune
* (ouvrir la palette, le panneau Spark, changer de domaine…).
@@ -132,6 +170,14 @@
* mais le menu affiche l'identité même sans gestionnaire hôte.
* Accessibilité : vrais <button>, aria-haspopup / aria-expanded,
* role="menu" / "menuitem", fermeture par Échap et clic extérieur.
* Menu compte (v0.3.0) — Profil n'est PLUS relayé : c'est un lien
* vers <hub>/profil, comme Télécharger / À Propos / Aides (liens
* Hub, data-hub-link, href resynchronisés à chaque ouverture via
* refreshCompteHrefs / refreshMenuHrefs). Seule « Déconnexion »
* reste relayée à l'hôte (data-compte-act="deconnexion"), inchangée.
* Les hôtes qui traitaient onAction("profil") (Kanbante → /profile)
* voient ce comportement remplacé par le lien Hub — décision du
* plan Hub : un seul Profil, celui du Hub.
*/
(function () {
"use strict";
@@ -144,8 +190,10 @@
flowdeck: "https://flowdeck.dracodev.net/",
tiktak: "https://tiktak.dracodev.net/",
kanbante: "https://kanbante.dracodev.net/",
shaarit: "https://shaarit.dracodev.net/",
agent: "https://agent.dracodev.net/",
spark: "https://spark.dracodev.net/"
spark: "https://spark.dracodev.net/",
hub: "https://hub.dracodev.net/"
};
/* Dérivation -dev (v0.1.6) — appliquée UNIQUEMENT quand l'hôte courant
@@ -157,12 +205,14 @@
flowdeck: "https://flowdeck-dev.dracodev.net/",
tiktak: "https://tiktak-dev.dracodev.net/",
kanbante: "https://kanbante-dev.dracodev.net/",
agent: "https://agent-dev.dracodev.net/"
shaarit: "https://shaarit-dev.dracodev.net/",
agent: "https://agent-dev.dracodev.net/",
hub: "https://hub-dev.dracodev.net/"
};
var APP_LABELS = {
obsigate: "ObsiGate", flowdeck: "Flowdeck",
tiktak: "TikTak", kanbante: "Kanbante"
tiktak: "TikTak", kanbante: "Kanbante", shaarit: "ShaarIt"
};
/* Vrai si hostname est un hôte -dev dracodev.net : première étiquette
@@ -296,6 +346,28 @@
menu.querySelectorAll(".draco-apps-item").forEach(function (a) {
if (a.dataset.app && resolvedUrls[a.dataset.app]) a.setAttribute("href", resolvedUrls[a.dataset.app]);
});
/* v0.3.0 : les liens du Hub (menu compte) sont resynchronisés
* eux aussi à l'ouverture d'un menu, pour refléter setUrls et
* la dérivation -dev même s'ils ont été construits avant. */
refreshCompteHrefs();
}
/* Liens du Hub dans le menu compte (v0.3.0) : chemin → libellé.
* « Profil » est le quatrième lien (vers <hub>/profil) ; il
* remplace l'ancien item relayé « profil ». */
var HUB_LINKS = [
["telecharger", "Télécharger"],
["a-propos", "À Propos"],
["aide", "Aides"],
["profil", "Profil"]
];
function hubHref(path) {
return withSlash(resolvedUrls.hub || DEFAULT_URLS.hub) + path;
}
function refreshCompteHrefs() {
if (!compteMenu) return;
compteMenu.querySelectorAll("[data-hub-link]").forEach(function (a) {
a.setAttribute("href", hubHref(a.getAttribute("data-hub-link")));
});
}
function togglePopup(menu, btn, force) {
if (!menu) return;
@@ -365,11 +437,15 @@
if (badge) a.appendChild(badge.cloneNode(true));
appsMenu.appendChild(a);
});
/* Entrée « Console Spark » (#70) : après Kanbante, vers la console
/* Entrée « Console Spark » (#70) : après les onglets d'apps
* (ObsiGate…ShaarIt, v0.2.0), vers la console
* de l'agent commun (URL agent résolue — explicite > -dev >
* défaut prod ; aucun hôte spark-dev inventé). data-app="agent"
* l'intègre à refreshMenuHrefs/applyUrls/setCurrent comme les
* autres items ; l'icône ✦ reprend la couleur agent du CSS. */
* autres items ; son icône CLONE celle de l'onglet agent
* (2026-10-29, logos barre-apps, corrigé le même jour) : le logo
* fourni, tel quel, jamais redessiné — le CSS --agent (rose) des
* autres éléments de la barre n'est pas changé ici. */
var sep = document.createElement("div");
sep.className = "draco-menu-sep";
sep.setAttribute("aria-hidden", "true");
@@ -379,10 +455,18 @@
sparkItem.href = resolvedUrls.agent || "#";
sparkItem.dataset.app = "agent";
sparkItem.setAttribute("role", "menuitem");
var sparkIco = document.createElement("span");
sparkIco.className = "draco-apptab-ico";
sparkIco.setAttribute("style", "--app:#c2437e");
sparkIco.textContent = "✦";
var sparkIco;
var agentTab = document.querySelector('.draco-apptab[data-app="agent"]');
var agentIco = agentTab ? agentTab.querySelector(".draco-apptab-ico") : null;
if (agentIco) {
sparkIco = agentIco.cloneNode(true);
} else {
sparkIco = document.createElement("span");
sparkIco.className = "draco-apptab-ico";
sparkIco.setAttribute("style", "--app:#d21f2c");
sparkIco.setAttribute("data-icon", "agent");
sparkIco.textContent = "✦";
}
sparkItem.appendChild(sparkIco);
var sparkTxt = document.createElement("span");
sparkTxt.className = "draco-apps-item-txt";
@@ -406,10 +490,74 @@
if (!appsMenu.hidden) lastOpener = brand;
}
/* Menu compte (v0.1.6) — en-tête d'identité + Profil / Déconnexion
* relayés à l'hôte. L'en-tête est rafraîchi à chaque ouverture, pour
* refléter un title posé après coup par l'hôte (ex. ObsiGate, qui
* remplit l'avatar depuis /api/auth/me après chargement). */
/* Icônes monoline du menu compte (v0.4.0) — glyphes contour
* dans le style des icônes du rail de la console : viewBox 24,
* stroke currentColor, remplissage nul, extrémités rondes.
* Chaque glyphe est une liste de primitives [balise, attributs]
* construites en DOM (jamais d'innerHTML), comme le reste du
* fichier. Le libellé reste le nœud texte de l'item ; l'icône,
* appendue après, est replacée à gauche par le CSS (order: -1
* sur .draco-menu-icone, l'item étant déjà en flex). */
var MENU_ICONES = {
/* Flèche vers le bas dans un plateau. */
"telecharger": [
["path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }],
["polyline", { points: "7 10 12 15 17 10" }],
["line", { x1: "12", y1: "15", x2: "12", y2: "3" }]
],
/* Cercle d'information. */
"a-propos": [
["circle", { cx: "12", cy: "12", r: "9" }],
["line", { x1: "12", y1: "11", x2: "12", y2: "16" }],
["line", { x1: "12", y1: "8", x2: "12.01", y2: "8" }]
],
/* Cercle d'interrogation. */
"aide": [
["circle", { cx: "12", cy: "12", r: "9" }],
["path", { d: "M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" }],
["line", { x1: "12", y1: "17", x2: "12.01", y2: "17" }]
],
/* Personne : tête + épaules. */
"profil": [
["path", { d: "M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" }],
["circle", { cx: "12", cy: "7", r: "4" }]
],
/* Flèche sortant d'un cadre (porte). */
"deconnexion": [
["path", { d: "M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" }],
["polyline", { points: "16 17 21 12 16 7" }],
["line", { x1: "21", y1: "12", x2: "9", y2: "12" }]
]
};
function menuIcone(nom) {
var span = document.createElement("span");
span.className = "draco-menu-icone";
span.setAttribute("aria-hidden", "true");
var svg = document.createElement("svg");
svg.setAttribute("viewBox", "0 0 24 24");
svg.setAttribute("fill", "none");
svg.setAttribute("stroke", "currentColor");
svg.setAttribute("stroke-width", "1.8");
svg.setAttribute("stroke-linecap", "round");
svg.setAttribute("stroke-linejoin", "round");
svg.setAttribute("focusable", "false");
(MENU_ICONES[nom] || []).forEach(function (spec) {
var el = document.createElement(spec[0]);
for (var k in spec[1]) el.setAttribute(k, spec[1][k]);
svg.appendChild(el);
});
span.appendChild(svg);
return span;
}
/* Menu compte (v0.1.6 ; v0.3.0 pour les liens) — en-tête
* d'identité, puis les liens du Hub (Télécharger, À Propos,
* Aides, Profil — vrais <a href>, même patron que le menu des
* applications), un séparateur, puis « Déconnexion » relayée à
* l'hôte. L'en-tête et les href sont rafraîchis à chaque
* ouverture, pour refléter un title posé après coup par l'hôte
* (ex. ObsiGate, qui remplit l'avatar depuis /api/auth/me après
* chargement) et la résolution d'URL la plus récente. */
function buildCompteMenu() {
if (!bar || !avatarBtn || compteMenu) return;
compteMenu = document.createElement("div");
@@ -420,15 +568,28 @@
var head = document.createElement("div");
head.className = "draco-compte-nom";
compteMenu.appendChild(head);
[["profil", "Profil"], ["deconnexion", "Déconnexion"]].forEach(function (pair) {
var b = document.createElement("button");
b.type = "button";
b.className = "draco-menu-item";
b.setAttribute("data-compte-act", pair[0]);
b.setAttribute("role", "menuitem");
b.textContent = pair[1];
compteMenu.appendChild(b);
HUB_LINKS.forEach(function (pair) {
var a = document.createElement("a");
a.className = "draco-menu-item";
a.href = hubHref(pair[0]);
a.setAttribute("data-hub-link", pair[0]);
a.setAttribute("role", "menuitem");
a.textContent = pair[1];
a.appendChild(menuIcone(pair[0]));
compteMenu.appendChild(a);
});
var sep = document.createElement("div");
sep.className = "draco-menu-sep";
sep.setAttribute("aria-hidden", "true");
compteMenu.appendChild(sep);
var b = document.createElement("button");
b.type = "button";
b.className = "draco-menu-item";
b.setAttribute("data-compte-act", "deconnexion");
b.setAttribute("role", "menuitem");
b.textContent = "Déconnexion";
b.appendChild(menuIcone("deconnexion"));
compteMenu.appendChild(b);
bar.appendChild(compteMenu);
}
function refreshCompteMenu() {
@@ -436,6 +597,7 @@
var head = compteMenu.querySelector(".draco-compte-nom");
var name = displayName();
if (head) head.textContent = name || "Compte";
refreshCompteHrefs();
}
function toggleCompteMenu(force) {
buildCompteMenu();
@@ -532,6 +694,7 @@
resolvedUrls = resolveUrls();
window.DRACO_BAR.urls = resolvedUrls;
applyUrls(resolvedUrls);
refreshCompteHrefs();
}
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

+9
View File
@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 240">
<!-- ShaarIt — signet (bookmark) blanc sur fond émeraude #059669,
couleur signature de l'app dans la barre (#059669, distincte du
teal Kanbante #0d9488). Fond arrondi comme les logos PNG 180 px
des autres apps ; géométrie simple, aucun redessin d'un logo
existant : le signet EST le symbole de l'app (bookmarks). -->
<rect x="8" y="8" width="224" height="224" rx="48" fill="#059669"/>
<path d="M 82 52 L 158 52 L 158 190 L 120 152 L 82 190 Z" fill="#ffffff"/>
</svg>

After

Width:  |  Height:  |  Size: 569 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

+8
View File
@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 240 240">
<!-- TikTak — deux arcs (les deux temps), coche-flèche. Bleu #0076fb, orange #fc750c.
Géométrie mesurée sur le visuel validé par Bruno (variante B). -->
<path d="M 38.2 101.1 A 84 84 0 0 1 201.8 101.1" fill="none" stroke="#0076fb" stroke-width="28" stroke-linecap="round"/>
<path d="M 201.5 140.3 A 84 84 0 0 1 38.5 140.3" fill="none" stroke="#fc750c" stroke-width="28" stroke-linecap="round"/>
<polyline points="80,129 106,146 163,84" fill="none" stroke="#0076fb" stroke-width="24" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M 128 80 L 163 84 L 167 122" fill="none" stroke="#0076fb" stroke-width="22" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 760 B

+49 -80
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>
@@ -333,10 +268,37 @@
<!-- Main Application (wrapped for show/hide) -->
<!-- Barre d'apps commune dracodev — Fusion #215 (ACC-1.1).
Snippet FIGÉ vendangé depuis Projets/dracodev-commons v0.1.6
(ui/barre-apps/) : ne pas modifier la structure ici ; toute
évolution passe par une nouvelle version de commons.
Snippet FIGÉ vendangé depuis Projets/dracodev-commons v0.3.0
(ui/barre-apps/, branche hub-menu-compte-v0-3-0 de la PR
commons #9 — HUB-1, fusion #115) : ne pas modifier la structure
ici ; toute évolution passe par une nouvelle version de commons.
v0.3.0 : menu compte — liens Hub Télécharger / À Propos /
Aides et Profil devenu lien (vers le Hub résolu par
backend/draco_config, clé "hub", passé en urls.hub de la
configuration ci-dessous) ; « Déconnexion » reste relayée.
v0.2.0 : 6e onglet ShaarIt (data-app="shaarit", après Kanbante,
avant l'onglet agent) — son href porte le défaut officiel et
la réécriture DRACO_URLS ci-dessous le résout comme les
autres (config backend/draco_config, dérivation -dev incluse) ;
le CSS vendangé gagne la garde data-theme sur
prefers-color-scheme (html:not([data-theme])).
CSS/JS : /static/draco-barre.css, /static/draco-barre.js.
Icônes logos v0.1.9 (2026-10-29) : les 5 tuiles sont les logos
fournis, tels quels, servis depuis /static/img/logos/ (copie de
ui/barre-apps/logos/ de dracodev-commons) ; l'entrée « Console Spark »
du menu clone l'icône de l'onglet agent (draco-barre.js).
v0.1.10 (2026-10-29) : --agent rose → rouge Spark #c00000, marque ✦
→ logo Spark 26 px, bouton Spark rose supprimé (remplacé par
l'avatar flottant bas-centre /static/img/avatar-spark.png,
data-act="spark" → panneau Spark), Alertes = cloche seule.
v0.1.11 (2026-10-29) : marque → étoile Spark monochrome
/static/img/brand-spark.png 26 px (blanche sur barre foncée
via [data-theme="dark"] d'ObsiGate, noire sur barre claire
via le filtre brightness(0) du CSS vendangé — aucune
surcharge propre nécessaire, la barre suit déjà
data-theme) ; onglet agent GARDANT le logo rouge ;
anneau rouge #c00000 (bordure 3 px) autour de l'avatar
flottant, via le CSS vendangé.
Chantier 5 (fusion #69/#71, sans bump) : 5e onglet Console Spark
(data-app="agent", défaut prod ; la réécriture DRACO_URLS
couvre la clé "agent" comme les autres onglets), après Kanbante ;
@@ -345,29 +307,33 @@
<div class="draco-bar-zone draco-bar-gauche">
<button type="button" class="draco-icon-btn" data-act="rail" aria-label="Barre latérale">☰</button>
<a class="draco-brand" href="/" data-act="apps" aria-haspopup="true" aria-expanded="false" aria-label="dracodev — changer d&#39;application" title="Changer d&#39;application">
<span class="draco-brand-mark">✦</span><span class="draco-brand-name">dracodev</span>
<span class="draco-brand-mark"><img src="/static/img/brand-spark.png" alt="" width="26" height="26"></span><span class="draco-brand-name">dracodev</span>
</a>
<nav class="draco-apptabs" aria-label="Applications">
<a class="draco-apptab" data-app="obsigate" href="https://og.dracodev.net/">
<span class="draco-apptab-ico" style="--app:#7a5af5">▤</span>
<span class="draco-apptab-ico" style="--app:#7a5af5" data-icon="obsigate"><img src="/static/img/logos/obsigate.png" alt="ObsiGate" width="30" height="30"></span>
<span class="draco-apptab-txt"><strong>ObsiGate</strong><small>Notes</small></span>
</a>
<a class="draco-apptab" data-app="flowdeck" href="https://flowdeck.dracodev.net/">
<span class="draco-apptab-ico" style="--app:#2f6fed">▦</span>
<span class="draco-apptab-ico" style="--app:#2f6fed" data-icon="flowdeck"><img src="/static/img/logos/flowdeck.png" alt="Flowdeck" width="30" height="30"></span>
<span class="draco-apptab-txt"><strong>Flowdeck</strong><small>Projets</small></span>
</a>
<a class="draco-apptab" data-app="tiktak" href="https://tiktak.dracodev.net/">
<span class="draco-apptab-ico" style="--app:#e8762c">✓</span>
<span class="draco-apptab-ico" style="--app:#e8762c" data-icon="tiktak"><img src="/static/img/logos/tiktak.svg" alt="TikTak" width="30" height="30"></span>
<span class="draco-apptab-txt"><strong>TikTak</strong><small>Tâches</small></span>
<span class="draco-badge" data-badge="tiktak" hidden>0</span>
</a>
<a class="draco-apptab" data-app="kanbante" href="https://kanbante.dracodev.net/">
<span class="draco-apptab-ico" style="--app:#0d9488">▥</span>
<span class="draco-apptab-ico" style="--app:#0d9488" data-icon="kanbante"><img src="/static/img/logos/kanbante.png" alt="Kanbante" width="30" height="30"></span>
<span class="draco-apptab-txt"><strong>Kanbante</strong><small>Kanban</small></span>
<span class="draco-badge" data-badge="kanbante" hidden>0</span>
</a>
<a class="draco-apptab" data-app="shaarit" href="https://shaarit.dracodev.net/">
<span class="draco-apptab-ico" style="--app:#059669" data-icon="shaarit"><img src="/static/img/logos/shaarit.svg" alt="ShaarIt" width="30" height="30"></span>
<span class="draco-apptab-txt"><strong>ShaarIt</strong><small>Bookmarks</small></span>
</a>
<a class="draco-apptab" data-app="agent" href="https://agent.dracodev.net/">
<span class="draco-apptab-ico" style="--app:#c2437e">✦</span>
<span class="draco-apptab-ico" style="--app:#c00000" data-icon="agent"><img src="/static/img/logos/spark.png" alt="Console Spark" width="30" height="30"></span>
<span class="draco-apptab-txt"><strong>Console Spark</strong><small>Agent commun</small></span>
</a>
</nav>
@@ -388,17 +354,13 @@
</div>
<button type="button" class="draco-alertes" data-act="alertes" aria-label="Alertes unifiées">
<svg viewBox="0 0 24 24" width="19" height="19" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/></svg>
<span class="draco-alertes-txt">Alertes</span>
<span class="draco-badge" data-badge="alertes" hidden>0</span>
</button>
<button type="button" class="draco-spark" data-act="spark" aria-label="Ouvrir Spark, l'agent commun">
<span class="draco-spark-ico">✦</span><span class="draco-spark-txt">Spark</span>
<span class="draco-badge" data-badge="spark" hidden>0</span>
</button>
<button type="button" class="draco-avatar" id="draco-avatar" data-act="compte" aria-label="Compte" aria-haspopup="menu" aria-expanded="false" title="">✦</button>
</div>
</header>
<div class="draco-bar-rappel" data-act="rappel-entete" hidden>✦ dracodev — afficher l'en-tête</div>
<button type="button" class="draco-spark-flottant" data-act="spark" aria-label="Ouvrir Spark, l'agent commun"><img src="/static/img/avatar-spark.png" alt="" width="64" height="64"></button>
<script>
/* Configuration ObsiGate de la barre commune (Fusion #215).
Phase 1 : navigation entre apps et domaine mémorisé
@@ -418,6 +380,13 @@
})();
window.DRACO_BAR_CONFIG = {
current: "obsigate",
/* Barre v0.3.0 (HUB-1) : la base du Hub vient de
window.DRACO_URLS (clé "hub" de backend/draco_config :
DRACO_HUB_URL explicite, dérivation -dev, défaut
officiel) — le Hub n'a pas d'onglet, c'est par
urls.hub que le menu compte construit ses liens
Télécharger / À Propos / Aides / Profil. */
urls: (function () { var u = window.DRACO_URLS && window.DRACO_URLS.hub; return u ? { hub: u.replace(/\/$/, "") + "/" } : {}; })(),
domain: (function () { try { return localStorage.getItem("draco_domain") || "pro"; } catch (e) { return "pro"; } })(),
counts: {},
onAction: function (act, el) {
+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')
+63 -62
View File
@@ -4,9 +4,15 @@
*
* Règles de sécurité appliquées ici (jamais assouplies) :
* - Le backend ne renvoie que des métadonnées (has_value, jamais la valeur,
* jamais un texte chiffré, jamais un nonce). Ce module n'affiche donc
* jamais de valeur de secret après enregistrement : colonne valeur =
* « •••••• » seulement, aucun bouton « afficher ».
* jamais un texte chiffré, jamais un nonce). La valeur d'un secret
* enregistré reste donc masquée (« •••••• ») par défaut ; seul le
* gestionnaire du coffre (propriétaire, ou admin sur coffre
* d'instance) peut la révéler, via POST .../secrets/{nom}/reveler
* (session + gestion, chaque révélation auditée) : bouton
* Afficher/Masquer dans le détail, ou « Copier la valeur » sans
* l'afficher. La valeur est effacée dès qu'elle est masquée ou que
* le détail change ; un simple lecteur par octroi utilisateur ne
* révèle jamais.
* - Aucune valeur ne transite par un titre d'onglet, une URL, un hash, un
* journal console ou un log. Les variables contenant une valeur sont
* effacées dès que l'opération est terminée (champ vidé, variable = null).
@@ -14,10 +20,11 @@
* consommateur (csk_…) et une KEK générée sont rendus UNE fois, dans une
* modale, avec un bouton Copier et un avertissement ; la modale fermée,
* ils sont perdus. Ce ne sont pas des valeurs de secret du coffre.
* - La copie manuelle à usage unique (admin seulement, voir
* _copieManuelle) passe par un consommateur temporaire créé puis révoqué
* immédiatement, avec distribution auditée ; il n'existe pas encore
* d'endpoint de distribution par session utilisateur côté backend.
* - L'ancienne copie manuelle à usage unique par consommateur
* temporaire a été supprimée : elle échouait systématiquement
* (aucun octroi pour le consommateur temporaire, donc 403
* octroi_absent à la distribution). Elle est remplacée par
* « Copier la valeur », qui passe par l'endpoint de révélation.
* - Phase 2 (issue #20) : « Importer l'existant » migre les clés de
* data/api_keys.json vers le coffre d'instance « ObsiGate interne »,
* en deux temps (import, puis purge confirmée). Aucune valeur ne
@@ -52,6 +59,8 @@ let _coffresCache = null;
let _coffresCacheAt = 0;
const CACHE_MS = 30000;
let _valeurAffichee = null; // valeur révélée du détail courant, sinon null
const viewState = {
coffres: [],
currentSlug: null,
@@ -354,6 +363,7 @@ function renderAlerts(host) {
function renderDetail(host) {
const s = viewState.detail;
_valeurAffichee = null;
host.innerHTML = '';
if (!s) {
host.appendChild(text('p', t('coffres.select_secret'), 'coffres-empty'));
@@ -377,7 +387,9 @@ function renderDetail(host) {
const dl = el('dl', { class: 'coffres-dl' }, []);
rows.forEach(([k, v]) => {
dl.appendChild(el('dt', {}, [document.createTextNode(k)]));
dl.appendChild(el('dd', {}, [document.createTextNode(v)]));
const dd = el('dd', {}, [document.createTextNode(v)]);
if (k === t('coffres.value')) dd.id = 'coffres-valeur';
dl.appendChild(dd);
});
host.appendChild(dl);
const actions = el('div', { class: 'coffres-actions' }, []);
@@ -386,11 +398,36 @@ function renderDetail(host) {
actions.appendChild(modalBtn(t('coffres.acces'), '', () => {
document.getElementById('coffres-acces')?.scrollIntoView({ behavior: 'smooth' });
}));
actions.appendChild(modalBtn(t('coffres.manual_copy'), '', () => copieManuelle(s)));
if (!isAdmin()) {
actions.lastChild.disabled = true;
actions.lastChild.title = t('coffres.manual_copy_admin_only');
}
const btnAfficher = modalBtn(t('coffres.show_value'), '', async () => {
if (_valeurAffichee !== null) {
_valeurAffichee = null;
document.getElementById('coffres-valeur').textContent = '••••••';
btnAfficher.textContent = t('coffres.show_value');
return;
}
let valeur;
try {
valeur = await revelerValeur(s);
} catch (e) {
showToast(e.message || t('common.error'), 'error');
return;
}
_valeurAffichee = valeur;
document.getElementById('coffres-valeur').textContent = valeur;
btnAfficher.textContent = t('coffres.hide_value');
});
actions.appendChild(btnAfficher);
actions.appendChild(modalBtn(t('coffres.copy_value'), '', async () => {
let valeur;
try {
valeur = await revelerValeur(s);
} catch (e) {
showToast(e.message || t('common.error'), 'error');
return;
}
await copyText(valeur);
valeur = null;
}));
actions.appendChild(modalBtn(t('coffres.delete'), 'danger', () => supprimerSecret(s)));
host.appendChild(actions);
@@ -819,55 +856,19 @@ function openCreateCoffreModal() {
]);
}
/** Copie manuelle à usage unique, admin seulement.
* Le backend n'expose pas (encore) de distribution par session : on passe
* par un consommateur temporaire créé, utilisé une fois, puis révoqué
* immédiatement. La valeur est copiée au presse-papiers sans jamais être
* affichée, puis la variable est effacée, même en cas d'échec. */
async function copieManuelle(s) {
if (!isAdmin()) return;
const ok = await showConfirm({
title: t('coffres.manual_copy'),
message: t('coffres.manual_copy_confirm'),
danger: true,
});
if (!ok) return;
let consommateurId = null;
let valeur = null;
try {
const created = await api('/api/coffres/consommateurs', {
method: 'POST',
body: JSON.stringify({ nom: `copie-manuelle-${me()}-${Date.now()}` }),
});
consommateurId = created.id;
const jeton = created.jeton;
const res = await fetch('/api/coffres/distribuer', {
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${jeton}`,
},
body: JSON.stringify({
reference: s.reference,
contexte_type: 'copie_manuelle',
contexte_id: `copie-manuelle-${Date.now()}`,
}),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data.detail || `Erreur ${res.status}`);
valeur = data.valeur;
await copyText(valeur);
} catch (e) {
showToast(e.message || t('common.error'), 'error');
} finally {
valeur = null;
if (consommateurId) {
try {
await api(`/api/coffres/consommateurs/${encodeURIComponent(consommateurId)}`, { method: 'DELETE' });
} catch { /* révocation best-effort, l'octroi reste révocable depuis Accès */ }
}
}
/** Révélation manuelle (gestionnaire du coffre seulement, auditée
* côté backend). Ne conserve rien : l'appelant affiche ou copie
* puis efface. Un lecteur par octroi utilisateur reçoit 403. */
async function revelerValeur(s) {
const ref = (s.reference || '').replace(/^coffre:/, '');
const [slug, nom] = ref.split('/');
const res = await fetch(
`/api/coffres/${encodeURIComponent(slug)}/secrets/${encodeURIComponent(nom)}/reveler`,
{ method: 'POST', credentials: 'include' },
);
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data.detail || `Erreur ${res.status}`);
return data.valeur;
}
/** Phase 2 — « Importer l'existant » (admin seulement, deux temps).
+39 -9
View File
@@ -2,11 +2,13 @@
* chantier 2+3 : onglets, « Tout voir », focus).
*
* Vanilla, sans dépendance. Un seul fichier vendangé tel quel par chaque
* hôte (ObsiGate, Flowdeck, TikTak, Kanbante) : la configuration vient
* des attributs data-* de #recherche, rendus côté serveur par l'hôte.
* hôte (ObsiGate, Flowdeck, TikTak, Kanbante, ShaarIt) : la configuration
* vient des attributs data-* de #recherche, rendus côté serveur par
* l'hôte. Source canonique : dracodev-commons, ui/recherche/
* (contrat : contrats/recherche-federee.md).
*
* Présentation en onglets : Tout, ObsiGate, Flowdeck, TikTak, Kanbante,
* Console Spark. « Tout » = les 4 sections groupées, chaque liste
* ShaarIt, Console Spark. « Tout » = les 5 sections groupées, chaque liste
* tronquée à PER_APP_LIMIT ; un onglet application = tous les résultats
* de cette application (aucune troncature côté client — la requête
* ObsiGate part avec limit=FULL_LIMIT, les autres API renvoient leur
@@ -46,6 +48,11 @@
* -> { groups: [{ full_name, rows: [{ issue, column }] }] }
* 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).
* - ShaarIt : GET {base}/api/search?q=
* -> { links: [{ id, title, url, description, tags, shorturl,
* private }] }
* Lien profond (contrat liens-profonds) : {base}shaare/<shorturl>.
* Recherche native adressable : {base}?q=<q> (page d'accueil).
* - Console Spark : GET {base-console}/api/v1/recherche?q=&limite=
* (agent-commun, authentifié par le cookie SSO draco_session) ->
* { q, domain, limite_par_type, resultats[], par_type,
@@ -87,8 +94,9 @@
* Authentification cross-origin : les appels sœurs partent avec
* credentials:"include". Le navigateur envoie alors les cookies de
* chaque origine, dont le cookie SSO partagé draco_session
* (Domain=.dracodev.net), accepté par les 4 API de recherche
* (ObsiGate et TikTak nativement, Flowdeck et Kanbante via #68)
* (Domain=.dracodev.net), accepté par les 5 API de recherche
* (ObsiGate et TikTak nativement, Flowdeck et Kanbante via #68,
* ShaarIt via la fédération v0.2.1 de commons)
* et par GET /api/v1/recherche de la console (identity() accepte
* draco_session). Chaque sœur n'autorise en CORS que les origines
* sœurs (GET sur l'endpoint de recherche uniquement, pas de "*") ;
@@ -109,12 +117,14 @@
flowdeck: "https://flowdeck.dracodev.net/",
tiktak: "https://tiktak.dracodev.net/",
kanbante: "https://kanbante.dracodev.net/",
shaarit: "https://shaarit.dracodev.net/",
};
var DEV = {
obsigate: "https://og-dev.dracodev.net/",
flowdeck: "https://flowdeck-dev.dracodev.net/",
tiktak: "https://tiktak-dev.dracodev.net/",
kanbante: "https://kanbante-dev.dracodev.net/",
shaarit: "https://shaarit-dev.dracodev.net/",
};
var PROD_AGENT = "https://agent.dracodev.net/";
var DEV_AGENT = "https://agent-dev.dracodev.net/";
@@ -125,6 +135,7 @@
{ key: "flowdeck", label: "Flowdeck" },
{ key: "tiktak", label: "TikTak" },
{ key: "kanbante", label: "Kanbante" },
{ key: "shaarit", label: "ShaarIt" },
];
var CONSOLE_APP = { key: "console", label: "Console Spark" };
var CONSOLE_TYPE_LABELS = {
@@ -145,7 +156,7 @@
planification: "/?section=planifie",
identite: "/?section=identites",
};
var TABS = ["all", "obsigate", "flowdeck", "tiktak", "kanbante", "console"];
var TABS = ["all", "obsigate", "flowdeck", "tiktak", "kanbante", "shaarit", "console"];
var COLUMN_LABELS = {
backlog: "Backlog",
in_progress: "En cours",
@@ -227,7 +238,7 @@
return TABS.indexOf(v) !== -1 ? v : "all";
}
/* Sections visibles pour un onglet. « Tout » = les 4 sections
/* Sections visibles pour un onglet. « Tout » = les 5 sections
* groupées (vue #68) ; la console n'y figure pas (voir en-tête). */
function visibleKeys(tab) {
tab = parseTab(tab);
@@ -256,7 +267,7 @@
* Chargement -> "…", ok/vide -> total ("0" honnête),
* erreur/indisponible/inactif -> "" (pas de faux 0).
* Tout : "…" tant qu'une section charge, sinon somme des totaux
* connus, "" si les 4 sections sont en erreur/inactives. */
* connus, "" si les 5 sections sont en erreur/inactives. */
function tabCounts(states) {
states = states || {};
var out = {};
@@ -367,6 +378,22 @@
};
});
}
if (app === "shaarit") {
return cut(data.links || []).map(function (l) {
var tags = (l.tags || []).filter(function (t) { return !!t; });
var ctx = "Bookmark";
if (l.private) ctx += " · privé";
if (tags.length) ctx += " · " + tags.join(", ");
return {
title: l.title || l.url || "Sans titre",
snippet: snippetOf(l.description || ""),
context: ctx,
/* Lien profond canonique (contrats/liens-profonds.md) ;
* sans shorturl, repli sur l'URL du bookmark lui-même. */
url: l.shorturl ? base + "shaare/" + l.shorturl : l.url || null,
};
});
}
return [];
}
@@ -455,13 +482,16 @@
if (app === "tiktak" || app === "kanbante") {
return base + "search?q=" + encodeURIComponent(q || "");
}
if (app === "shaarit") {
return base + "?q=" + encodeURIComponent(q || "");
}
/* ObsiGate et Flowdeck n'ont pas de page de recherche adressable :
* leur recherche native vit dans la palette (Ctrl+K) de l'app. */
return base;
}
function nativeSearchLabel(app) {
if (app === "tiktak" || app === "kanbante") {
if (app === "tiktak" || app === "kanbante" || app === "shaarit") {
return "Ouvrir la recherche dans " + appLabel(app);
}
return "Ouvrir " + appLabel(app) + " (recherche via la palette Ctrl+K)";
+29 -18
View File
@@ -209,6 +209,17 @@ function _openSecretsMenu(x, y) {
}, 0);
}
/**
* Vault déclaré en lecture seule (DIR_N_READONLY, champ « readonly » de
* /api/vaults) : ses menus et boutons d'action ne doivent proposer aucune
* mutation (renommer, déplacer, supprimer…) — le backend les refuse en
* 403 de toute façon, l'UI le dit avant le clic.
*/
function vaultIsReadonly(vaultName) {
const v = (state.allVaults || []).find((x) => x.name === vaultName);
return !!(v && v.readonly);
}
/** Fichier partagé (créé ou reçu) → icône share-2 bleue, sinon icône usuelle. */
function sharedFileIconSync(sharedPaths, itemName, vaultName, itemPath) {
if (sharedPaths && sharedPaths.has(`${vaultName}::${itemPath}`)) return _sharedIcon();
@@ -356,11 +367,11 @@ async function refreshSidebarForContext() {
vaultItem.addEventListener("contextmenu", (e) => {
e.preventDefault();
const isReadonly = false;
const isReadonly = vaultIsReadonly(v.name);
ContextMenuManager.show(e.clientX, e.clientY, v.name, '', 'vault', isReadonly);
});
attachTreeItemActionButton(vaultItem, v.name, "", "vault", false);
attachTreeItemLongPress(vaultItem, () => ({ vault: v.name, path: "", type: "vault", isReadonly: false }));
attachTreeItemActionButton(vaultItem, v.name, "", "vault", vaultIsReadonly(v.name));
attachTreeItemLongPress(vaultItem, () => ({ vault: v.name, path: "", type: "vault", isReadonly: vaultIsReadonly(v.name) }));
container.appendChild(vaultItem);
@@ -460,11 +471,11 @@ async function loadVaults() {
vaultItem.addEventListener("contextmenu", (e) => {
e.preventDefault();
const isReadonly = false;
const isReadonly = vaultIsReadonly(v.name);
ContextMenuManager.show(e.clientX, e.clientY, v.name, '', 'vault', isReadonly);
});
attachTreeItemActionButton(vaultItem, v.name, "", "vault", false);
attachTreeItemLongPress(vaultItem, () => ({ vault: v.name, path: "", type: "vault", isReadonly: false }));
attachTreeItemActionButton(vaultItem, v.name, "", "vault", vaultIsReadonly(v.name));
attachTreeItemLongPress(vaultItem, () => ({ vault: v.name, path: "", type: "vault", isReadonly: vaultIsReadonly(v.name) }));
container.appendChild(vaultItem);
@@ -596,8 +607,8 @@ async function incrementalLoadDirectory(vaultName, dirPath, container) {
// New item — create it
if (item.type === "directory") {
const dirItem = el("div", { class: "tree-item", "data-vault": vaultName, "data-path": item.path, "data-type": "directory" }, [icon("chevron-right", 14), icon("folder", 16), el("span", { class: "tree-item-text" }, [document.createTextNode(item.name)]), smallBadge(item.children_count)]);
attachTreeItemActionButton(dirItem, vaultName, item.path, "directory", false);
attachTreeItemLongPress(dirItem, () => ({ vault: vaultName, path: item.path, type: "directory", isReadonly: false }));
attachTreeItemActionButton(dirItem, vaultName, item.path, "directory", vaultIsReadonly(vaultName));
attachTreeItemLongPress(dirItem, () => ({ vault: vaultName, path: item.path, type: "directory", isReadonly: vaultIsReadonly(vaultName) }));
fragment.appendChild(dirItem);
const subContainer = el("div", { class: "tree-children collapsed", id: `dir-${vaultName}-${item.path}` });
@@ -624,14 +635,14 @@ async function incrementalLoadDirectory(vaultName, dirPath, container) {
dirItem.addEventListener("contextmenu", (e) => {
e.preventDefault();
ContextMenuManager.show(e.clientX, e.clientY, vaultName, item.path, "directory", false);
ContextMenuManager.show(e.clientX, e.clientY, vaultName, item.path, "directory", vaultIsReadonly(vaultName));
});
} else {
const fileIconName = getFileIcon(item.name);
const displayName = item.name.match(/\.md$/i) ? item.name.replace(/\.md$/i, "") : item.name;
const fileItem = el("div", { class: "tree-item", "data-vault": vaultName, "data-path": item.path, "data-type": "file" }, [sharedFileIconSync(sharedPaths, item.name, vaultName, item.path), el("span", { class: "tree-item-text" }, [document.createTextNode(displayName)])]);
attachTreeItemActionButton(fileItem, vaultName, item.path, "file", false);
attachTreeItemLongPress(fileItem, () => ({ vault: vaultName, path: item.path, type: "file", isReadonly: false }));
attachTreeItemActionButton(fileItem, vaultName, item.path, "file", vaultIsReadonly(vaultName));
attachTreeItemLongPress(fileItem, () => ({ vault: vaultName, path: item.path, type: "file", isReadonly: vaultIsReadonly(vaultName) }));
fileItem.addEventListener("click", () => {
scrollTreeItemIntoView(fileItem, false);
if (window.innerWidth <= 768) {
@@ -649,7 +660,7 @@ async function incrementalLoadDirectory(vaultName, dirPath, container) {
fileItem.addEventListener("contextmenu", (e) => {
e.preventDefault();
ContextMenuManager.show(e.clientX, e.clientY, vaultName, item.path, "file", false);
ContextMenuManager.show(e.clientX, e.clientY, vaultName, item.path, "file", vaultIsReadonly(vaultName));
});
fragment.appendChild(fileItem);
@@ -944,8 +955,8 @@ async function loadDirectory(vaultName, dirPath, container) {
if (item.type === "directory") {
const dirItem = el("div", { class: "tree-item", "data-vault": vaultName, "data-path": item.path, "data-type": "directory" }, [icon("chevron-right", 14), icon("folder", 16), el("span", { class: "tree-item-text" }, [document.createTextNode(item.name)]), smallBadge(item.children_count)]);
attachTreeItemActionButton(dirItem, vaultName, item.path, "directory", false);
attachTreeItemLongPress(dirItem, () => ({ vault: vaultName, path: item.path, type: "directory", isReadonly: false }));
attachTreeItemActionButton(dirItem, vaultName, item.path, "directory", vaultIsReadonly(vaultName));
attachTreeItemLongPress(dirItem, () => ({ vault: vaultName, path: item.path, type: "directory", isReadonly: vaultIsReadonly(vaultName) }));
fragment.appendChild(dirItem);
const subContainer = el("div", { class: "tree-children collapsed", id: `dir-${vaultName}-${item.path}` });
@@ -972,7 +983,7 @@ async function loadDirectory(vaultName, dirPath, container) {
dirItem.addEventListener("contextmenu", (e) => {
e.preventDefault();
const isReadonly = false;
const isReadonly = vaultIsReadonly(vaultName);
ContextMenuManager.show(e.clientX, e.clientY, vaultName, item.path, 'directory', isReadonly);
});
} else {
@@ -980,8 +991,8 @@ async function loadDirectory(vaultName, dirPath, container) {
const displayName = item.name.match(/\.md$/i) ? item.name.replace(/\.md$/i, "") : item.name;
const isShared = sharedPaths.has(`${vaultName}::${item.path}`);
const fileItem = el("div", { class: "tree-item", "data-vault": vaultName, "data-path": item.path, "data-type": "file" }, [isShared ? _sharedIcon() : icon(fileIconName, 16), el("span", { class: "tree-item-text" }, [document.createTextNode(displayName)])]);
attachTreeItemActionButton(fileItem, vaultName, item.path, "file", false);
attachTreeItemLongPress(fileItem, () => ({ vault: vaultName, path: item.path, type: "file", isReadonly: false }));
attachTreeItemActionButton(fileItem, vaultName, item.path, "file", vaultIsReadonly(vaultName));
attachTreeItemLongPress(fileItem, () => ({ vault: vaultName, path: item.path, type: "file", isReadonly: vaultIsReadonly(vaultName) }));
fileItem.addEventListener("click", () => {
scrollTreeItemIntoView(fileItem, false);
if (window.innerWidth <= 768) {
@@ -999,7 +1010,7 @@ async function loadDirectory(vaultName, dirPath, container) {
fileItem.addEventListener("contextmenu", (e) => {
e.preventDefault();
const isReadonly = false;
const isReadonly = vaultIsReadonly(vaultName);
ContextMenuManager.show(e.clientX, e.clientY, vaultName, item.path, 'file', isReadonly);
});
+3
View File
@@ -2680,6 +2680,9 @@
"coffres.copy_failed": "Copy failed.",
"coffres.copy_generated": "Copy the generated value",
"coffres.copy_reference": "Copy",
"coffres.show_value": "Show",
"coffres.hide_value": "Hide",
"coffres.copy_value": "Copy value",
"coffres.create_coffre_title": "New vault",
"coffres.create_octroi": "Create a grant",
"coffres.create_secret_title": "New secret",
+3
View File
@@ -2680,6 +2680,9 @@
"coffres.copy_failed": "Copie impossible.",
"coffres.copy_generated": "Copier la valeur générée",
"coffres.copy_reference": "Copier",
"coffres.show_value": "Afficher",
"coffres.hide_value": "Masquer",
"coffres.copy_value": "Copier la valeur",
"coffres.create_coffre_title": "Nouveau coffre",
"coffres.create_octroi": "Créer un octroi",
"coffres.create_secret_title": "Nouveau secret",
+20 -14
View File
@@ -14,7 +14,7 @@
retomberait sur les défauts prod (les jetons sont ignorés par
le JS) — jamais d'URL inventée.
La barre ci-dessous est une copie statique du snippet figé de
index.html (commons v0.1.6 + chantier 5 fusion #69/#71, sans
index.html (commons v0.2.0 + chantier 5 fusion #69/#71, sans
bump) : les onglets sont des liens réels, le bouton central
replace le curseur dans la boîte (recherche.js,
clic data-act="palette"). Re-vendoring alignement recherche :
@@ -58,29 +58,33 @@
<div class="draco-bar-zone draco-bar-gauche">
<button type="button" class="draco-icon-btn" data-act="rail" aria-label="Barre latérale">☰</button>
<a class="draco-brand" href="/" data-act="apps" aria-haspopup="true" aria-expanded="false" aria-label="dracodev — changer d'application" title="Changer d'application">
<span class="draco-brand-mark">✦</span><span class="draco-brand-name">dracodev</span>
<span class="draco-brand-mark"><img src="/static/img/brand-spark.png" alt="" width="26" height="26"></span><span class="draco-brand-name">dracodev</span>
</a>
<nav class="draco-apptabs" aria-label="Applications">
<a class="draco-apptab" data-app="obsigate" href="{{URL_OBSIGATE}}">
<span class="draco-apptab-ico" style="--app:#7a5af5">▤</span>
<span class="draco-apptab-ico" style="--app:#7a5af5" data-icon="obsigate"><img src="/static/img/logos/obsigate.png" alt="ObsiGate" width="30" height="30"></span>
<span class="draco-apptab-txt"><strong>ObsiGate</strong><small>Notes</small></span>
</a>
<a class="draco-apptab" data-app="flowdeck" href="{{URL_FLOWDECK}}">
<span class="draco-apptab-ico" style="--app:#2f6fed">▦</span>
<span class="draco-apptab-ico" style="--app:#2f6fed" data-icon="flowdeck"><img src="/static/img/logos/flowdeck.png" alt="Flowdeck" width="30" height="30"></span>
<span class="draco-apptab-txt"><strong>Flowdeck</strong><small>Projets</small></span>
</a>
<a class="draco-apptab" data-app="tiktak" href="{{URL_TIKTAK}}">
<span class="draco-apptab-ico" style="--app:#e8762c">✓</span>
<span class="draco-apptab-ico" style="--app:#e8762c" data-icon="tiktak"><img src="/static/img/logos/tiktak.svg" alt="TikTak" width="30" height="30"></span>
<span class="draco-apptab-txt"><strong>TikTak</strong><small>Tâches</small></span>
<span class="draco-badge" data-badge="tiktak" hidden>0</span>
</a>
<a class="draco-apptab" data-app="kanbante" href="{{URL_KANBANTE}}">
<span class="draco-apptab-ico" style="--app:#0d9488">▥</span>
<span class="draco-apptab-ico" style="--app:#0d9488" data-icon="kanbante"><img src="/static/img/logos/kanbante.png" alt="Kanbante" width="30" height="30"></span>
<span class="draco-apptab-txt"><strong>Kanbante</strong><small>Kanban</small></span>
<span class="draco-badge" data-badge="kanbante" hidden>0</span>
</a>
<a class="draco-apptab" data-app="shaarit" href="{{URL_SHAARIT}}">
<span class="draco-apptab-ico" style="--app:#059669" data-icon="shaarit"><img src="/static/img/logos/shaarit.svg" alt="ShaarIt" width="30" height="30"></span>
<span class="draco-apptab-txt"><strong>ShaarIt</strong><small>Bookmarks</small></span>
</a>
<a class="draco-apptab" data-app="agent" href="{{URL_AGENT}}">
<span class="draco-apptab-ico" style="--app:#c2437e">✦</span>
<span class="draco-apptab-ico" style="--app:#c00000" data-icon="agent"><img src="/static/img/logos/spark.png" alt="Console Spark" width="30" height="30"></span>
<span class="draco-apptab-txt"><strong>Console Spark</strong><small>Agent commun</small></span>
</a>
</nav>
@@ -101,16 +105,12 @@
</div>
<button type="button" class="draco-alertes" data-act="alertes" aria-label="Alertes unifiées">
<svg viewBox="0 0 24 24" width="19" height="19" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/><path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/></svg>
<span class="draco-alertes-txt">Alertes</span>
<span class="draco-badge" data-badge="alertes" hidden>0</span>
</button>
<button type="button" class="draco-spark" data-act="spark" aria-label="Ouvrir Spark, l'agent commun">
<span class="draco-spark-ico">✦</span><span class="draco-spark-txt">Spark</span>
<span class="draco-badge" data-badge="spark" hidden>0</span>
</button>
<button type="button" class="draco-avatar" id="draco-avatar" data-act="compte" aria-label="Compte" aria-haspopup="menu" aria-expanded="false" title="">✦</button>
</div>
</header>
<button type="button" class="draco-spark-flottant" data-act="spark" aria-label="Ouvrir Spark, l'agent commun"><img src="/static/img/avatar-spark.png" alt="" width="64" height="64"></button>
<div id="app" class="app-container">
<header class="header">
<div class="header-left">
@@ -132,9 +132,10 @@
data-base-obsigate="{{URL_OBSIGATE}}"
data-base-flowdeck="{{URL_FLOWDECK}}"
data-base-tiktak="{{URL_TIKTAK}}"
data-base-kanbante="{{URL_KANBANTE}}">
data-base-kanbante="{{URL_KANBANTE}}"
data-base-shaarit="{{URL_SHAARIT}}">
<h1>Rechercher partout</h1>
<p class="rp-hint">Notes ObsiGate, pages Flowdeck, tâches TikTak, cartes Kanbante — dans l'environnement courant (dev reste dev). « / » ou Ctrl+K pour rechercher, ↑/↓ puis Entrée pour ouvrir un résultat. « Tout voir » bascule vers l'onglet de l'application, sans quitter la page.</p>
<p class="rp-hint">Notes ObsiGate, pages Flowdeck, tâches TikTak, cartes Kanbante, bookmarks ShaarIt — dans l'environnement courant (dev reste dev). « / » ou Ctrl+K pour rechercher, ↑/↓ puis Entrée pour ouvrir un résultat. « Tout voir » bascule vers l'onglet de l'application, sans quitter la page.</p>
<div class="rp-form">
<input id="rp-q" class="rp-input" type="search" name="q" value=""
placeholder="Rechercher partout…" autocomplete="off" autofocus>
@@ -145,6 +146,7 @@
<button type="button" class="rp-tab" role="tab" aria-selected="false" tabindex="-1" data-rp-tab="flowdeck">Flowdeck <span class="rp-tab-count"></span></button>
<button type="button" class="rp-tab" role="tab" aria-selected="false" tabindex="-1" data-rp-tab="tiktak">TikTak <span class="rp-tab-count"></span></button>
<button type="button" class="rp-tab" role="tab" aria-selected="false" tabindex="-1" data-rp-tab="kanbante">Kanbante <span class="rp-tab-count"></span></button>
<button type="button" class="rp-tab" role="tab" aria-selected="false" tabindex="-1" data-rp-tab="shaarit">ShaarIt <span class="rp-tab-count"></span></button>
<button type="button" class="rp-tab" role="tab" aria-selected="false" tabindex="-1" data-rp-tab="console">Console Spark <span class="rp-tab-count"></span></button>
</nav>
<section class="rp-section" data-rp-section="obsigate">
@@ -163,6 +165,10 @@
<div class="rp-head"><h2>Kanbante — cartes</h2><span class="rp-count"></span><a class="rp-all" href="#">Tout voir</a></div>
<div class="rp-body"><p class="rp-state">Tapez une recherche ci-dessus.</p></div>
</section>
<section class="rp-section" data-rp-section="shaarit">
<div class="rp-head"><h2>ShaarIt — bookmarks</h2><span class="rp-count"></span><a class="rp-all" href="#">Tout voir</a></div>
<div class="rp-body"><p class="rp-state">Tapez une recherche ci-dessus.</p></div>
</section>
<section class="rp-section" data-rp-section="console">
<div class="rp-head"><h2>Console Spark — discussions</h2><span class="rp-count"></span><a class="rp-all" href="#">Tout voir</a></div>
<div class="rp-body"><p class="rp-state">Tapez une recherche ci-dessus.</p></div>
+1 -1
View File
@@ -8493,7 +8493,7 @@ body.popup-mode .content-area {
box-shadow: 0 2px 16px rgba(0,0,0,0.35);
width: fit-content;
max-width: 95%;
z-index: 50;
z-index: 66; /* au-dessus de .draco-spark-flottant (65) sinon la barre flottante DRACO intercepte les clics des flèches de recherche */
backdrop-filter: blur(8px);
}
.results-bar-count {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "obsigate",
"version": "2.82.3",
"version": "2.83.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": {
+1 -1
View File
@@ -25,7 +25,7 @@ async function login(page) {
await page.click('#login-btn');
}
// Wait until app init() resolved (all handlers bound).
await page.waitForFunction(() => window.__OBSIGATE_BOOTED === true, { timeout: 20000 });
await page.waitForFunction(() => document.readyState === 'complete' && !!document.querySelector('.tree-item'), { timeout: 20000 });
}
async function openFile(page, vault, filePath) {
+1 -1
View File
@@ -28,7 +28,7 @@ async function login(page) {
await page.fill('#login-password', CREDS.password);
await page.click('#login-btn');
}
await page.waitForFunction(() => window.__OBSIGATE_BOOTED === true, { timeout: 20000 });
await page.waitForFunction(() => document.readyState === 'complete' && !!document.querySelector('.tree-item'), { timeout: 20000 });
}
async function authEnabled(page) {
+1 -1
View File
@@ -26,7 +26,7 @@ async function login(page) {
await page.fill('#login-password', CREDS.password);
await page.click('#login-btn');
}
await page.waitForFunction(() => window.__OBSIGATE_BOOTED === true, { timeout: 20000 });
await page.waitForFunction(() => document.readyState === 'complete' && !!document.querySelector('.tree-item'), { timeout: 20000 });
}
async function openFile(page, vault, filePath) {
+1 -1
View File
@@ -25,7 +25,7 @@ async function login(page) {
await page.fill('#login-password', CREDS.password);
await page.click('#login-btn');
}
await page.waitForFunction(() => window.__OBSIGATE_BOOTED === true, { timeout: 20000 });
await page.waitForFunction(() => document.readyState === 'complete' && !!document.querySelector('.tree-item'), { timeout: 20000 });
}
async function openFile(page, vault, filePath) {
+1 -1
View File
@@ -12,7 +12,7 @@ import { test, expect } from '@playwright/test';
async function boot(page) {
await page.goto('/');
await page.waitForFunction(() => window.__OBSIGATE_BOOTED === true, { timeout: 20000 });
await page.waitForFunction(() => document.readyState === 'complete' && !!document.querySelector('.tree-item'), { timeout: 20000 });
await page.waitForSelector('#dashboard-home, #search-input', { timeout: 15000 });
}
+1 -1
View File
@@ -9,7 +9,7 @@ const BASE = process.env.BASE_URL || 'http://localhost:2029';
async function goHome(page) {
await page.goto(BASE);
// Wait until app init() resolved (all handlers bound, incl. search).
await page.waitForFunction(() => (window as any).__OBSIGATE_BOOTED === true, { timeout: 20000 });
await page.waitForFunction(() => document.readyState === 'complete' && !!document.querySelector('.tree-item'), { timeout: 20000 });
await page.waitForSelector('#dashboard-home, #search-input', { timeout: 15000 });
}
+1 -1
View File
@@ -30,7 +30,7 @@ async function login(page) {
await page.fill('#login-password', CREDS.password);
await page.click('#login-btn');
}
await page.waitForFunction(() => window.__OBSIGATE_BOOTED === true, { timeout: 20000 });
await page.waitForFunction(() => document.readyState === 'complete' && !!document.querySelector('.tree-item'), { timeout: 20000 });
}
async function openFile(page, vault, filePath) {
+1 -1
View File
@@ -57,7 +57,7 @@ async function login(page) {
await page.fill('#login-password', process.env.OBSIGATE_PASS || 'test123');
await page.click('#login-btn');
}
await page.waitForFunction(() => window.__OBSIGATE_BOOTED === true, { timeout: 20000 });
await page.waitForFunction(() => document.readyState === 'complete' && !!document.querySelector('.tree-item'), { timeout: 20000 });
}
async function openFixture(page) {
+1 -1
View File
@@ -117,7 +117,7 @@ test.describe('XSS — lecteur markdown (BUG-021)', () => {
await api(request, 'POST', `/api/file/${VAULT}`, { path: XSS_FILE, content: `${XSS_BODY}\n` });
await page.goto(BASE);
await page.waitForFunction(() => window.__OBSIGATE_BOOTED === true, { timeout: 20000 });
await page.waitForFunction(() => document.readyState === 'complete' && !!document.querySelector('.tree-item'), { timeout: 20000 });
await openFile(page, VAULT, XSS_FILE);
const content = page.locator('#content-area');
+36 -5
View File
@@ -38,6 +38,7 @@ const b = rp.resolveBases({
ok(b.tiktak === "https://tiktak-perso.example/", "explicite prioritaire");
ok(b.obsigate === "https://og-dev.dracodev.net/", "dev pour l'hôte courant");
ok(b.kanbante === "https://kanbante-dev.dracodev.net/", "dev avant href");
ok(b.shaarit === "https://shaarit-dev.dracodev.net/", "base shaarit dev");
ok(
rp.searchPath("obsigate", "café") === "api/search?q=caf%C3%A9&vault=all&limit=8",
"path ObsiGate",
@@ -58,21 +59,47 @@ ok(
"https://tiktak-dev.dracodev.net/search?q=q",
"repli TikTak",
);
ok(
rp.nativeSearchUrl("shaarit", "https://shaarit-dev.dracodev.net/", "q") ===
"https://shaarit-dev.dracodev.net/?q=q",
"repli ShaarIt = accueil ?q=",
);
/* normalize ShaarIt (commons v0.2.1) : lien profond shaare/<shorturl>,
* contexte privé + tags, repli sur l'URL du bookmark sans shorturl */
const signets = rp.normalize(
"shaarit",
{ links: [{ title: "Yandex", url: "https://yandex.com", description: "moteur", tags: ["recherche", "web"], shorturl: "xYz9", private: true }] },
"https://shaarit-dev.dracodev.net/",
"recherche",
);
ok(signets[0].url === "https://shaarit-dev.dracodev.net/shaare/xYz9", "lien profond ShaarIt (shaare/<shorturl>)");
ok(signets[0].context === "Bookmark · privé · recherche, web", "contexte ShaarIt (privé + tags)");
ok(signets[0].snippet === "moteur", "extrait ShaarIt = description");
const sansShort = rp.normalize(
"shaarit",
{ links: [{ title: "", url: "https://exemple.fr", tags: [], shorturl: "" }] },
"https://shaarit.dracodev.net/",
"x",
);
ok(sansShort[0].url === "https://exemple.fr" && sansShort[0].title === "https://exemple.fr", "ShaarIt sans shorturl : repli URL + titre");
/* ---- Chantier 2+3 : onglets, compteurs, URL, console (logique pure) ---- */
/* parseTab */
ok(rp.parseTab("obsigate") === "obsigate", "onglet valide conservé");
ok(rp.parseTab("shaarit") === "shaarit", "onglet shaarit valide");
ok(rp.parseTab("console") === "console", "onglet console valide");
ok(rp.parseTab("bidon") === "all", "onglet inconnu -> Tout");
ok(rp.parseTab(undefined) === "all", "onglet absent -> Tout");
/* visibleKeys : Tout = les 4 sections groupées, sans la console */
ok(JSON.stringify(rp.visibleKeys("all")) === JSON.stringify(["obsigate", "flowdeck", "tiktak", "kanbante"]), "Tout = 4 sections, pas la console");
/* visibleKeys : Tout = les 5 sections groupées, sans la console */
ok(JSON.stringify(rp.visibleKeys("all")) === JSON.stringify(["obsigate", "flowdeck", "tiktak", "kanbante", "shaarit"]), "Tout = 5 sections, pas la console");
ok(JSON.stringify(rp.visibleKeys("tiktak")) === JSON.stringify(["tiktak"]), "onglet app = sa seule section");
ok(JSON.stringify(rp.visibleKeys("shaarit")) === JSON.stringify(["shaarit"]), "onglet shaarit = sa seule section");
ok(JSON.stringify(rp.visibleKeys("console")) === JSON.stringify(["console"]), "onglet console = sa seule section");
ok(JSON.stringify(rp.visibleKeys("bidon")) === JSON.stringify(["obsigate", "flowdeck", "tiktak", "kanbante"]), "onglet inconnu -> Tout");
ok(JSON.stringify(rp.visibleKeys("bidon")) === JSON.stringify(["obsigate", "flowdeck", "tiktak", "kanbante", "shaarit"]), "onglet inconnu -> Tout");
/* pageItems : troncature Tout (8), complet dans l'onglet de l'app */
let douze = Array.from({ length: 12 }, (_, i) => ({ title: "T" + i }));
@@ -85,9 +112,11 @@ let c = rp.tabCounts({
flowdeck: { status: "ok", total: 3 },
tiktak: { status: "empty", total: 0 },
kanbante: { status: "error", total: 0 },
shaarit: { status: "ok", total: 2 },
console: { status: "idle", total: 0 },
});
ok(c.obsigate === "…", "compteur … pendant le chargement");
ok(c.shaarit === "2", "compteur shaarit");
ok(c.flowdeck === "3" && c.tiktak === "0", "compteur total, vide honnête à 0");
ok(c.kanbante === "" && c.console === "", "erreur/inactif : pas de faux 0");
ok(c.all === "…", "Tout = … tant qu'une section charge");
@@ -96,18 +125,20 @@ c = rp.tabCounts({
flowdeck: { status: "ok", total: 2 },
tiktak: { status: "empty", total: 0 },
kanbante: { status: "error", total: 0 },
shaarit: { status: "ok", total: 1 },
console: { status: "ok", total: 4 },
});
ok(c.all === "7", "Tout = somme des totaux connus");
ok(c.all === "8", "Tout = somme des totaux connus (5 sections)");
ok(c.console === "4", "compteur console");
c = rp.tabCounts({
obsigate: { status: "error", total: 0 },
flowdeck: { status: "error", total: 0 },
tiktak: { status: "error", total: 0 },
kanbante: { status: "error", total: 0 },
shaarit: { status: "error", total: 0 },
console: { status: "idle", total: 0 },
});
ok(c.all === "", "Tout vide si les 4 sections sont en erreur");
ok(c.all === "", "Tout vide si les 5 sections sont en erreur");
/* buildSearchUrl : état onglet partageable */
ok(rp.buildSearchUrl("café", "tiktak") === "?q=caf%C3%A9&tab=tiktak", "URL q+onglet");
+3 -1
View File
@@ -112,8 +112,10 @@ function testModulesHaveImports() {
{ file: 'navfacets.js', reason: 'pure helpers — no deps, imported by vaulthome.js' },
{ file: 'sidebar_raw.js', reason: 'legacy file — excluded' },
{ file: 'desktop.js', reason: 'standalone — reads window.__TAURI__ global, no imports needed' },
{ file: 'recherche.js', reason: 'standalone — vanilla sans dépendance, vendangé tel quel par chaque hôte (#68)', noExport: true },
];
const expectNoImport = new Set(expected.map(e => e.file));
const expectNoExport = new Set(expected.filter(e => e.noExport).map(e => e.file));
for (const file of files) {
const content = readFileSync(join(JS_DIR, file), 'utf-8');
@@ -123,7 +125,7 @@ function testModulesHaveImports() {
if (!expectNoImport.has(file)) {
assert.ok(hasImport, `${file} should have at least one import`);
}
if (file !== 'state.js' && file !== 'app.js') {
if (file !== 'state.js' && file !== 'app.js' && !expectNoExport.has(file)) {
assert.ok(hasExport, `${file} should have at least one export`);
}
}
+53
View File
@@ -778,3 +778,56 @@ class TestDistribuables:
# Flag désactivé : 503 comme les autres routes.
monkeypatch.setenv("OBSIGATE_SECRETS_ENABLED", "0")
assert self._lister(client, jeton).status_code == 503
# ---------------------------------------------------------------------------
# Révélation manuelle (panneau détail : Afficher / Copier la valeur)
# ---------------------------------------------------------------------------
class TestRevelation:
def test_proprietaire_revele_et_audit_sans_valeur(self, client, env_coffre):
_creer_coffre_secret(client)
resp = client.post("/api/coffres/personnel/secrets/gitea-token/reveler")
assert resp.status_code == 200, resp.text
corps = resp.json()
assert corps["valeur"] == VALEUR_TEST
assert corps["reference"] == "coffre:personnel/gitea-token"
assert corps["version"] == 1
audit = (env_coffre / "audit.log").read_text(encoding="utf-8")
assert "revelation_manuelle" in audit
assert VALEUR_TEST not in audit
def test_lecteur_octroi_utilisateur_ne_revele_pas(self, client):
_creer_coffre_secret(client)
resp = client.post("/api/coffres/personnel/octrois", json={
"cible_type": "utilisateur", "cible_id": "alice",
"secret_nom": "gitea-token"})
assert resp.status_code == 200, resp.text
_connecter(client, "alice", role="user")
# Voit les métadonnées…
assert client.get("/api/coffres/personnel/secrets").status_code == 200
# …mais ne révèle jamais la valeur.
resp = client.post("/api/coffres/personnel/secrets/gitea-token/reveler")
assert resp.status_code == 403
def test_secret_inexistant_404(self, client):
_creer_coffre_secret(client)
resp = client.post("/api/coffres/personnel/secrets/absent/reveler")
assert resp.status_code == 404
def test_secret_expire_410(self, client):
_creer_coffre_secret(client)
hier = (datetime.now(timezone.utc) - timedelta(days=1)).isoformat()
resp = client.post("/api/coffres/personnel/secrets", json={
"nom": "vieux-jeton", "type": "token", "valeur": VALEUR_TEST,
"expiration": hier})
assert resp.status_code == 200, resp.text
resp = client.post("/api/coffres/personnel/secrets/vieux-jeton/reveler")
assert resp.status_code == 410
assert resp.json()["detail"] == "secret_expire"
def test_flag_desactive_503(self, client, monkeypatch):
_creer_coffre_secret(client)
monkeypatch.setenv("OBSIGATE_SECRETS_ENABLED", "0")
resp = client.post("/api/coffres/personnel/secrets/gitea-token/reveler")
assert resp.status_code == 503
+85 -5
View File
@@ -34,13 +34,71 @@ def test_palier_mobile_resserre_v012():
def test_menu_des_applications_par_la_marque():
# v0.1.3 : la marque ✦ ouvre le menu des 4 applications
# (changement d'app en mobile), logo à 24 px.
# v0.1.3 : la marque ouvre le menu des applications
# (changement d'app en mobile). v0.1.10 : la marque n'est plus
# le glyphe ✦ à 24 px mais le logo Spark en img 26 px.
# v0.1.11 : la marque n'est plus le logo rouge mais l'étoile
# Spark monochrome (brand-spark.png) ; l'onglet agent, lui,
# garde le logo rouge.
html = _index()
assert 'data-act="apps"' in html
js = (FRONTEND / "draco-barre.js").read_text(encoding="utf-8")
assert "draco-apps-menu" in js
assert "font-size: 24px" in _css()
assert '<span class="draco-brand-mark"><img src="/static/img/brand-spark.png"' in html
assert ".draco-brand-mark img" in _css()
assert "width: 26px" in _css()
assert "filter: brightness(0)" in _css()
assert '[data-theme="dark"] .draco-bar .draco-brand-mark img { filter: none; }' in _css()
assert 'data-icon="agent"><img src="/static/img/logos/spark.png"' in html
assert "border: 3px solid var(--agent)" in _css()
recherche = (FRONTEND / "recherche.html").read_text(encoding="utf-8")
assert '<span class="draco-brand-mark"><img src="/static/img/brand-spark.png"' in recherche
def test_barre_v020_onglet_shaarit_et_garde_data_theme():
# Barre v0.2.0 (dracodev-commons @70c837a) : 6e onglet ShaarIt
# après Kanbante et avant l'onglet agent, logo fourni tel quel,
# et garde data-theme sur les blocs prefers-color-scheme du CSS
# vendangé (html:not([data-theme])) — sans elle, un hôte en
# data-theme="light" sur système sombre héritait du sombre.
css = _css()
assert "html:not([data-theme]) .draco-bar {" in css
assert "html:not([data-theme]) .draco-bar .draco-brand-mark img" in css
assert (FRONTEND / "img" / "logos" / "shaarit.svg").is_file()
for nom, jeton in (("index.html", "https://shaarit.dracodev.net/"),
("recherche.html", "{{URL_SHAARIT}}")):
html = (FRONTEND / nom).read_text(encoding="utf-8")
nav = html.split('<nav class="draco-apptabs"')[1].split("</nav>")[0]
assert f'data-app="shaarit" href="{jeton}"' in nav
assert 'data-icon="shaarit"><img src="/static/img/logos/shaarit.svg"' in nav
assert nav.index('data-app="kanbante"') < nav.index('data-app="shaarit"')
assert nav.index('data-app="shaarit"') < nav.index('data-app="agent"')
js = (FRONTEND / "draco-barre.js").read_text(encoding="utf-8")
assert 'shaarit: "https://shaarit.dracodev.net/"' in js
assert 'shaarit: "https://shaarit-dev.dracodev.net/"' in js
def test_barre_v030_liens_hub_menu_compte():
# Barre v0.3.0 (dracodev-commons, HUB-1 / fusion #115) : clé
# urls.hub (défaut officiel + dérivation hub-dev), liens du menu
# compte (Télécharger, À Propos, Aides, Profil-lien) construits
# par le JS vendangé, et branchement hôte : index.html passe la
# base Hub de window.DRACO_URLS (backend/draco_config) en
# urls.hub de DRACO_BAR_CONFIG.
js = (FRONTEND / "draco-barre.js").read_text(encoding="utf-8")
assert 'hub: "https://hub.dracodev.net/"' in js
assert 'hub: "https://hub-dev.dracodev.net/"' in js
assert '"telecharger", "Télécharger"' in js
assert '"a-propos", "À Propos"' in js
assert '"aide", "Aides"' in js
assert '"profil", "Profil"' in js
assert 'data-hub-link' in js
# Profil n'est plus un item relayé ; Déconnexion le reste.
assert 'data-compte-act", "profil"' not in js
assert 'data-compte-act", "deconnexion"' in js
html = _index()
assert "window.DRACO_URLS.hub" in html
assert "v0.3.0" in html
def test_panneau_spark_integre_218():
@@ -87,7 +145,8 @@ def test_panneau_console_surimpression_222():
assert "draco-console-sur" in panneau
assert "assurerSurimpression" in panneau
assert "draco-console-styles" in panneau
assert 'surCadre.src = urlConsole()' in panneau
assert 'var url = urlConsole()' in panneau
assert 'surCadre.src = url' in panneau
assert 'surEl.style.width = "100%"' in panneau
assert 'data-dc="sidebar"' in panneau
assert "Mode sidebar" in panneau
@@ -104,7 +163,8 @@ def test_csp_ouvre_le_service_d_agents_218():
# dérivée de la configuration (backend.draco_config.agent_origine,
# testée via la config dans tests/test_security_headers.py).
main = (Path(__file__).resolve().parent.parent / "backend" / "main.py").read_text(encoding="utf-8")
ligne = next(l for l in main.splitlines() if "connect-src" in l)
ligne = next(l for l in main.splitlines()
if "connect-src" in l and not l.strip().startswith("#"))
assert "agent_origine" in main
assert "{agent}" in ligne
@@ -146,3 +206,23 @@ def test_panneau_version_service_affichee_225():
# La ligne naît masquée et n'est démasquée qu'avec une version lue.
assert 'class="draco-panneau-version"' in panneau
assert "versionEl.hidden = false" in panneau
def test_barre_v040_icones_monoline_menu_compte():
# v0.4.0 (HUB-11, fusion #125) : les items du menu compte
# portent des icônes SVG monoline construites en DOM, dans le
# style des icônes du rail de la console — aucun emoji, aucune
# icône pleine. La vendange reste verbatim commons.
js = (FRONTEND / "draco-barre.js").read_text(encoding="utf-8")
assert "MENU_ICONES" in js
for nom in ("telecharger", "a-propos", "aide", "profil", "deconnexion"):
assert nom in js
assert "draco-menu-icone" in js
assert "stroke: currentColor" not in js # attributs posés en DOM
assert 'setAttribute("stroke", "currentColor")' in js
assert 'setAttribute("fill", "none")' in js
css = _css()
assert ".draco-menu-icone" in css
bloc = css.split(".draco-menu-icone {", 1)[1].split("}", 1)[0]
assert "width: 18px" in bloc
assert "var(--ink-2)" in bloc
+33 -1
View File
@@ -19,7 +19,9 @@ def env_propre(monkeypatch):
"""Aucune variable d'URL d'écosystème posée (défauts officiels)."""
for nom in (
"DRACO_AGENT_URL", "DRACO_OBSIGATE_URL", "DRACO_FLOWDECK_URL",
"DRACO_TIKTAK_URL", "DRACO_KANBANTE_URL", "DRACO_GITEA_URL",
"DRACO_TIKTAK_URL", "DRACO_KANBANTE_URL", "DRACO_SHAARIT_URL",
"DRACO_HUB_URL",
"DRACO_GITEA_URL",
"AGENT_EVENEMENTS_URL",
):
monkeypatch.delenv(nom, raising=False)
@@ -37,6 +39,36 @@ def test_defauts_officiels_sans_dev(env_propre):
assert "-dev" not in url
def test_shaarit_defaut_et_derivation(env_propre):
"""Barre v0.2.0 : ShaarIt suit le même mécanisme que les autres
sœurs — défaut officiel, dérivation -dev depuis l'hôte de l'agent,
variable explicite prioritaire."""
assert draco_config.app_url("shaarit") == "https://shaarit.dracodev.net"
env_propre.setenv(
"AGENT_EVENEMENTS_URL", "https://agent-dev.dracodev.net/api/v1/evenements")
assert draco_config.app_url("shaarit") == "https://shaarit-dev.dracodev.net"
assert draco_config.urls_publiques()["shaarit"] == \
"https://shaarit-dev.dracodev.net"
env_propre.setenv("DRACO_SHAARIT_URL", "https://bm.example.net/")
assert draco_config.app_url("shaarit") == "https://bm.example.net"
def test_hub_defaut_et_derivation(env_propre):
"""Barre v0.3.0 (HUB-1) : le Hub suit le même mécanisme que les
autres sœurs — défaut officiel, dérivation -dev depuis l'hôte
de l'agent, variable explicite prioritaire ; il est servi dans
urls_publiques() (window.DRACO_URLS) pour les liens du menu
compte, sans onglet dans la barre."""
assert draco_config.app_url("hub") == "https://hub.dracodev.net"
env_propre.setenv(
"AGENT_EVENEMENTS_URL", "https://agent-dev.dracodev.net/api/v1/evenements")
assert draco_config.app_url("hub") == "https://hub-dev.dracodev.net"
assert draco_config.urls_publiques()["hub"] == \
"https://hub-dev.dracodev.net"
env_propre.setenv("DRACO_HUB_URL", "https://hub.example.net/")
assert draco_config.app_url("hub") == "https://hub.example.net"
def test_agent_derive_des_evenements(env_propre):
"""Sans DRACO_AGENT_URL, l'origine suit AGENT_EVENEMENTS_URL —
la configuration réelle du poste (dev inclus)."""
+5 -2
View File
@@ -38,17 +38,20 @@ def test_origin_of():
def test_sister_origins_dev():
# Correctif 2026-10-28 : 10 origines officielles (8 sœurs + 2 agent), prod ET dev,
# quelle que soit la résolution.
# Correctif 2026-10-28 : origines officielles prod ET dev, quelle
# que soit la résolution ; ShaarIt ajoutée par la fédération
# v0.2.1 (12 origines : 10 sœurs + 2 agent).
assert sister_origins(RESOLVED_DEV) == {
"https://og.dracodev.net",
"https://flowdeck.dracodev.net",
"https://tiktak.dracodev.net",
"https://kanbante.dracodev.net",
"https://shaarit.dracodev.net",
"https://og-dev.dracodev.net",
"https://flowdeck-dev.dracodev.net",
"https://tiktak-dev.dracodev.net",
"https://kanbante-dev.dracodev.net",
"https://shaarit-dev.dracodev.net",
"https://agent.dracodev.net",
"https://agent-dev.dracodev.net",
}
+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
+1 -1
View File
@@ -33,7 +33,7 @@ class TestListAccessibleVaults:
def test_metadata_fields(self, client):
vault = next(v for v in list_accessible_vaults(ADMIN) if v["name"] == "TestVault")
assert set(vault) == {"name", "file_count", "tag_count", "type"}
assert set(vault) == {"name", "file_count", "tag_count", "type", "readonly"}
assert vault["file_count"] >= 1
+251
View File
@@ -0,0 +1,251 @@
"""Tests des vaults DIR « Spark » : exclusions par vault et lecture seule.
Couvre le contrat DIR_N_READONLY / DIR_N_IGNORED_DIRS / DIR_N_IGNORED_PATHS :
- parsing des trois variables dans load_vault_config() ;
- helper chemin_exclu (noms de dossiers et motifs fnmatch à préfixes) ;
- navigation filtrée, lectures exclues en 404 (vue, raw, téléchargement),
fichier non exclu lisible, indexation et list_paths filtrés ;
- mutation refusée en 403 sur un vault déclaré readonly ;
- /api/vaults expose readonly=true pour un DIR readonly.
Le vault de test est un répertoire temporaire monté en DIR (jamais un
vault de notes), avec une arborescence qui imite la vue Spark visée :
Console/, Workspaces/ (avec un .home), Executeurs/Hermes/{missions,data,.env}.
"""
from __future__ import annotations
import asyncio
import os
from pathlib import Path
from typing import ClassVar
import pytest
from fastapi.testclient import TestClient
VAULT_NAME = "SparkTest"
def _build_spark_tree(root: Path) -> None:
"""Crée l'arborescence technique de test sous *root*."""
(root / "Console").mkdir(parents=True)
(root / "Console" / "code.py").write_text('print("visible")\n', encoding="utf-8")
(root / "Console" / "notes.md").write_text("# Notes\n", encoding="utf-8")
atelier = root / "Workspaces" / "atelier"
(atelier / ".home").mkdir(parents=True)
(atelier / "note.md").write_text("# Atelier\n", encoding="utf-8")
(atelier / ".home" / "cache.md").write_text("# Cache du sandbox\n", encoding="utf-8")
venv = root / ".venv"
venv.mkdir()
(venv / "lib.py").write_text("# lib du venv\n", encoding="utf-8")
hermes = root / "Executeurs" / "Hermes"
(hermes / "missions").mkdir(parents=True)
(hermes / "data").mkdir(parents=True)
(hermes / "missions" / "mission.md").write_text("# Mission\n", encoding="utf-8")
(hermes / "data" / "config.md").write_text("# Profil de l'exécuteur\n", encoding="utf-8")
(hermes / ".env").write_text("CLE_FICTIVE=ne-pas-exposer\n", encoding="utf-8")
(root / "racine.md").write_text("# Racine\n", encoding="utf-8")
@pytest.fixture
def spark_dir(tmp_path: Path) -> Path:
root = tmp_path / "SparkDir"
root.mkdir()
_build_spark_tree(root)
return root
@pytest.fixture
def spark_client(spark_dir: Path):
"""TestClient avec le répertoire de test monté en vault DIR readonly.
Même patron que le fixture ``app_with_vault`` de conftest.py, mais en
DIR_N_* avec les trois nouvelles variables posées.
"""
os.environ["DIR_1_NAME"] = VAULT_NAME
os.environ["DIR_1_PATH"] = str(spark_dir)
os.environ["DIR_1_READONLY"] = "true"
os.environ["DIR_1_IGNORED_DIRS"] = ".home,.venv"
os.environ["DIR_1_IGNORED_PATHS"] = "Executeurs/*/data,Executeurs/*/.env"
os.environ["OBSIGATE_AUTH_ENABLED"] = "false"
import backend.main
backend.main._load_config = lambda: {"watcher_enabled": False}
from backend.indexer import build_index
from backend.main import app
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop.run_until_complete(build_index())
return TestClient(app)
# ── Parsing de la configuration ─────────────────────────────────────
class TestParsingConfig:
def test_dir_variables_parsees(self, monkeypatch, tmp_path):
monkeypatch.setenv("DIR_1_NAME", "SparkParse")
monkeypatch.setenv("DIR_1_PATH", str(tmp_path))
monkeypatch.setenv("DIR_1_READONLY", "true")
monkeypatch.setenv("DIR_1_IGNORED_DIRS", ".home, .venv ,__pycache__")
monkeypatch.setenv("DIR_1_IGNORED_PATHS", "Executeurs/*/data, Executeurs/*/.env")
from backend.indexer import load_vault_config
cfg = load_vault_config()["SparkParse"]
assert cfg["type"] == "DIR"
assert cfg["readonly"] is True
assert cfg["ignoredDirs"] == {".home", ".venv", "__pycache__"}
assert cfg["ignoredPaths"] == ["Executeurs/*/data", "Executeurs/*/.env"]
def test_dir_sans_variables_defauts(self, monkeypatch, tmp_path):
monkeypatch.setenv("DIR_1_NAME", "DirNu")
monkeypatch.setenv("DIR_1_PATH", str(tmp_path))
from backend.indexer import load_vault_config
cfg = load_vault_config()["DirNu"]
assert cfg["readonly"] is False
assert cfg["ignoredDirs"] == set()
assert cfg["ignoredPaths"] == []
# ── Helper chemin_exclu ─────────────────────────────────────────────
class TestCheminExclu:
CFG: ClassVar[dict] = {
"ignoredDirs": {".home", ".venv"},
"ignoredPaths": ["Executeurs/*/data", "Executeurs/*/.env"],
}
def test_nom_de_dossier_a_toute_profondeur(self):
from backend.services.vaults import chemin_exclu_dans_config
assert chemin_exclu_dans_config(self.CFG, "Workspaces/atelier/.home/cache.md")
assert chemin_exclu_dans_config(self.CFG, ".venv/lib.py")
assert chemin_exclu_dans_config(self.CFG, ".home")
def test_motif_chemin_et_sous_arbre(self):
from backend.services.vaults import chemin_exclu_dans_config
assert chemin_exclu_dans_config(self.CFG, "Executeurs/Hermes/data")
assert chemin_exclu_dans_config(self.CFG, "Executeurs/Hermes/data/config.md")
assert chemin_exclu_dans_config(self.CFG, "Executeurs/Pi/data/x/y.py")
assert chemin_exclu_dans_config(self.CFG, "Executeurs/Hermes/.env")
def test_non_exclus_et_tolerance(self):
from backend.services.vaults import chemin_exclu_dans_config
assert not chemin_exclu_dans_config(self.CFG, "Console/code.py")
assert not chemin_exclu_dans_config(self.CFG, "Executeurs/Hermes/missions/mission.md")
assert not chemin_exclu_dans_config({}, "Console/code.py")
assert not chemin_exclu_dans_config(None, "Console/code.py")
assert not chemin_exclu_dans_config(self.CFG, "")
# ── Navigation et lectures via l'API ────────────────────────────────
class TestNavigationEtLecture:
def test_browse_racine_cache_les_dossiers_ignores(self, spark_client):
resp = spark_client.get(f"/api/browse/{VAULT_NAME}")
assert resp.status_code == 200
names = {item["name"] for item in resp.json()["items"]}
assert {"Console", "Workspaces", "Executeurs", "racine.md"} <= names
assert ".venv" not in names
def test_browse_sous_dossier_cache_data(self, spark_client):
resp = spark_client.get(f"/api/browse/{VAULT_NAME}", params={"path": "Executeurs/Hermes"})
assert resp.status_code == 200
names = {item["name"] for item in resp.json()["items"]}
assert "missions" in names
assert "data" not in names
def test_browse_chemin_exclu_404(self, spark_client):
resp = spark_client.get(f"/api/browse/{VAULT_NAME}", params={"path": "Executeurs/Hermes/data"})
assert resp.status_code == 404
def test_vue_fichier_exclu_404(self, spark_client):
resp = spark_client.get(f"/api/file/{VAULT_NAME}", params={"path": "Executeurs/Hermes/data/config.md"})
assert resp.status_code == 404
def test_raw_exclu_404(self, spark_client):
resp = spark_client.get(f"/api/file/{VAULT_NAME}/raw", params={"path": "Executeurs/Hermes/data/config.md"})
assert resp.status_code == 404
def test_raw_env_exclu_404(self, spark_client):
resp = spark_client.get(f"/api/file/{VAULT_NAME}/raw", params={"path": "Executeurs/Hermes/.env"})
assert resp.status_code == 404
assert "CLE_FICTIVE" not in resp.text
def test_telechargement_env_exclu_404(self, spark_client):
resp = spark_client.get(f"/api/file/{VAULT_NAME}/download", params={"path": "Executeurs/Hermes/.env"})
assert resp.status_code == 404
assert "CLE_FICTIVE" not in resp.text
def test_fichier_non_exclu_lisible(self, spark_client):
resp = spark_client.get(f"/api/file/{VAULT_NAME}/raw", params={"path": "Console/code.py"})
assert resp.status_code == 200
assert "visible" in resp.json()["raw"]
# ── Indexation et liste des chemins ─────────────────────────────────
class TestIndexation:
def test_index_ne_contient_aucun_exclu(self, spark_client):
from backend.indexer import get_vault_data
data = get_vault_data(VAULT_NAME)
assert data is not None
file_paths = {f["path"] for f in data.get("files", [])}
assert "Console/code.py" in file_paths
for p in file_paths:
segments = p.split("/")
assert ".home" not in segments and ".venv" not in segments
assert "data" not in segments[:-1] or not p.startswith("Executeurs/")
path_entries = {e["path"] for e in data.get("paths", [])}
assert "Executeurs/Hermes/data" not in path_entries
assert "Workspaces/atelier/.home" not in path_entries
def test_list_paths_filtre_les_exclus(self, spark_client):
from backend.services.search import list_paths
result = list_paths(VAULT_NAME)
paths = {r["path"] for r in result["results"]}
assert "Console/code.py" in paths
assert not any("/data/" in f"/{p}/" and p.startswith("Executeurs/") for p in paths)
assert not any(".home" in p.split("/") or ".venv" in p.split("/") for p in paths)
# ── Lecture seule ───────────────────────────────────────────────────
class TestLectureSeule:
def test_save_refuse_403(self, spark_client):
resp = spark_client.put(
f"/api/file/{VAULT_NAME}/save",
params={"path": "Console/code.py"},
json={"content": "print('modifié')\n"},
)
assert resp.status_code == 403
def test_creation_dossier_refusee_403(self, spark_client):
resp = spark_client.post(f"/api/directory/{VAULT_NAME}", json={"path": "NouveauDossier"})
assert resp.status_code == 403
def test_vaults_expose_readonly(self, spark_client):
resp = spark_client.get("/api/vaults")
assert resp.status_code == 200
entry = next(v for v in resp.json() if v["name"] == VAULT_NAME)
assert entry["type"] == "DIR"
assert entry["readonly"] is True