Compare commits

...
Author SHA1 Message Date
bruno f3b507b8d9 Merge pull request '[Theme] Nouveau theme Spark (dark/light, identique a la Console Spark) et defaut = Spark dark' (#28) from feat/theme-spark into main
CI / security (push) Failing after 0s
CI / lint (push) Successful in 3m6s
CI / test (push) Failing after 0s
CI / build (push) Skipped
CI / e2e (push) Skipped
2026-10-27 23:25:10 -04:00
bruno c01c7e862d Merge pull request '[fix] Mypy Coffre Secrets (3 erreurs) + version 2.80.2' (#27) from fix/mypy-coffre-2.80.2 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-27 23:24:44 -04:00
bruno 99cd765997 [fix] CHANGELOG 2.80.2 : bouton Nouveau secret + plancher msgpack
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-27 23:20:41 -04:00
bruno 5266506b56 [fix] Securite : plancher msgpack>=1.2.3 (CVE-2026-114613, pip-audit bloquant)
CI / lint (pull_request) Failing after 1s
CI / test (pull_request) Skipped
CI / build (pull_request) Skipped
CI / e2e (pull_request) Skipped
CI / security (pull_request) Successful in 1m56s
2026-10-27 23:20:35 -04:00
bruno 682a5030d3 [fix] Coffre Secrets : valeurField attache l'oeil a wrap avant insertBefore (NotFoundError bouton Nouveau secret)
CI / security (pull_request) Failing after 0s
CI / lint (pull_request) Successful in 3m19s
CI / test (pull_request) Failing after 1m47s
CI / build (pull_request) Skipped
CI / e2e (pull_request) Skipped
2026-10-27 23:20:30 -04:00
bruno fc3d847d85 [fix] mypy Coffre + bump 2.80.2 : CHANGELOG.md
CI / security (pull_request) Failing after 2m29s
CI / lint (pull_request) Successful in 2m55s
CI / test (pull_request) Failing after 5m10s
CI / build (pull_request) Skipped
CI / e2e (pull_request) Skipped
2026-10-27 23:10:51 -04:00
bruno 82756030c6 [fix] mypy Coffre + bump 2.80.2 : docs/ROADMAP.md 2026-10-27 23:10:48 -04:00
bruno 0352451ead [fix] mypy Coffre + bump 2.80.2 : README.fr.md 2026-10-27 23:10:44 -04:00
bruno 593cf492c3 [fix] mypy Coffre + bump 2.80.2 : README.md 2026-10-27 23:10:41 -04:00
bruno 0b8ad8b0f3 [fix] mypy Coffre + bump 2.80.2 : desktop/Cargo.lock 2026-10-27 23:10:39 -04:00
bruno 111d927ece [fix] mypy Coffre + bump 2.80.2 : desktop/Cargo.toml 2026-10-27 23:10:36 -04:00
bruno 2dae023ee3 [fix] mypy Coffre + bump 2.80.2 : desktop/tauri.conf.json 2026-10-27 23:10:34 -04:00
bruno b12122f78a [fix] mypy Coffre + bump 2.80.2 : package.json 2026-10-27 23:10:31 -04:00
bruno 0c7b3162c8 [fix] mypy Coffre + bump 2.80.2 : VERSION 2026-10-27 23:10:29 -04:00
bruno d4d050297b [fix] mypy Coffre + bump 2.80.2 : backend/coffre_store.py 2026-10-27 23:10:27 -04:00
bruno 8d768bf9d5 [fix] mypy Coffre + bump 2.80.2 : backend/coffre_crypto.py 2026-10-27 23:10:24 -04:00
bruno 47ac8bf6c0 Merge pull request '[fix] Version 2.80.1 synchronisee + corrections ruff (60 erreurs CI)' (#26) from fix/version-2.80.1-ruff into main
CI / security (push) Failing after 1s
CI / lint (push) Failing after 2m10s
CI / test (push) Skipped
CI / build (push) Skipped
CI / e2e (push) Skipped
2026-10-27 23:00:13 -04:00
bruno 01572736c0 [fix] Corrections ruff CI (UP017/UP041/UP012/I001/F401/FURB162) : backend/webhooks.py
CI / security (pull_request) Successful in 1m57s
CI / lint (pull_request) Failing after 2m7s
CI / test (pull_request) Skipped
CI / build (pull_request) Skipped
CI / e2e (pull_request) Skipped
2026-10-27 22:58:14 -04:00
bruno b6e5f01748 [fix] Corrections ruff CI (UP017/UP041/UP012/I001/F401/FURB162) : backend/watcher.py 2026-10-27 22:58:11 -04:00
bruno 90da0ac990 [fix] Corrections ruff CI (UP017/UP041/UP012/I001/F401/FURB162) : backend/tools/audit.py 2026-10-27 22:58:08 -04:00
bruno bc26d1506b [fix] Corrections ruff CI (UP017/UP041/UP012/I001/F401/FURB162) : backend/share.py 2026-10-27 22:58:05 -04:00
bruno 769e3ca559 [fix] Corrections ruff CI (UP017/UP041/UP012/I001/F401/FURB162) : backend/services/vaults.py 2026-10-27 22:58:02 -04:00
bruno b5d789f02a [fix] Corrections ruff CI (UP017/UP041/UP012/I001/F401/FURB162) : backend/services/recent.py 2026-10-27 22:57:59 -04:00
bruno c8dce25e0d [fix] Corrections ruff CI (UP017/UP041/UP012/I001/F401/FURB162) : backend/services/backups.py 2026-10-27 22:57:56 -04:00
bruno b322e2d24a [fix] Corrections ruff CI (UP017/UP041/UP012/I001/F401/FURB162) : backend/scheduler.py 2026-10-27 22:57:53 -04:00
bruno 414ec69c0a [fix] Corrections ruff CI (UP017/UP041/UP012/I001/F401/FURB162) : backend/routers/realtime.py 2026-10-27 22:57:50 -04:00
bruno f5ca656c04 [fix] Corrections ruff CI (UP017/UP041/UP012/I001/F401/FURB162) : backend/routers/backups.py 2026-10-27 22:57:47 -04:00
bruno e158232aba [fix] Corrections ruff CI (UP017/UP041/UP012/I001/F401/FURB162) : backend/push.py 2026-10-27 22:57:44 -04:00
bruno 632871ac24 [fix] Corrections ruff CI (UP017/UP041/UP012/I001/F401/FURB162) : backend/notify.py 2026-10-27 22:57:40 -04:00
bruno 7af7b2c8c2 [fix] Corrections ruff CI (UP017/UP041/UP012/I001/F401/FURB162) : backend/indexer.py 2026-10-27 22:57:36 -04:00
bruno 97132770e4 [fix] Corrections ruff CI (UP017/UP041/UP012/I001/F401/FURB162) : backend/guide_export.py 2026-10-27 22:57:32 -04:00
bruno 683ad3ef94 [fix] Corrections ruff CI (UP017/UP041/UP012/I001/F401/FURB162) : backend/export.py 2026-10-27 22:57:29 -04:00
bruno 604383f51c [fix] Corrections ruff CI (UP017/UP041/UP012/I001/F401/FURB162) : backend/coffre_store.py 2026-10-27 22:57:25 -04:00
bruno bdeb2f00b3 [fix] Corrections ruff CI (UP017/UP041/UP012/I001/F401/FURB162) : backend/coffre_crypto.py 2026-10-27 22:57:21 -04:00
bruno 8688875fae [fix] Corrections ruff CI (UP017/UP041/UP012/I001/F401/FURB162) : backend/bookslm_routes.py 2026-10-27 22:57:17 -04:00
bruno a42271c7de [fix] Corrections ruff CI (UP017/UP041/UP012/I001/F401/FURB162) : backend/auth/user_store.py 2026-10-27 22:57:13 -04:00
bruno cf4685eaf1 [fix] Corrections ruff CI (UP017/UP041/UP012/I001/F401/FURB162) : backend/auth/router.py 2026-10-27 22:57:10 -04:00
bruno 5023fb6774 [fix] Corrections ruff CI (UP017/UP041/UP012/I001/F401/FURB162) : backend/audit.py 2026-10-27 22:57:06 -04:00
bruno 04eb70cbe4 [fix] Corrections ruff CI (UP017/UP041/UP012/I001/F401/FURB162) : backend/admin.py 2026-10-27 22:57:01 -04:00
bruno 4dc010f54d [fix] Bump VERSION 2.79.0 -> 2.80.1 (synchronisation bump_version.py) : CHANGELOG.md 2026-10-27 22:56:57 -04:00
bruno f7e36bc44e [fix] Bump VERSION 2.79.0 -> 2.80.1 (synchronisation bump_version.py) : docs/ROADMAP.md 2026-10-27 22:56:53 -04:00
bruno a9ac1c06a7 [fix] Bump VERSION 2.79.0 -> 2.80.1 (synchronisation bump_version.py) : README.fr.md 2026-10-27 22:56:48 -04:00
bruno 9bf6e7492f [fix] Bump VERSION 2.79.0 -> 2.80.1 (synchronisation bump_version.py) : README.md 2026-10-27 22:56:43 -04:00
bruno 14070cef43 [fix] Bump VERSION 2.79.0 -> 2.80.1 (synchronisation bump_version.py) : desktop/Cargo.lock 2026-10-27 22:56:37 -04:00
bruno 6b1abbb50e [fix] Bump VERSION 2.79.0 -> 2.80.1 (synchronisation bump_version.py) : desktop/Cargo.toml 2026-10-27 22:56:33 -04:00
bruno 46c641ebc5 [fix] Bump VERSION 2.79.0 -> 2.80.1 (synchronisation bump_version.py) : desktop/tauri.conf.json 2026-10-27 22:56:30 -04:00
bruno 2d036f5126 [fix] Bump VERSION 2.79.0 -> 2.80.1 (synchronisation bump_version.py) : package.json 2026-10-27 22:56:27 -04:00
bruno 2c69e357b8 [fix] Bump VERSION 2.79.0 -> 2.80.1 (synchronisation bump_version.py) : VERSION 2026-10-27 22:56:25 -04:00
bruno ba81f22d40 Merge pull request '[Coffre Secrets] Phase 1 complète : chiffrement, API et UI (1a + 1b + 1c)' (#25) from feat/coffre-secrets-phase1 into main
CI / lint (push) Failing after 1m47s
CI / test (push) Skipped
CI / build (push) Skipped
CI / e2e (push) Skipped
CI / security (push) Successful in 1m58s
2026-10-27 22:29:09 -04:00
bruno 070add4e7b [Thème Spark] Anti-FOUC admin aligné sur fond/texte Spark
CI / security (pull_request) Failing after 0s
CI / lint (pull_request) Successful in 3m10s
CI / test (pull_request) Failing after 5m22s
CI / build (pull_request) Skipped
CI / e2e (pull_request) Skipped
2026-10-27 22:10:14 -04:00
bruno 847b3225b2 [Thème Spark] Splash et meta theme-color alignés sur Spark dark 2026-10-27 22:10:11 -04:00
bruno 3ae5c7efac [Thème Spark] Bloc :root de secours aligné sur la palette dark Spark 2026-10-27 22:10:07 -04:00
bruno 4966c27548 [Thème Spark] Ajout du thème 'spark' (dark/light, 39 variables) et défaut spark/dark 2026-10-27 22:10:04 -04:00
37 changed files with 258 additions and 149 deletions
+25 -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). et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
> **En cours de développement** : les changements à venir sont listés dans la section > **En cours de développement** : les changements à venir sont listés dans la section
> [Unreleased](#unreleased). La dernière version livrée est **2.79.0**. > [Unreleased](#unreleased). La dernière version livrée est **2.80.2**.
--- ---
@@ -14,6 +14,30 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
--- ---
## [2.80.2] — 2026-10-27
### Corrigé
- **CI lint (mypy)** — 3 erreurs dans le Coffre Secrets : annotation explicite `type[Any] | None` pour `AESGCM` dans `coffre_crypto.py` (le repli sans `cryptography` reste `None`, et `_require_aesgcm()` retourne désormais la classe pour un narrowing propre) ; annotation `dict[str, Any]` du coffre dans `create_coffre` (`coffre_store.py`), qui inférait ses valeurs en `str | None` et refusait le dict de `wrap_dek` ainsi que `coffre["id"]` en argument. Aucun `type: ignore` aveugle ajouté, aucun changement de comportement.
- **Bouton Nouveau secret inoperant (console NotFoundError)** — `valeurField` appelait `wrap.insertBefore(input, eyeBtn)` alors que le bouton oeil n'avait jamais ete rattache a `wrap` : l'oeil est desormais ajoute a `wrap` avant le premier `build`, la modale de creation (et celle de rotation) s'ouvre.
- **CI security (pip-audit)** — plancher `msgpack>=1.2.3` : la version transitive 1.2.1 etait signalee pour CVE-2026-114613 (correctif 1.2.3).
---
## [2.80.1] — 2026-10-27
### Ajouté
- **Coffre Secrets — Phase 1** (contenu de la release 2.80.0, dont l'entrée changelog manquait) : chiffrement par enveloppe AES-256-GCM, API `/api/coffres`, nœud virtuel Secrets sous le Home de l'utilisateur.
### Corrigé
- **Étiquette de version 2.80.0** — le tag v2.80.0 pointait sur un contenu dont le fichier `VERSION` valait encore 2.79.0 (un merge ne déclenche pas le bump automatique) ; `VERSION` et tous les fichiers dérivés passent à 2.80.1, pour que la version affichée corresponde au tag de la prochaine release.
- **CI lint (ruff)** — 60 erreurs corrigées dans `backend/` : alias `datetime.UTC` (Python 3.11+), `TimeoutError` natif, tri d'imports et import inutilisé dans `coffre_store.py`, `.encode()` sans argument redondant, `fromisoformat` gérant « Z » ; aucune règle ruff masquée, aucun changement de comportement.
---
## [2.79.0] — 2026-10-26 ## [2.79.0] — 2026-10-26
### Ajouté ### Ajouté
+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. **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.79.0-blue.svg)]() [![Version](https://img.shields.io/badge/Version-2.80.2-blue.svg)]()
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![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/) [![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/) [![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 ## 📝 Changelog
Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.79.0). Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.80.2).
--- ---
*Projet : ObsiGate | Version : 2.79.0 | Dernière mise à jour : Septembre 2026* *Projet : ObsiGate | Version : 2.80.2 | 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. **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.79.0-blue.svg)]() [![Version](https://img.shields.io/badge/Version-2.80.2-blue.svg)]()
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![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/) [![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/) [![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 ## 📝 Changelog
See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.79.0). See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.80.2).
--- ---
*Project: ObsiGate | Version: 2.79.0 | Last updated: September 2026* *Project: ObsiGate | Version: 2.80.2 | Last updated: September 2026*
+1 -1
View File
@@ -1 +1 @@
2.79.0 2.80.2
+2 -2
View File
@@ -10,7 +10,7 @@ import logging
import os import os
import shutil import shutil
import time import time
from datetime import datetime, timezone from datetime import UTC, datetime
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@@ -244,7 +244,7 @@ def _build_stats_payload() -> dict:
"disk_used_gb": disk_used_gb, "disk_used_gb": disk_used_gb,
"disk_total_gb": disk_total_gb, "disk_total_gb": disk_total_gb,
"uptime_seconds": int(_server_uptime_seconds()), "uptime_seconds": int(_server_uptime_seconds()),
"timestamp": datetime.now(timezone.utc).isoformat(), "timestamp": datetime.now(UTC).isoformat(),
} }
+7 -7
View File
@@ -10,7 +10,7 @@ Format: JSON lines (one JSON object per line) for easy parsing.
import json import json
import logging import logging
import os import os
from datetime import datetime, timezone from datetime import UTC, datetime
from pathlib import Path from pathlib import Path
logger = logging.getLogger("obsigate.audit") logger = logging.getLogger("obsigate.audit")
@@ -54,7 +54,7 @@ def log_file_save(
): ):
"""Log a file save (PUT) operation.""" """Log a file save (PUT) operation."""
_write_entry({ _write_entry({
"timestamp": datetime.now(timezone.utc).isoformat(), "timestamp": datetime.now(UTC).isoformat(),
"action": "file_save", "action": "file_save",
"username": username, "username": username,
"ip": ip or "unknown", "ip": ip or "unknown",
@@ -72,7 +72,7 @@ def log_file_delete(
): ):
"""Log a file delete operation.""" """Log a file delete operation."""
_write_entry({ _write_entry({
"timestamp": datetime.now(timezone.utc).isoformat(), "timestamp": datetime.now(UTC).isoformat(),
"action": "file_delete", "action": "file_delete",
"username": username, "username": username,
"ip": ip or "unknown", "ip": ip or "unknown",
@@ -88,7 +88,7 @@ def log_config_change(
): ):
"""Log a configuration change.""" """Log a configuration change."""
_write_entry({ _write_entry({
"timestamp": datetime.now(timezone.utc).isoformat(), "timestamp": datetime.now(UTC).isoformat(),
"action": "config_change", "action": "config_change",
"username": username, "username": username,
"ip": ip or "unknown", "ip": ip or "unknown",
@@ -99,7 +99,7 @@ def log_config_change(
def log_vault_add(username: str, vault_name: str, vault_path: str, ip: str | None = None): def log_vault_add(username: str, vault_name: str, vault_path: str, ip: str | None = None):
"""Log a vault addition.""" """Log a vault addition."""
_write_entry({ _write_entry({
"timestamp": datetime.now(timezone.utc).isoformat(), "timestamp": datetime.now(UTC).isoformat(),
"action": "vault_add", "action": "vault_add",
"username": username, "username": username,
"ip": ip or "unknown", "ip": ip or "unknown",
@@ -111,7 +111,7 @@ def log_vault_add(username: str, vault_name: str, vault_path: str, ip: str | Non
def log_vault_remove(username: str, vault_name: str, ip: str | None = None): def log_vault_remove(username: str, vault_name: str, ip: str | None = None):
"""Log a vault removal.""" """Log a vault removal."""
_write_entry({ _write_entry({
"timestamp": datetime.now(timezone.utc).isoformat(), "timestamp": datetime.now(UTC).isoformat(),
"action": "vault_remove", "action": "vault_remove",
"username": username, "username": username,
"ip": ip or "unknown", "ip": ip or "unknown",
@@ -136,7 +136,7 @@ def log_coffre_event(
(coffre:<slug>/<nom>), des identifiants et des codes de résultat. (coffre:<slug>/<nom>), des identifiants et des codes de résultat.
""" """
entry = { entry = {
"timestamp": datetime.now(timezone.utc).isoformat(), "timestamp": datetime.now(UTC).isoformat(),
"action": action, "action": action,
"username": username, "username": username,
"ip": ip or "unknown", "ip": ip or "unknown",
+3 -2
View File
@@ -7,6 +7,7 @@ import binascii
import logging import logging
import os import os
import re import re
from datetime import UTC
from fastapi import APIRouter, Body, Depends, HTTPException, Request, Response from fastapi import APIRouter, Body, Depends, HTTPException, Request, Response
from pydantic import BaseModel, validator from pydantic import BaseModel, validator
@@ -696,7 +697,7 @@ async def mfa_webauthn_register(
Returns recovery codes when MFA is newly enabled (they were never issued). Returns recovery codes when MFA is newly enabled (they were never issued).
""" """
from datetime import datetime, timezone from datetime import datetime
from .user_store import get_user, update_user from .user_store import get_user, update_user
from .webauthn_mfa import complete_registration, resolve_relying_party from .webauthn_mfa import complete_registration, resolve_relying_party
@@ -716,7 +717,7 @@ async def mfa_webauthn_register(
logger.warning(f"WebAuthn registration failed for {current_user['username']}: {e}") logger.warning(f"WebAuthn registration failed for {current_user['username']}: {e}")
raise HTTPException(400, "Validation du credential WebAuthn échouée") raise HTTPException(400, "Validation du credential WebAuthn échouée")
record["registered_at"] = datetime.now(timezone.utc).isoformat() record["registered_at"] = datetime.now(UTC).isoformat()
creds = list(user.get("webauthn_credentials", [])) creds = list(user.get("webauthn_credentials", []))
creds = [c for c in creds if c.get("credential_id") != record["credential_id"]] creds = [c for c in creds if c.get("credential_id") != record["credential_id"]]
creds.append(record) creds.append(record)
+7 -7
View File
@@ -8,7 +8,7 @@ import logging
import shutil import shutil
import threading import threading
import uuid import uuid
from datetime import datetime, timedelta, timezone from datetime import UTC, datetime, timedelta
from pathlib import Path from pathlib import Path
from .password import hash_password from .password import hash_password
@@ -97,8 +97,8 @@ def create_user(
"active": True, "active": True,
"language": "fr", # default UI language "language": "fr", # default UI language
"avatar": None, # profile picture data-URL (#113) "avatar": None, # profile picture data-URL (#113)
"created_at": datetime.now(timezone.utc).isoformat(), "created_at": datetime.now(UTC).isoformat(),
"password_changed_at": datetime.now(timezone.utc).timestamp(), "password_changed_at": datetime.now(UTC).timestamp(),
"last_login": None, "last_login": None,
"failed_attempts": 0, "failed_attempts": 0,
"locked_until": None, "locked_until": None,
@@ -126,7 +126,7 @@ def update_user(username: str, updates: dict) -> dict:
if "password" in safe_updates: if "password" in safe_updates:
safe_updates["password_hash"] = hash_password(safe_updates.pop("password")) safe_updates["password_hash"] = hash_password(safe_updates.pop("password"))
# BUG-028: invalidate every token issued before this change. # BUG-028: invalidate every token issued before this change.
safe_updates["password_changed_at"] = datetime.now(timezone.utc).timestamp() safe_updates["password_changed_at"] = datetime.now(UTC).timestamp()
data["users"][username].update(safe_updates) data["users"][username].update(safe_updates)
_write(data) _write(data)
@@ -148,7 +148,7 @@ def delete_user(username: str):
def record_login_success(username: str): def record_login_success(username: str):
"""Record a successful login: update last_login, reset failed attempts.""" """Record a successful login: update last_login, reset failed attempts."""
update_user(username, { update_user(username, {
"last_login": datetime.now(timezone.utc).isoformat(), "last_login": datetime.now(UTC).isoformat(),
"failed_attempts": 0, "failed_attempts": 0,
"locked_until": None, "locked_until": None,
}) })
@@ -171,7 +171,7 @@ def record_login_failure(username: str) -> int:
# Lock after 5 failed attempts (15 minutes) # Lock after 5 failed attempts (15 minutes)
if attempts >= 5: if attempts >= 5:
locked_until = ( locked_until = (
datetime.now(timezone.utc) + timedelta(minutes=15) datetime.now(UTC) + timedelta(minutes=15)
).isoformat() ).isoformat()
updates["locked_until"] = locked_until updates["locked_until"] = locked_until
logger.warning(f"Account '{username}' locked after {attempts} failed attempts") logger.warning(f"Account '{username}' locked after {attempts} failed attempts")
@@ -189,7 +189,7 @@ def is_locked(username: str) -> bool:
if not user or not user.get("locked_until"): if not user or not user.get("locked_until"):
return False return False
locked_until = datetime.fromisoformat(user["locked_until"]) locked_until = datetime.fromisoformat(user["locked_until"])
if datetime.now(timezone.utc) > locked_until: if datetime.now(UTC) > locked_until:
# Lock expired — unlock # Lock expired — unlock
update_user(username, {"locked_until": None, "failed_attempts": 0}) update_user(username, {"locked_until": None, "failed_attempts": 0})
return False return False
+1 -1
View File
@@ -591,7 +591,7 @@ async def api_bookslm_agent(
while True: while True:
try: try:
kind, item = await asyncio.wait_for(queue.get(), timeout=0.25) kind, item = await asyncio.wait_for(queue.get(), timeout=0.25)
except asyncio.TimeoutError: except TimeoutError:
if run_task.done(): if run_task.done():
break break
continue continue
+19 -8
View File
@@ -24,14 +24,19 @@ import logging
import os import os
import stat import stat
from pathlib import Path from pathlib import Path
from typing import Any
logger = logging.getLogger("obsigate.coffre_crypto") logger = logging.getLogger("obsigate.coffre_crypto")
# Annotation explicite : sans elle, mypy refuse l'assignation de deux types
# (la classe AESGCM importée, puis None dans le repli sans cryptography)
# et le commentaire `type: ignore[assignment]` ne couvre pas ce code [misc].
AESGCM: type[Any] | None
try: # cryptography est une dépendance de la Phase 1a (requirements.txt) try: # cryptography est une dépendance de la Phase 1a (requirements.txt)
from cryptography.exceptions import InvalidTag from cryptography.exceptions import InvalidTag
from cryptography.hazmat.primitives.ciphers.aead import AESGCM from cryptography.hazmat.primitives.ciphers.aead import AESGCM
except Exception: # pragma: no cover - environnement sans cryptography except Exception: # pragma: no cover - environnement sans cryptography
AESGCM = None # type: ignore[assignment] AESGCM = None
InvalidTag = Exception # type: ignore[assignment,misc] InvalidTag = Exception # type: ignore[assignment,misc]
@@ -55,11 +60,17 @@ class CryptoIndisponibleError(CoffreCryptoError):
"""La bibliothèque ``cryptography`` n'est pas installée.""" """La bibliothèque ``cryptography`` n'est pas installée."""
def _require_aesgcm() -> None: def _require_aesgcm() -> type[Any]:
"""Retourne la classe AESGCM, ou lève si cryptography est absent.
Retourner la classe (plutôt qu'un simple garde) permet à mypy de
savoir que l'appelant détient bien un constructible, et non None.
"""
if AESGCM is None: if AESGCM is None:
raise CryptoIndisponibleError( raise CryptoIndisponibleError(
"cryptography n'est pas installé (dépendance Phase 1a)" "cryptography n'est pas installé (dépendance Phase 1a)"
) )
return AESGCM
def parse_kek(raw: str) -> bytes: def parse_kek(raw: str) -> bytes:
@@ -146,17 +157,17 @@ def _unb64(value: str) -> bytes:
def encrypt_with_key(key: bytes, plaintext: bytes, aad: bytes) -> dict: def encrypt_with_key(key: bytes, plaintext: bytes, aad: bytes) -> dict:
"""Chiffre avec AES-256-GCM. Retourne {nonce, ciphertext} en base64.""" """Chiffre avec AES-256-GCM. Retourne {nonce, ciphertext} en base64."""
_require_aesgcm() aesgcm = _require_aesgcm()
nonce = os.urandom(12) nonce = os.urandom(12)
ciphertext = AESGCM(key).encrypt(nonce, plaintext, aad) ciphertext = aesgcm(key).encrypt(nonce, plaintext, aad)
return {"nonce": _b64(nonce), "ciphertext": _b64(ciphertext)} return {"nonce": _b64(nonce), "ciphertext": _b64(ciphertext)}
def decrypt_with_key(key: bytes, nonce_b64: str, ciphertext_b64: str, aad: bytes) -> bytes: def decrypt_with_key(key: bytes, nonce_b64: str, ciphertext_b64: str, aad: bytes) -> bytes:
"""Déchiffre avec AES-256-GCM. Tag invalide -> TagInvalideError.""" """Déchiffre avec AES-256-GCM. Tag invalide -> TagInvalideError."""
_require_aesgcm() aesgcm = _require_aesgcm()
try: try:
return AESGCM(key).decrypt(_unb64(nonce_b64), _unb64(ciphertext_b64), aad) return aesgcm(key).decrypt(_unb64(nonce_b64), _unb64(ciphertext_b64), aad)
except InvalidTag as exc: except InvalidTag as exc:
raise TagInvalideError("Tag GCM invalide : données altérées ou mauvaise clé") from exc raise TagInvalideError("Tag GCM invalide : données altérées ou mauvaise clé") from exc
except (binascii.Error, ValueError) as exc: except (binascii.Error, ValueError) as exc:
@@ -177,7 +188,7 @@ def unwrap_dek(wrapped: dict, kek: bytes, coffre_id: str) -> bytes:
def encrypt_secret_value(dek: bytes, secret_id: str, version: int, valeur: str) -> dict: def encrypt_secret_value(dek: bytes, secret_id: str, version: int, valeur: str) -> dict:
"""Chiffre une valeur de secret (AAD = secret_id:version).""" """Chiffre une valeur de secret (AAD = secret_id:version)."""
aad = f"{secret_id}:{version}".encode("utf-8") aad = f"{secret_id}:{version}".encode()
return encrypt_with_key(dek, valeur.encode("utf-8"), aad) return encrypt_with_key(dek, valeur.encode("utf-8"), aad)
@@ -185,7 +196,7 @@ def decrypt_secret_value(
dek: bytes, secret_id: str, version: int, nonce_b64: str, ciphertext_b64: str dek: bytes, secret_id: str, version: int, nonce_b64: str, ciphertext_b64: str
) -> str: ) -> str:
"""Déchiffre une valeur de secret.""" """Déchiffre une valeur de secret."""
aad = f"{secret_id}:{version}".encode("utf-8") aad = f"{secret_id}:{version}".encode()
return decrypt_with_key(dek, nonce_b64, ciphertext_b64, aad).decode("utf-8") return decrypt_with_key(dek, nonce_b64, ciphertext_b64, aad).decode("utf-8")
+9 -9
View File
@@ -40,19 +40,19 @@ import re
import secrets as _secrets import secrets as _secrets
import threading import threading
import uuid import uuid
from datetime import datetime, timedelta, timezone from datetime import UTC, datetime, timedelta
from pathlib import Path from pathlib import Path
from typing import Any
from backend.coffre_crypto import ( from backend.coffre_crypto import (
KEKAbsenteError, decrypt_secret_value,
encrypt_secret_value,
generate_dek, generate_dek,
load_kek, load_kek,
parse_kek, parse_kek,
reencrypt_secret_value, reencrypt_secret_value,
unwrap_dek, unwrap_dek,
wrap_dek, wrap_dek,
encrypt_secret_value,
decrypt_secret_value,
) )
logger = logging.getLogger("obsigate.coffre_store") logger = logging.getLogger("obsigate.coffre_store")
@@ -123,7 +123,7 @@ def _valeurs_path(base: Path) -> Path:
def _now_iso() -> str: def _now_iso() -> str:
return datetime.now(timezone.utc).isoformat() return datetime.now(UTC).isoformat()
def _parse_date(value: str | None) -> datetime | None: def _parse_date(value: str | None) -> datetime | None:
@@ -140,7 +140,7 @@ def _parse_date(value: str | None) -> datetime | None:
except ValueError as exc: except ValueError as exc:
raise ValidationError(f"Date invalide : {value!r}") from exc raise ValidationError(f"Date invalide : {value!r}") from exc
if parsed.tzinfo is None: if parsed.tzinfo is None:
parsed = parsed.replace(tzinfo=timezone.utc) parsed = parsed.replace(tzinfo=UTC)
return parsed return parsed
@@ -149,7 +149,7 @@ def est_expire(expiration: str | None, maintenant: datetime | None = None) -> bo
parsed = _parse_date(expiration) parsed = _parse_date(expiration)
if parsed is None: if parsed is None:
return False return False
ref = maintenant or datetime.now(timezone.utc) ref = maintenant or datetime.now(UTC)
return parsed <= ref return parsed <= ref
@@ -307,7 +307,7 @@ def create_coffre(
data = _load_coffres(base) data = _load_coffres(base)
if any(c["slug"] == slug for c in data["coffres"]): if any(c["slug"] == slug for c in data["coffres"]):
raise CoffreExisteError(f"Coffre déjà existant : {slug}") raise CoffreExisteError(f"Coffre déjà existant : {slug}")
coffre = { coffre: dict[str, Any] = {
"id": str(uuid.uuid4()), "id": str(uuid.uuid4()),
"slug": slug, "slug": slug,
"nom": nom, "nom": nom,
@@ -766,7 +766,7 @@ def create_octroi(
): ):
raise IntrouvableError("Secret introuvable dans ce coffre") raise IntrouvableError("Secret introuvable dans ce coffre")
if expiration is None: if expiration is None:
expiration = (datetime.now(timezone.utc) + timedelta(days=DUREE_OCTROI_DEFAUT_JOURS)).isoformat() expiration = (datetime.now(UTC) + timedelta(days=DUREE_OCTROI_DEFAUT_JOURS)).isoformat()
else: else:
_parse_date(expiration) _parse_date(expiration)
with _lock: with _lock:
+2 -2
View File
@@ -359,7 +359,7 @@ def export_epub(vault_path: Path, file_path: Path) -> bytes:
rendered = _render_body(body, path.parent, vault_path, path) rendered = _render_body(body, path.parent, vault_path, path)
uid = f"obsigate-{_slugify(title)}-{int(datetime.datetime.now(tz=datetime.timezone.utc).timestamp())}" uid = f"obsigate-{_slugify(title)}-{int(datetime.datetime.now(tz=datetime.UTC).timestamp())}"
xhtml = f"""<?xml version="1.0" encoding="utf-8"?> xhtml = f"""<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE html> <!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr" lang="fr"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr" lang="fr">
@@ -383,7 +383,7 @@ def export_epub(vault_path: Path, file_path: Path) -> bytes:
<dc:title>{html_mod.escape(title)}</dc:title> <dc:title>{html_mod.escape(title)}</dc:title>
<dc:creator>{html_mod.escape(author)}</dc:creator> <dc:creator>{html_mod.escape(author)}</dc:creator>
<dc:language>fr</dc:language> <dc:language>fr</dc:language>
<meta property="dcterms:modified">{datetime.datetime.now(datetime.timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')}</meta> <meta property="dcterms:modified">{datetime.datetime.now(datetime.UTC).strftime('%Y-%m-%dT%H:%M:%SZ')}</meta>
</metadata> </metadata>
<manifest> <manifest>
<item id="ncx" href="toc.ncx" media-type="application/x-dtbncx+xml"/> <item id="ncx" href="toc.ncx" media-type="application/x-dtbncx+xml"/>
+1 -1
View File
@@ -355,7 +355,7 @@ def _export_footer(lang: str) -> str:
template = loc.get("guide105.export_footer", "") template = loc.get("guide105.export_footer", "")
if "%s" not in template and "{" not in template: if "%s" not in template and "{" not in template:
template = "ObsiGate {version}" template = "ObsiGate {version}"
today = datetime.datetime.now(tz=datetime.timezone.utc).date().isoformat() today = datetime.datetime.now(tz=datetime.UTC).date().isoformat()
return _collapse(template).format(version=_app_version(), date=today) return _collapse(template).format(version=_app_version(), date=today)
+4 -4
View File
@@ -5,7 +5,7 @@ import os
import re import re
import threading import threading
from collections.abc import Callable from collections.abc import Callable
from datetime import datetime, timezone from datetime import UTC, datetime
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@@ -581,7 +581,7 @@ def _scan_vault(
try: try:
relative = fpath.relative_to(vault_root) relative = fpath.relative_to(vault_root)
stat = fpath.stat() stat = fpath.stat()
modified = datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc).isoformat() modified = datetime.fromtimestamp(stat.st_mtime, tz=UTC).isoformat()
# #86 differential scan: reuse the previous entry when neither # #86 differential scan: reuse the previous entry when neither
# size nor mtime changed — skips the disk read + parse below. # size nor mtime changed — skips the disk read + parse below.
@@ -866,7 +866,7 @@ async def build_index(progress_callback=None) -> None:
# Record timestamp of full index rebuild # Record timestamp of full index rebuild
global _last_full_index_ts global _last_full_index_ts
_last_full_index_ts = datetime.now(timezone.utc).isoformat() _last_full_index_ts = datetime.now(UTC).isoformat()
# Build attachment index # Build attachment index
from backend.attachment_indexer import build_attachment_index from backend.attachment_indexer import build_attachment_index
@@ -1033,7 +1033,7 @@ def _index_single_file_sync(vault_name: str, vault_path: str, file_path: str, va
return None return None
stat = fpath.stat() stat = fpath.stat()
modified = datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc).isoformat() modified = datetime.fromtimestamp(stat.st_mtime, tz=UTC).isoformat()
# PDF handling — binary, must go through pdf_reader (same as _scan_vault) # PDF handling — binary, must go through pdf_reader (same as _scan_vault)
tags: list[str] = [] tags: list[str] = []
+4 -4
View File
@@ -25,7 +25,7 @@ import os
import smtplib import smtplib
import threading import threading
import uuid import uuid
from datetime import datetime, timezone from datetime import UTC, datetime
from email.message import EmailMessage from email.message import EmailMessage
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@@ -182,7 +182,7 @@ def create_channel(name: str, channel_type: str, config: dict[str, Any]) -> dict
"type": channel_type, "type": channel_type,
"enabled": True, "enabled": True,
"config": normalized, "config": normalized,
"created_at": datetime.now(timezone.utc).isoformat(), "created_at": datetime.now(UTC).isoformat(),
"last_sent_at": None, "last_sent_at": None,
"last_error": None, "last_error": None,
} }
@@ -287,7 +287,7 @@ def _send_webhook(url: str, title: str, message: str, trigger: str) -> None:
"event": trigger, "event": trigger,
"title": title, "title": title,
"message": message, "message": message,
"timestamp": datetime.now(timezone.utc).isoformat(), "timestamp": datetime.now(UTC).isoformat(),
"source": "obsigate-notify", "source": "obsigate-notify",
}, },
timeout=10.0, timeout=10.0,
@@ -345,6 +345,6 @@ def _mark_sent(channel_id: str, error: str | None) -> None:
channels = _read_channels() channels = _read_channels()
for channel in channels: for channel in channels:
if channel.get("id") == channel_id: if channel.get("id") == channel_id:
channel["last_sent_at"] = datetime.now(timezone.utc).isoformat() channel["last_sent_at"] = datetime.now(UTC).isoformat()
channel["last_error"] = error channel["last_error"] = error
_write_channels(channels) _write_channels(channels)
+2 -2
View File
@@ -6,7 +6,7 @@ import base64
import json import json
import logging import logging
import os import os
from datetime import datetime, timezone from datetime import UTC, datetime
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@@ -185,7 +185,7 @@ async def subscribe_push(
"keys": request.subscription.keys, "keys": request.subscription.keys,
"vault": request.vault, "vault": request.vault,
"username": username, "username": username,
"created_at": datetime.now(timezone.utc).isoformat() "created_at": datetime.now(UTC).isoformat()
} }
_push_subscriptions.append(subscription) _push_subscriptions.append(subscription)
+4
View File
@@ -75,3 +75,7 @@ filelock>=4.2.1
opentelemetry-api>=1.45.1 opentelemetry-api>=1.45.1
# Coffre Secrets (Phase 1a) : chiffrement par enveloppe AES-256-GCM. # Coffre Secrets (Phase 1a) : chiffrement par enveloppe AES-256-GCM.
cryptography>=44.0 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
+2 -2
View File
@@ -19,7 +19,7 @@ Adaptations strictement équivalentes :
import logging import logging
import os import os
import time import time
from datetime import datetime, timezone from datetime import UTC, datetime
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@@ -207,7 +207,7 @@ async def api_backups_list(
"file": rel_file, "file": rel_file,
"backup_file": fpath.name, "backup_file": fpath.name,
"timestamp": ts, "timestamp": ts,
"datetime": datetime.fromtimestamp(ts, tz=timezone.utc).isoformat(), "datetime": datetime.fromtimestamp(ts, tz=UTC).isoformat(),
"size": fsize, "size": fsize,
"full_path": str(fpath), "full_path": str(fpath),
}) })
+1 -1
View File
@@ -49,7 +49,7 @@ async def api_events(current_user=Depends(require_auth)):
try: try:
msg = await asyncio.wait_for(queue.get(), timeout=30.0) msg = await asyncio.wait_for(queue.get(), timeout=30.0)
yield f"event: {msg['event']}\ndata: {msg['data']}\n\n" yield f"event: {msg['event']}\ndata: {msg['data']}\n\n"
except asyncio.TimeoutError: except TimeoutError:
# Keepalive comment # Keepalive comment
yield ": keepalive\n\n" yield ": keepalive\n\n"
except asyncio.CancelledError: except asyncio.CancelledError:
+12 -12
View File
@@ -26,7 +26,7 @@ import logging
import os import os
import threading import threading
import uuid import uuid
from datetime import datetime, timedelta, timezone from datetime import UTC, datetime, timedelta
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@@ -118,7 +118,7 @@ def _validate_schedule(schedule: dict[str, Any]) -> dict[str, Any]:
try: try:
parsed = datetime.fromisoformat(at) parsed = datetime.fromisoformat(at)
if parsed.tzinfo is None: if parsed.tzinfo is None:
parsed = parsed.replace(tzinfo=timezone.utc) parsed = parsed.replace(tzinfo=UTC)
except ValueError: except ValueError:
raise ValueError("at doit être une date ISO-8601 (ex. 2026-10-05T08:30:00)") from None raise ValueError("at doit être une date ISO-8601 (ex. 2026-10-05T08:30:00)") from None
return {"kind": kind, "at": parsed.isoformat()} return {"kind": kind, "at": parsed.isoformat()}
@@ -138,7 +138,7 @@ def create_task(
"""Create a scheduled task. Raises ValueError on invalid action/schedule.""" """Create a scheduled task. Raises ValueError on invalid action/schedule."""
validated_action = _validate_action(action) validated_action = _validate_action(action)
validated_schedule = _validate_schedule(schedule) validated_schedule = _validate_schedule(schedule)
now = datetime.now(timezone.utc) now = datetime.now(UTC)
with _lock: with _lock:
tasks = _read_tasks() tasks = _read_tasks()
task = { task = {
@@ -200,9 +200,9 @@ def delete_task(task_id: str) -> bool:
def compute_next_run(task: dict[str, Any], now: datetime | None = None) -> datetime: def compute_next_run(task: dict[str, Any], now: datetime | None = None) -> datetime:
"""Compute the next due datetime for *task*.""" """Compute the next due datetime for *task*."""
now = now or datetime.now(timezone.utc) now = now or datetime.now(UTC)
if now.tzinfo is None: if now.tzinfo is None:
now = now.replace(tzinfo=timezone.utc) now = now.replace(tzinfo=UTC)
schedule = task.get("schedule", {}) schedule = task.get("schedule", {})
kind = schedule.get("kind") kind = schedule.get("kind")
last_run_at = task.get("last_run_at") last_run_at = task.get("last_run_at")
@@ -211,7 +211,7 @@ def compute_next_run(task: dict[str, Any], now: datetime | None = None) -> datet
try: try:
last = datetime.fromisoformat(str(last_run_at)) last = datetime.fromisoformat(str(last_run_at))
if last.tzinfo is None: if last.tzinfo is None:
last = last.replace(tzinfo=timezone.utc) last = last.replace(tzinfo=UTC)
except ValueError: except ValueError:
last = None last = None
if kind == "interval_hours": if kind == "interval_hours":
@@ -233,11 +233,11 @@ def compute_next_run(task: dict[str, Any], now: datetime | None = None) -> datet
try: try:
at = datetime.fromisoformat(str(schedule.get("at"))) at = datetime.fromisoformat(str(schedule.get("at")))
if at.tzinfo is None: if at.tzinfo is None:
at = at.replace(tzinfo=timezone.utc) at = at.replace(tzinfo=UTC)
except ValueError: except ValueError:
return now return now
if task.get("last_run_at"): if task.get("last_run_at"):
return datetime.max.replace(tzinfo=timezone.utc) # déjà exécutée return datetime.max.replace(tzinfo=UTC) # déjà exécutée
return at return at
return now + timedelta(hours=24) return now + timedelta(hours=24)
@@ -278,7 +278,7 @@ def run_task(task_id: str, *, manual: bool = False) -> dict[str, Any]:
return {"task_id": task_id, "skipped": True, "reason": "disabled"} return {"task_id": task_id, "skipped": True, "reason": "disabled"}
try: try:
result = _execute_action(task) result = _execute_action(task)
task["last_run_at"] = datetime.now(timezone.utc).isoformat() task["last_run_at"] = datetime.now(UTC).isoformat()
task["last_status"] = "ok" task["last_status"] = "ok"
task["last_error"] = None task["last_error"] = None
task["run_count"] = int(task.get("run_count", 0)) + 1 task["run_count"] = int(task.get("run_count", 0)) + 1
@@ -292,7 +292,7 @@ def run_task(task_id: str, *, manual: bool = False) -> dict[str, Any]:
broadcast("schedule_success", f"Tâche « {task['name']} » OK", "Exécution manuelle réussie.") broadcast("schedule_success", f"Tâche « {task['name']} » OK", "Exécution manuelle réussie.")
return {"task_id": task_id, "ok": True, "result": result} return {"task_id": task_id, "ok": True, "result": result}
except Exception as e: except Exception as e:
task["last_run_at"] = datetime.now(timezone.utc).isoformat() task["last_run_at"] = datetime.now(UTC).isoformat()
task["last_status"] = "error" task["last_status"] = "error"
task["last_error"] = str(e) task["last_error"] = str(e)
task["run_count"] = int(task.get("run_count", 0)) + 1 task["run_count"] = int(task.get("run_count", 0)) + 1
@@ -315,7 +315,7 @@ def run_task(task_id: str, *, manual: bool = False) -> dict[str, Any]:
def tick(now: datetime | None = None) -> list[dict[str, Any]]: def tick(now: datetime | None = None) -> list[dict[str, Any]]:
"""Run every due task. Returns per-task outcomes (empty when idle).""" """Run every due task. Returns per-task outcomes (empty when idle)."""
now = now or datetime.now(timezone.utc) now = now or datetime.now(UTC)
outcomes: list[dict[str, Any]] = [] outcomes: list[dict[str, Any]] = []
for task in _read_tasks(): for task in _read_tasks():
if not task.get("enabled", True): if not task.get("enabled", True):
@@ -323,7 +323,7 @@ def tick(now: datetime | None = None) -> list[dict[str, Any]]:
try: try:
next_run = datetime.fromisoformat(str(task.get("next_run_at") or "")) next_run = datetime.fromisoformat(str(task.get("next_run_at") or ""))
if next_run.tzinfo is None: if next_run.tzinfo is None:
next_run = next_run.replace(tzinfo=timezone.utc) next_run = next_run.replace(tzinfo=UTC)
except ValueError: except ValueError:
next_run = compute_next_run(task, now) next_run = compute_next_run(task, now)
if next_run <= now: if next_run <= now:
+3 -3
View File
@@ -14,7 +14,7 @@ import logging
import os import os
import shutil import shutil
import time import time
from datetime import datetime, timezone from datetime import UTC, datetime
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@@ -145,7 +145,7 @@ def list_backup_files(vault_name: str, relative_path: str) -> list[dict[str, Any
except (ValueError, TypeError): except (ValueError, TypeError):
continue continue
try: try:
dt = datetime.fromtimestamp(ts, tz=timezone.utc).isoformat() dt = datetime.fromtimestamp(ts, tz=UTC).isoformat()
except (OSError, OverflowError, ValueError) as ts_err: except (OSError, OverflowError, ValueError) as ts_err:
logger.warning(f"Skipping backup with invalid timestamp {ts}: {ts_err}") logger.warning(f"Skipping backup with invalid timestamp {ts}: {ts_err}")
continue continue
@@ -219,7 +219,7 @@ def diff_backup(
status=500, status=500,
details={"vault": vault_name, "path": path, "version": ts}, details={"vault": vault_name, "path": path, "version": ts},
) from e ) from e
dt = datetime.fromtimestamp(ts, tz=timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC") dt = datetime.fromtimestamp(ts, tz=UTC).strftime("%Y-%m-%d %H:%M:%S UTC")
return content, f"{path}@{dt}" return content, f"{path}@{dt}"
try: try:
+1 -1
View File
@@ -117,7 +117,7 @@ def list_recent(
for v_name, f in recent: for v_name, f in recent:
iso_modified = f.get("modified", "") iso_modified = f.get("modified", "")
try: try:
mtime_dt = datetime.fromisoformat(iso_modified.replace("Z", "+00:00")) mtime_dt = datetime.fromisoformat(iso_modified)
mtime_val = mtime_dt.timestamp() mtime_val = mtime_dt.timestamp()
except Exception: except Exception:
mtime_val = time.time() mtime_val = time.time()
+5 -4
View File
@@ -7,6 +7,7 @@ Single source of truth consumed by both the REST routes (``/api/vaults``,
from __future__ import annotations from __future__ import annotations
from datetime import UTC
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@@ -49,7 +50,7 @@ def get_vault_root(vault_name: str) -> Path:
def browse_directory(vault_name: str, path: str = "") -> dict[str, Any]: def browse_directory(vault_name: str, path: str = "") -> dict[str, Any]:
"""Return the direct children of a vault directory (directories first).""" """Return the direct children of a vault directory (directories first)."""
from datetime import datetime, timezone from datetime import datetime
from backend.indexer import SUPPORTED_EXTENSIONS from backend.indexer import SUPPORTED_EXTENSIONS
from backend.vault_settings import get_vault_setting from backend.vault_settings import get_vault_setting
@@ -98,7 +99,7 @@ def browse_directory(vault_name: str, path: str = "") -> dict[str, Any]:
"type": "file", "type": "file",
"size": stat.st_size, "size": stat.st_size,
"modified": stat.st_mtime, "modified": stat.st_mtime,
"modified_iso": datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc).isoformat(), "modified_iso": datetime.fromtimestamp(stat.st_mtime, tz=UTC).isoformat(),
"extension": entry.suffix.lower(), "extension": entry.suffix.lower(),
}) })
except PermissionError: except PermissionError:
@@ -156,7 +157,7 @@ def list_all_files(
ServiceError: ``not_found`` (404) if the directory does not exist, ServiceError: ``not_found`` (404) if the directory does not exist,
``permission_denied`` (403) if it cannot be read. ``permission_denied`` (403) if it cannot be read.
""" """
from datetime import datetime, timezone from datetime import datetime
from backend.indexer import IGNORED_DIRS from backend.indexer import IGNORED_DIRS
@@ -215,7 +216,7 @@ def list_all_files(
"vault": vault_name, "vault": vault_name,
"size": stat.st_size, "size": stat.st_size,
"modified": stat.st_mtime, "modified": stat.st_mtime,
"modified_iso": datetime.fromtimestamp(stat.st_mtime, tz=timezone.utc).isoformat(), "modified_iso": datetime.fromtimestamp(stat.st_mtime, tz=UTC).isoformat(),
"extension": ext.lstrip(".") if ext else "", "extension": ext.lstrip(".") if ext else "",
"tags": _indexed_tags(vault_name, rel_path), "tags": _indexed_tags(vault_name, rel_path),
} }
+5 -5
View File
@@ -11,7 +11,7 @@ import json
import logging import logging
import secrets import secrets
import threading import threading
from datetime import datetime, timedelta, timezone from datetime import UTC, datetime, timedelta
from pathlib import Path from pathlib import Path
logger = logging.getLogger("obsigate.share") logger = logging.getLogger("obsigate.share")
@@ -58,7 +58,7 @@ def create_share(
expires_at = None expires_at = None
if expires_in_hours: if expires_in_hours:
expires_at = (datetime.now(timezone.utc) + timedelta(hours=expires_in_hours)).isoformat() expires_at = (datetime.now(UTC) + timedelta(hours=expires_in_hours)).isoformat()
share = { share = {
"id": token, "id": token,
@@ -66,7 +66,7 @@ def create_share(
"vault": vault, "vault": vault,
"path": path, "path": path,
"created_by": created_by, "created_by": created_by,
"created_at": datetime.now(timezone.utc).isoformat(), "created_at": datetime.now(UTC).isoformat(),
"expires_at": expires_at, "expires_at": expires_at,
"access_count": 0, "access_count": 0,
"last_accessed": None, "last_accessed": None,
@@ -86,7 +86,7 @@ def get_share_by_token(token: str) -> dict | None:
return None return None
if share.get("expires_at"): if share.get("expires_at"):
expires = datetime.fromisoformat(share["expires_at"]) expires = datetime.fromisoformat(share["expires_at"])
if datetime.now(timezone.utc) > expires: if datetime.now(UTC) > expires:
return None return None
return share return share
@@ -98,7 +98,7 @@ def record_access(token: str):
share = data["shares"].get(token) share = data["shares"].get(token)
if share: if share:
share["access_count"] = share.get("access_count", 0) + 1 share["access_count"] = share.get("access_count", 0) + 1
share["last_accessed"] = datetime.now(timezone.utc).isoformat() share["last_accessed"] = datetime.now(UTC).isoformat()
_write(data) _write(data)
+2 -2
View File
@@ -7,7 +7,7 @@ Reuses the application audit log (``data/audit.log``, JSON lines) and adds an
from __future__ import annotations from __future__ import annotations
from datetime import datetime, timezone from datetime import UTC, datetime
from typing import Any from typing import Any
from backend.audit import _write_entry from backend.audit import _write_entry
@@ -41,7 +41,7 @@ def log_tool_call(
) -> None: ) -> None:
"""Append an ``ai_tool_call`` entry to the audit log.""" """Append an ``ai_tool_call`` entry to the audit log."""
_write_entry({ _write_entry({
"timestamp": datetime.now(timezone.utc).isoformat(), "timestamp": datetime.now(UTC).isoformat(),
"action": "ai_tool_call", "action": "ai_tool_call",
"username": username, "username": username,
"ip": ip or "unknown", "ip": ip or "unknown",
+1 -1
View File
@@ -244,7 +244,7 @@ class VaultWatcher:
key = event.get('dest') or event['src'] key = event.get('dest') or event['src']
pending[key] = event pending[key] = event
except asyncio.TimeoutError: except TimeoutError:
if pending: if pending:
events_to_process = list(pending.values()) events_to_process = list(pending.values())
pending.clear() pending.clear()
+4 -4
View File
@@ -31,7 +31,7 @@ import os
import socket import socket
import threading import threading
import uuid import uuid
from datetime import datetime, timezone from datetime import UTC, datetime
from pathlib import Path from pathlib import Path
from urllib.parse import urlparse from urllib.parse import urlparse
@@ -275,7 +275,7 @@ def create_webhook(
"url": url, "url": url,
"events": [e for e in events if e in VALID_EVENTS], "events": [e for e in events if e in VALID_EVENTS],
"enabled": True, "enabled": True,
"created_at": datetime.now(timezone.utc).isoformat(), "created_at": datetime.now(UTC).isoformat(),
"last_fired_at": None, "last_fired_at": None,
# #198 : filtres optionnels (None = pas de filtre, reçoit tout). # #198 : filtres optionnels (None = pas de filtre, reçoit tout).
"vault": _normalize_vault_filter(vault), "vault": _normalize_vault_filter(vault),
@@ -344,7 +344,7 @@ async def dispatch_webhooks(event_type: str, data: dict):
payload = { payload = {
"event": event_type, "event": event_type,
"timestamp": datetime.now(timezone.utc).isoformat(), "timestamp": datetime.now(UTC).isoformat(),
"data": data, "data": data,
} }
body = json.dumps(payload, default=str) body = json.dumps(payload, default=str)
@@ -369,7 +369,7 @@ async def dispatch_webhooks(event_type: str, data: dict):
logger.debug(f"Webhook '{wh['name']}' OK ({resp.status})") logger.debug(f"Webhook '{wh['name']}' OK ({resp.status})")
else: else:
logger.warning(f"Webhook '{wh['name']}' failed ({resp.status})") logger.warning(f"Webhook '{wh['name']}' failed ({resp.status})")
update_webhook(wh["id"], {"last_fired_at": datetime.now(timezone.utc).isoformat()}) update_webhook(wh["id"], {"last_fired_at": datetime.now(UTC).isoformat()})
except Exception as e: except Exception as e:
logger.warning(f"Webhook '{wh['name']}' error: {e}") logger.warning(f"Webhook '{wh['name']}' error: {e}")
+1 -1
View File
@@ -2626,7 +2626,7 @@ dependencies = [
[[package]] [[package]]
name = "obsigate-desktop" name = "obsigate-desktop"
version = "2.79.0" version = "2.80.2"
dependencies = [ dependencies = [
"chrono", "chrono",
"env_logger", "env_logger",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "obsigate-desktop" name = "obsigate-desktop"
version = "2.79.0" version = "2.80.2"
description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian" description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian"
authors = ["Bruno Charest"] authors = ["Bruno Charest"]
edition = "2021" edition = "2021"
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json", "$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json",
"productName": "ObsiGate", "productName": "ObsiGate",
"version": "2.79.0", "version": "2.80.2",
"identifier": "com.obsigate.desktop", "identifier": "com.obsigate.desktop",
"build": { "build": {
"frontendDist": "../frontend", "frontendDist": "../frontend",
+1 -1
View File
@@ -1,6 +1,6 @@
# ObsiGate — Roadmap # ObsiGate — Roadmap
> **Version :** 2.79.0 | **Dernière mise à jour :** 2026-10-26 > **Version :** 2.80.2 | **Dernière mise à jour :** 2026-10-27
> **Ce fichier ne contient que le travail à venir** (🔵 En cours + ⚪ Backlog) et un index compact > **Ce fichier ne contient que le travail à venir** (🔵 En cours + ⚪ Backlog) et un index compact
> vers les fonctionnalités livrées. > vers les fonctionnalités livrées.
> - **Méthode de livraison à appliquer pour toute tâche : [DELIVERY_WORKFLOW.md](./DELIVERY_WORKFLOW.md)** > - **Méthode de livraison à appliquer pour toute tâche : [DELIVERY_WORKFLOW.md](./DELIVERY_WORKFLOW.md)**
+4 -4
View File
@@ -19,8 +19,8 @@
var mode = localStorage.getItem("obsigate-theme-mode") || "dark"; var mode = localStorage.getItem("obsigate-theme-mode") || "dark";
root.setAttribute("data-theme", mode); root.setAttribute("data-theme", mode);
var light = mode === "light" || mode === "sepia"; var light = mode === "light" || mode === "sepia";
root.style.setProperty("--bg-primary", light ? "#ffffff" : "#0f1117"); root.style.setProperty("--bg-primary", light ? "#fbfbfa" : "#17181a");
root.style.setProperty("--text-primary", light ? "#1a1a1a" : "#e6edf3"); root.style.setProperty("--text-primary", light ? "#242624" : "#eceeeb");
} catch (e) { root.setAttribute("data-theme", "dark"); } } catch (e) { root.setAttribute("data-theme", "dark"); }
})(); })();
</script> </script>
@@ -41,8 +41,8 @@
min-height: 100vh; min-height: 100vh;
overflow: auto; /* override SPA style.css: body{overflow:hidden} */ overflow: auto; /* override SPA style.css: body{overflow:hidden} */
height: auto; /* override SPA style.css: body{height:100vh} */ height: auto; /* override SPA style.css: body{height:100vh} */
background: var(--bg-primary, #0f1117); background: var(--bg-primary, #17181a);
color: var(--text-primary, #e6edf3); color: var(--text-primary, #eceeeb);
font-family: "Segoe UI", system-ui, -apple-system, sans-serif; font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
} }
+4 -4
View File
@@ -10,7 +10,7 @@
name="description" name="description"
content="Porte d'entrée web pour vos vaults Obsidian - Accédez, naviguez et recherchez dans toutes vos notes" content="Porte d'entrée web pour vos vaults Obsidian - Accédez, naviguez et recherchez dans toutes vos notes"
/> />
<meta name="theme-color" content="#2563eb" /> <meta name="theme-color" content="#7c5cd6" />
<meta name="mobile-web-app-capable" content="yes" /> <meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" /> <meta name="apple-mobile-web-app-capable" content="yes" />
<meta <meta
@@ -54,14 +54,14 @@
#boot-splash { #boot-splash {
position: fixed; inset: 0; z-index: 2147483647; position: fixed; inset: 0; z-index: 2147483647;
display: flex; align-items: center; justify-content: center; display: flex; align-items: center; justify-content: center;
background: radial-gradient(1200px 600px at 50% -10%, #1b2030 0%, #0e1117 60%); background: radial-gradient(1200px 600px at 50% -10%, #26272b 0%, #17181a 60%);
color: #e6e8ee; font-family: "Segoe UI", system-ui, -apple-system, sans-serif; color: #eceeeb; font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
} }
body.app-booted #boot-splash { display: none; } body.app-booted #boot-splash { display: none; }
.bs-card { display: flex; flex-direction: column; align-items: center; gap: 14px; } .bs-card { display: flex; flex-direction: column; align-items: center; gap: 14px; }
.bs-logo { .bs-logo {
width: 72px; height: 72px; display: flex; align-items: center; justify-content: center; width: 72px; height: 72px; display: flex; align-items: center; justify-content: center;
font-size: 38px; background: #171c26; border: 1px solid #2a3242; border-radius: 18px; font-size: 38px; background: #1f2023; border: 1px solid #33353a; border-radius: 18px;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.45); box-shadow: 0 8px 30px rgba(0, 0, 0, 0.45);
} }
.bs-title { font-size: 22px; font-weight: 600; letter-spacing: 0.3px; } .bs-title { font-size: 22px; font-weight: 600; letter-spacing: 0.3px; }
+3
View File
@@ -292,6 +292,9 @@ function valeurField(initialType) {
else if (input) input.type = 'password'; else if (input) input.type = 'password';
}); });
eyeBtn.title = t('coffres.show_before_save'); eyeBtn.title = t('coffres.show_before_save');
// L'oeil doit etre enfant de wrap AVANT le premier build : insertBefore(input, eyeBtn)
// leve sinon NotFoundError et la modale ne s'ouvre jamais (bug bouton Nouveau secret).
wrap.appendChild(eyeBtn);
build(initialType); build(initialType);
return { return {
wrap, wrap,
+67 -2
View File
@@ -1,5 +1,5 @@
/* ObsiGate — Theme System /* ObsiGate — Theme System
* 15 themes x 2 modes (dark/light) * 16 themes x 2 modes (dark/light)
* Persisted in localStorage as 'obsigate-theme' * Persisted in localStorage as 'obsigate-theme'
* Usage: import { initThemes, applyTheme, getCurrentTheme, THEMES } from './themes.js' * Usage: import { initThemes, applyTheme, getCurrentTheme, THEMES } from './themes.js'
*/ */
@@ -45,6 +45,71 @@ var THEMES = {
} }
}, },
// ── Thème Spark ──────────────────────────────────────────────────────
// Palette source : console Spark (agent.dracodev.net), app/console/console.css
// (variables --p-*). Mapping Spark -> ObsiGate, identique en structure pour
// dark et light ; seules les valeurs changent (sauf accent/ok/warn/err).
// Fidèle (valeur Spark directe) :
// fond -> --bg-primary, --surface2 ; surface -> --surface ;
// chip -> --bg-secondary, --tag-bg ; chip-survol -> --bg-hover ;
// bord -> --border ; chip-bord -> --border-md, --scrollbar, --resize-handle ;
// texte -> --text-primary, --text ; doux -> --text-secondary, --text-2 ;
// faint -> --text-muted, --text-3 ; champ -> --search-bg ;
// code -> --code-bg ; accent -> --accent, --accent-card, --purple, --ai ;
// lien -> --accent-text (variante lisible de l'accent sur fond teinté) ;
// ok -> --accent-green, --success, --green ;
// danger -> --danger ; danger-fond -> --danger-bg ;
// pastilles ok (fond) -> --success-bg, --green-bg ;
// pastilles ok (texte) -> --ok ; ambre -> --warn ; rouge -> --err.
// Approximations documentées (ObsiGate a plus de variables que Spark,
// ce ne sont PAS des valeurs Spark) :
// --bg-sidebar <- fond (Spark n'a pas de token sidebar dédié) ;
// --accent-bg <- agent (surface teintée de Spark, pas de teinte lavande) ;
// --surface3 <- agent ; --purple-bg <- appro-fond (fond teinté le plus
// proche, la palette Spark n'a pas de teinte lavande) ;
// --overlay-bg <- convention ObsiGate (rgba noir 0.3 clair / 0.5 sombre,
// aucun token voile dans la palette Spark).
// Non reportés (aucune variable ObsiGate correspondante dans les 39) :
// bulle utilisateur/envoi #2f6fed, titres approbation #9a6a1c / #e8bc7a.
// Une seule entrée 'spark' : dark et light sont ses deux modes, jamais
// deux thèmes séparés. Modes high-contrast/sepia générés comme pour les
// autres thèmes (à partir de dark/light).
'spark': {
name: 'Spark', desc: 'Palette de la console Spark',
modes: {
dark: {
'--bg-primary': '#17181a', '--bg-secondary': '#26272b', '--bg-sidebar': '#17181a',
'--bg-hover': '#2e3036', '--border': '#33353a', '--text-primary': '#eceeeb',
'--text-secondary': '#a6aba0', '--text-muted': '#83887e', '--accent': '#7c5cd6',
'--accent-green': '#2ea36b', '--tag-bg': '#26272b', '--tag-text': '#7c5cd6',
'--code-bg': '#2c2e33', '--search-bg': '#1f2023', '--scrollbar': '#3a3c41',
'--resize-handle': '#3a3c41', '--overlay-bg': 'rgba(0,0,0,0.5)', '--danger': '#e8a08d',
'--danger-bg': '#40241f', '--success': '#2ea36b', '--success-bg': '#173d29',
'--accent-card': '#7c5cd6', '--accent-bg': '#26272b', '--accent-text': '#b39df5',
'--green': '#2ea36b', '--green-bg': '#173d29', '--purple': '#7c5cd6',
'--purple-bg': '#2a2118', '--surface': '#1f2023', '--surface2': '#17181a',
'--surface3': '#26272b', '--border-md': '#3a3c41', '--text': '#eceeeb',
'--text-2': '#a6aba0', '--text-3': '#83887e', '--ok': '#7fd6a8',
'--warn': '#e8a13c', '--ai': '#7c5cd6', '--err': '#cf4b32'
},
light: {
'--bg-primary': '#fbfbfa', '--bg-secondary': '#f4f5f1', '--bg-sidebar': '#fbfbfa',
'--bg-hover': '#e9ebe4', '--border': '#e4e5e0', '--text-primary': '#242624',
'--text-secondary': '#6c7268', '--text-muted': '#8a8f84', '--accent': '#7c5cd6',
'--accent-green': '#2ea36b', '--tag-bg': '#f4f5f1', '--tag-text': '#7c5cd6',
'--code-bg': '#e8eae3', '--search-bg': '#ffffff', '--scrollbar': '#d6d8d1',
'--resize-handle': '#d6d8d1', '--overlay-bg': 'rgba(0,0,0,0.3)', '--danger': '#a03a28',
'--danger-bg': '#f6e3de', '--success': '#2ea36b', '--success-bg': '#dff2e7',
'--accent-card': '#7c5cd6', '--accent-bg': '#eef0ea', '--accent-text': '#5b3df0',
'--green': '#2ea36b', '--green-bg': '#dff2e7', '--purple': '#7c5cd6',
'--purple-bg': '#fdf6ec', '--surface': '#ffffff', '--surface2': '#fbfbfa',
'--surface3': '#eef0ea', '--border-md': '#d6d8d1', '--text': '#242624',
'--text-2': '#6c7268', '--text-3': '#8a8f84', '--ok': '#1d7a4c',
'--warn': '#e8a13c', '--ai': '#7c5cd6', '--err': '#cf4b32'
}
}
},
'github': { 'github': {
name: 'GitHub', desc: 'Contraste doux', name: 'GitHub', desc: 'Contraste doux',
modes: { modes: {
@@ -523,7 +588,7 @@ var THEMES = {
}; };
// ── Theme Engine ───────────────────────────────────────────────────── // ── Theme Engine ─────────────────────────────────────────────────────
var _currentTheme = 'defaut-obsigate'; var _currentTheme = 'spark';
var _currentMode = 'dark'; var _currentMode = 'dark';
var AVAILABLE_MODES = ['dark', 'light', 'high-contrast', 'sepia']; var AVAILABLE_MODES = ['dark', 'light', 'high-contrast', 'sepia'];
+42 -42
View File
@@ -11,58 +11,58 @@
/* ── Editor save dot state colors ── */ /* ── Editor save dot state colors ── */
:root { :root {
--ok: #34d399; --ok: #7fd6a8;
--warn: #fbbf24; --warn: #e8a13c;
--ai: #8b5cf6; --ai: #7c5cd6;
--err: #f87171; --err: #cf4b32;
} }
/* ===== THEME — DARK (fallback default, no data-theme) ===== */ /* ===== THEME — DARK (fallback default, no data-theme) ===== */
:root { :root {
--bg-primary: #0f1117; --bg-primary: #17181a;
--bg-secondary: #161b22; --bg-secondary: #26272b;
--bg-sidebar: #13161d; --bg-sidebar: #17181a;
--bg-hover: #1f2430; --bg-hover: #2e3036;
--border: #21262d; --border: #33353a;
--text-primary: #e6edf3; --text-primary: #eceeeb;
--text-secondary: #8b949e; --text-secondary: #a6aba0;
--text-muted: #484f58; --text-muted: #83887e;
--accent: #58a6ff; --accent: #7c5cd6;
--accent-green: #3fb950; --accent-green: #2ea36b;
--tag-bg: #1f2a3a; --tag-bg: #26272b;
--tag-text: #79c0ff; --tag-text: #7c5cd6;
--code-bg: #161b22; --code-bg: #2c2e33;
--search-bg: #21262d; --search-bg: #1f2023;
--scrollbar: #30363d; --scrollbar: #3a3c41;
--resize-handle: #30363d; --resize-handle: #3a3c41;
--overlay-bg: rgba(0, 0, 0, 0.5); --overlay-bg: rgba(0, 0, 0, 0.5);
--danger: #ff7b72; --danger: #e8a08d;
--danger-bg: #3d1a18; --danger-bg: #40241f;
--success: #3fb950; --success: #2ea36b;
--success-bg: #1a3d1f; --success-bg: #173d29;
--accent-card: #58a6ff; --accent-card: #7c5cd6;
--accent-bg: #1f2a3a; --accent-bg: #26272b;
--accent-text: #79c0ff; --accent-text: #b39df5;
--green: #3fb950; --green: #2ea36b;
--green-bg: #1a3d1f; --green-bg: #173d29;
--purple: #a371f7; --purple: #7c5cd6;
--purple-bg: #2d1b4e; --purple-bg: #2a2118;
--surface: #161b22; --surface: #1f2023;
--surface2: #0f1117; --surface2: #17181a;
--surface3: #0d0f14; --surface3: #26272b;
--border-md: #30363d; --border-md: #3a3c41;
--text: #e6edf3; --text: #eceeeb;
--text-2: #8b949e; --text-2: #a6aba0;
--text-3: #484f58; --text-3: #83887e;
--mono: "JetBrains Mono", monospace; --mono: "JetBrains Mono", monospace;
--radius: 6px; --radius: 6px;
--radius-lg: 10px; --radius-lg: 10px;
/* #154-A1 — data-grid tokens: the sheet reads slightly lighter than the /* #154-A1 — data-grid tokens: the sheet reads slightly lighter than the
chrome (toolbar/panels), headers are clearly distinct from cells. */ chrome (toolbar/panels), headers are clearly distinct from cells. */
--grid-bg: #161b22; --grid-bg: #1f2023;
--grid-header-bg: #1f2430; --grid-header-bg: #2e3036;
--grid-header-text: #e6edf3; --grid-header-text: #eceeeb;
--grid-border: #3d444d; --grid-border: #33353a;
--grid-zebra: rgba(255, 255, 255, 0.025); --grid-zebra: rgba(255, 255, 255, 0.025);
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "obsigate", "name": "obsigate",
"version": "2.79.0", "version": "2.80.2",
"description": "**Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive.", "description": "**Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive.",
"main": "patch.js", "main": "patch.js",
"directories": { "directories": {