Merge pull request '[fix] Version 2.80.1 synchronisee + corrections ruff (60 erreurs CI)' (#26) from fix/version-2.80.1-ruff into main
This commit was merged in pull request #26.
This commit is contained in:
+14
-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.1**.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -14,6 +14,19 @@ et [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## [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.1).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Projet : ObsiGate | Version : 2.79.0 | Dernière mise à jour : Septembre 2026*
|
*Projet : ObsiGate | Version : 2.80.1 | 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.1).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Project: ObsiGate | Version: 2.79.0 | Last updated: September 2026*
|
*Project: ObsiGate | Version: 2.80.1 | 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
|
||||||
|
|||||||
@@ -177,7 +177,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 +185,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,18 @@ 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 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 +122,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 +139,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 +148,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
|
||||||
|
|
||||||
|
|
||||||
@@ -766,7 +765,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)
|
||||||
|
|||||||
@@ -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.1"
|
||||||
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.1"
|
||||||
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.1",
|
||||||
"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.1 | **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)**
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "obsigate",
|
"name": "obsigate",
|
||||||
"version": "2.79.0",
|
"version": "2.80.1",
|
||||||
"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