Compare commits
53
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3b507b8d9 | ||
|
|
c01c7e862d | ||
|
|
99cd765997 | ||
|
|
5266506b56 | ||
|
|
682a5030d3 | ||
|
|
fc3d847d85 | ||
|
|
82756030c6 | ||
|
|
0352451ead | ||
|
|
593cf492c3 | ||
|
|
0b8ad8b0f3 | ||
|
|
111d927ece | ||
|
|
2dae023ee3 | ||
|
|
b12122f78a | ||
|
|
0c7b3162c8 | ||
|
|
d4d050297b | ||
|
|
8d768bf9d5 | ||
|
|
47ac8bf6c0 | ||
|
|
01572736c0 | ||
|
|
b6e5f01748 | ||
|
|
90da0ac990 | ||
|
|
bc26d1506b | ||
|
|
769e3ca559 | ||
|
|
b5d789f02a | ||
|
|
c8dce25e0d | ||
|
|
b322e2d24a | ||
|
|
414ec69c0a | ||
|
|
f5ca656c04 | ||
|
|
e158232aba | ||
|
|
632871ac24 | ||
|
|
7af7b2c8c2 | ||
|
|
97132770e4 | ||
|
|
683ad3ef94 | ||
|
|
604383f51c | ||
|
|
bdeb2f00b3 | ||
|
|
8688875fae | ||
|
|
a42271c7de | ||
|
|
cf4685eaf1 | ||
|
|
5023fb6774 | ||
|
|
04eb70cbe4 | ||
|
|
4dc010f54d | ||
|
|
f7e36bc44e | ||
|
|
a9ac1c06a7 | ||
|
|
9bf6e7492f | ||
|
|
14070cef43 | ||
|
|
6b1abbb50e | ||
|
|
46c641ebc5 | ||
|
|
2d036f5126 | ||
|
|
2c69e357b8 | ||
|
|
ba81f22d40 | ||
|
|
070add4e7b | ||
|
|
847b3225b2 | ||
|
|
3ae5c7efac | ||
|
|
4966c27548 |
+25
-1
@@ -6,7 +6,7 @@ Format basé sur [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/),
|
|||||||
et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
> **En cours de développement** : les changements à venir sont listés dans la section
|
> **En cours de développement** : les changements à venir sont listés dans la section
|
||||||
> [Unreleased](#unreleased). La dernière version livrée est **2.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
@@ -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.
|
||||||
|
|
||||||
[]()
|
[]()
|
||||||
[](https://opensource.org/licenses/MIT)
|
[](https://opensource.org/licenses/MIT)
|
||||||
[](https://www.docker.com/)
|
[](https://www.docker.com/)
|
||||||
[](https://www.python.org/)
|
[](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*
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
[]()
|
[]()
|
||||||
[](https://opensource.org/licenses/MIT)
|
[](https://opensource.org/licenses/MIT)
|
||||||
[](https://www.docker.com/)
|
[](https://www.docker.com/)
|
||||||
[](https://www.python.org/)
|
[](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*
|
||||||
|
|||||||
+2
-2
@@ -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
@@ -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",
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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"/>
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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}")
|
||||||
|
|
||||||
|
|||||||
Generated
+1
-1
@@ -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
@@ -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,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
@@ -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
@@ -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
@@ -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; }
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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": {
|
||||||
|
|||||||
Reference in New Issue
Block a user