From 2c69e357b8af6877d746f1103e74a520d57b5396 Mon Sep 17 00:00:00 2001 From: bruno Date: Tue, 27 Oct 2026 22:56:25 -0400 Subject: [PATCH 01/31] [fix] Bump VERSION 2.79.0 -> 2.80.1 (synchronisation bump_version.py) : VERSION --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 45cc88f..a2f5270 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.79.0 +2.80.1 -- 2.54.0 From 2d036f512661a1aa7447baa97e849b9ff492fc0c Mon Sep 17 00:00:00 2001 From: bruno Date: Tue, 27 Oct 2026 22:56:27 -0400 Subject: [PATCH 02/31] [fix] Bump VERSION 2.79.0 -> 2.80.1 (synchronisation bump_version.py) : package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 522c8dc..19f92dd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "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.", "main": "patch.js", "directories": { -- 2.54.0 From 46c641ebc55a964a93e0fc8a2dceb99fe9b6df09 Mon Sep 17 00:00:00 2001 From: bruno Date: Tue, 27 Oct 2026 22:56:30 -0400 Subject: [PATCH 03/31] [fix] Bump VERSION 2.79.0 -> 2.80.1 (synchronisation bump_version.py) : desktop/tauri.conf.json --- desktop/tauri.conf.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desktop/tauri.conf.json b/desktop/tauri.conf.json index 03aaaa2..b90a424 100644 --- a/desktop/tauri.conf.json +++ b/desktop/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://raw.githubusercontent.com/nicedoc/obsigate/main/desktop/tauri.conf.schema.json", "productName": "ObsiGate", - "version": "2.79.0", + "version": "2.80.1", "identifier": "com.obsigate.desktop", "build": { "frontendDist": "../frontend", -- 2.54.0 From 6b1abbb50e829f5970529e0da88ec9c1046559f9 Mon Sep 17 00:00:00 2001 From: bruno Date: Tue, 27 Oct 2026 22:56:33 -0400 Subject: [PATCH 04/31] [fix] Bump VERSION 2.79.0 -> 2.80.1 (synchronisation bump_version.py) : desktop/Cargo.toml --- desktop/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml index 8d7dfe4..d54b571 100644 --- a/desktop/Cargo.toml +++ b/desktop/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "obsigate-desktop" -version = "2.79.0" +version = "2.80.1" description = "ObsiGate Desktop — Porte d'entrée native pour vos vaults Obsidian" authors = ["Bruno Charest"] edition = "2021" -- 2.54.0 From 14070cef43964d9976b0847ac760e3f4a0fd847f Mon Sep 17 00:00:00 2001 From: bruno Date: Tue, 27 Oct 2026 22:56:37 -0400 Subject: [PATCH 05/31] [fix] Bump VERSION 2.79.0 -> 2.80.1 (synchronisation bump_version.py) : desktop/Cargo.lock --- desktop/Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desktop/Cargo.lock b/desktop/Cargo.lock index 8721dc7..473c67e 100644 --- a/desktop/Cargo.lock +++ b/desktop/Cargo.lock @@ -2626,7 +2626,7 @@ dependencies = [ [[package]] name = "obsigate-desktop" -version = "2.79.0" +version = "2.80.1" dependencies = [ "chrono", "env_logger", -- 2.54.0 From 9bf6e7492f170c3d05532c8deb7a4ea20fce2238 Mon Sep 17 00:00:00 2001 From: bruno Date: Tue, 27 Oct 2026 22:56:43 -0400 Subject: [PATCH 06/31] [fix] Bump VERSION 2.79.0 -> 2.80.1 (synchronisation bump_version.py) : README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0fb677e..6097e5d 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ **Ultra-light web gateway for your Obsidian vaults** — Access, browse, and search all your Obsidian notes from any device via a modern, responsive web interface. -[![Version](https://img.shields.io/badge/Version-2.79.0-blue.svg)]() +[![Version](https://img.shields.io/badge/Version-2.80.1-blue.svg)]() [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Docker](https://img.shields.io/badge/Docker-Ready-blue.svg)](https://www.docker.com/) [![Python](https://img.shields.io/badge/Python-3.11+-green.svg)](https://www.python.org/) @@ -1152,8 +1152,8 @@ This project is licensed under the **MIT License** - see the [LICENSE](LICENSE) ## 📝 Changelog -See [CHANGELOG.md](./CHANGELOG.md) for the complete version history (v1.0.0 → v2.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.54.0 From a9ac1c06a753d989068c80df5c80d0b8c6c0179b Mon Sep 17 00:00:00 2001 From: bruno Date: Tue, 27 Oct 2026 22:56:48 -0400 Subject: [PATCH 07/31] [fix] Bump VERSION 2.79.0 -> 2.80.1 (synchronisation bump_version.py) : README.fr.md --- README.fr.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.fr.md b/README.fr.md index 2d80934..8ce92e1 100644 --- a/README.fr.md +++ b/README.fr.md @@ -4,7 +4,7 @@ **Porte d'entrée web ultra-léger pour vos vaults Obsidian** — Accédez, naviguez et recherchez dans toutes vos notes Obsidian depuis n'importe quel appareil via une interface web moderne et responsive. -[![Version](https://img.shields.io/badge/Version-2.79.0-blue.svg)]() +[![Version](https://img.shields.io/badge/Version-2.80.1-blue.svg)]() [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Docker](https://img.shields.io/badge/Docker-Ready-blue.svg)](https://www.docker.com/) [![Python](https://img.shields.io/badge/Python-3.11+-green.svg)](https://www.python.org/) @@ -977,8 +977,8 @@ Ce projet est sous licence **MIT** — voir le fichier [LICENSE](LICENSE) pour l ## 📝 Changelog -Consultez le [CHANGELOG.md](./CHANGELOG.md) pour l'historique complet de toutes les versions (v1.0.0 → v2.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.54.0 From f7e36bc44e007c28390bf1cb609a0ff68a7d4d7b Mon Sep 17 00:00:00 2001 From: bruno Date: Tue, 27 Oct 2026 22:56:53 -0400 Subject: [PATCH 08/31] [fix] Bump VERSION 2.79.0 -> 2.80.1 (synchronisation bump_version.py) : docs/ROADMAP.md --- docs/ROADMAP.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index e37c565..d67128d 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -1,6 +1,6 @@ # 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 > vers les fonctionnalités livrées. > - **Méthode de livraison à appliquer pour toute tâche : [DELIVERY_WORKFLOW.md](./DELIVERY_WORKFLOW.md)** -- 2.54.0 From 4dc010f54dfce4891161733a94f406d51001b9f5 Mon Sep 17 00:00:00 2001 From: bruno Date: Tue, 27 Oct 2026 22:56:57 -0400 Subject: [PATCH 09/31] [fix] Bump VERSION 2.79.0 -> 2.80.1 (synchronisation bump_version.py) : CHANGELOG.md --- CHANGELOG.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index acba604..d6a7117 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ Format basé sur [Keep a Changelog](https://keepachangelog.com/fr/1.1.0/), et [Semantic Versioning](https://semver.org/spec/v2.0.0.html). > **En cours de développement** : les changements à venir sont listés dans la section -> [Unreleased](#unreleased). La dernière version livrée est **2.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 ### Ajouté -- 2.54.0 From 04eb70cbe4709a6a23ff816e58d969ac47734625 Mon Sep 17 00:00:00 2001 From: bruno Date: Tue, 27 Oct 2026 22:57:01 -0400 Subject: [PATCH 10/31] [fix] Corrections ruff CI (UP017/UP041/UP012/I001/F401/FURB162) : backend/admin.py --- backend/admin.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/admin.py b/backend/admin.py index d0661cf..51d063f 100644 --- a/backend/admin.py +++ b/backend/admin.py @@ -10,7 +10,7 @@ import logging import os import shutil import time -from datetime import datetime, timezone +from datetime import UTC, datetime from pathlib import Path from typing import Any @@ -244,7 +244,7 @@ def _build_stats_payload() -> dict: "disk_used_gb": disk_used_gb, "disk_total_gb": disk_total_gb, "uptime_seconds": int(_server_uptime_seconds()), - "timestamp": datetime.now(timezone.utc).isoformat(), + "timestamp": datetime.now(UTC).isoformat(), } -- 2.54.0 From 5023fb67740719bb09f90118ee5e10376b47cc19 Mon Sep 17 00:00:00 2001 From: bruno Date: Tue, 27 Oct 2026 22:57:06 -0400 Subject: [PATCH 11/31] [fix] Corrections ruff CI (UP017/UP041/UP012/I001/F401/FURB162) : backend/audit.py --- backend/audit.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/backend/audit.py b/backend/audit.py index cfb8a44..42d615d 100644 --- a/backend/audit.py +++ b/backend/audit.py @@ -10,7 +10,7 @@ Format: JSON lines (one JSON object per line) for easy parsing. import json import logging import os -from datetime import datetime, timezone +from datetime import UTC, datetime from pathlib import Path logger = logging.getLogger("obsigate.audit") @@ -54,7 +54,7 @@ def log_file_save( ): """Log a file save (PUT) operation.""" _write_entry({ - "timestamp": datetime.now(timezone.utc).isoformat(), + "timestamp": datetime.now(UTC).isoformat(), "action": "file_save", "username": username, "ip": ip or "unknown", @@ -72,7 +72,7 @@ def log_file_delete( ): """Log a file delete operation.""" _write_entry({ - "timestamp": datetime.now(timezone.utc).isoformat(), + "timestamp": datetime.now(UTC).isoformat(), "action": "file_delete", "username": username, "ip": ip or "unknown", @@ -88,7 +88,7 @@ def log_config_change( ): """Log a configuration change.""" _write_entry({ - "timestamp": datetime.now(timezone.utc).isoformat(), + "timestamp": datetime.now(UTC).isoformat(), "action": "config_change", "username": username, "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): """Log a vault addition.""" _write_entry({ - "timestamp": datetime.now(timezone.utc).isoformat(), + "timestamp": datetime.now(UTC).isoformat(), "action": "vault_add", "username": username, "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): """Log a vault removal.""" _write_entry({ - "timestamp": datetime.now(timezone.utc).isoformat(), + "timestamp": datetime.now(UTC).isoformat(), "action": "vault_remove", "username": username, "ip": ip or "unknown", @@ -136,7 +136,7 @@ def log_coffre_event( (coffre:/), des identifiants et des codes de résultat. """ entry = { - "timestamp": datetime.now(timezone.utc).isoformat(), + "timestamp": datetime.now(UTC).isoformat(), "action": action, "username": username, "ip": ip or "unknown", -- 2.54.0 From cf4685eaf1a390ed38e24aa7ae7c6b7004e7e852 Mon Sep 17 00:00:00 2001 From: bruno Date: Tue, 27 Oct 2026 22:57:10 -0400 Subject: [PATCH 12/31] [fix] Corrections ruff CI (UP017/UP041/UP012/I001/F401/FURB162) : backend/auth/router.py --- backend/auth/router.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/auth/router.py b/backend/auth/router.py index 77220cf..ff78aba 100644 --- a/backend/auth/router.py +++ b/backend/auth/router.py @@ -7,6 +7,7 @@ import binascii import logging import os import re +from datetime import UTC from fastapi import APIRouter, Body, Depends, HTTPException, Request, Response 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). """ - from datetime import datetime, timezone + from datetime import datetime from .user_store import get_user, update_user 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}") 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 = [c for c in creds if c.get("credential_id") != record["credential_id"]] creds.append(record) -- 2.54.0 From a42271c7dea6f474c7e4ed122f1a7d50125e5b84 Mon Sep 17 00:00:00 2001 From: bruno Date: Tue, 27 Oct 2026 22:57:13 -0400 Subject: [PATCH 13/31] [fix] Corrections ruff CI (UP017/UP041/UP012/I001/F401/FURB162) : backend/auth/user_store.py --- backend/auth/user_store.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/backend/auth/user_store.py b/backend/auth/user_store.py index 2562fad..0c3309a 100644 --- a/backend/auth/user_store.py +++ b/backend/auth/user_store.py @@ -8,7 +8,7 @@ import logging import shutil import threading import uuid -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from pathlib import Path from .password import hash_password @@ -97,8 +97,8 @@ def create_user( "active": True, "language": "fr", # default UI language "avatar": None, # profile picture data-URL (#113) - "created_at": datetime.now(timezone.utc).isoformat(), - "password_changed_at": datetime.now(timezone.utc).timestamp(), + "created_at": datetime.now(UTC).isoformat(), + "password_changed_at": datetime.now(UTC).timestamp(), "last_login": None, "failed_attempts": 0, "locked_until": None, @@ -126,7 +126,7 @@ def update_user(username: str, updates: dict) -> dict: if "password" in safe_updates: safe_updates["password_hash"] = hash_password(safe_updates.pop("password")) # 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) _write(data) @@ -148,7 +148,7 @@ def delete_user(username: str): def record_login_success(username: str): """Record a successful login: update last_login, reset failed attempts.""" update_user(username, { - "last_login": datetime.now(timezone.utc).isoformat(), + "last_login": datetime.now(UTC).isoformat(), "failed_attempts": 0, "locked_until": None, }) @@ -171,7 +171,7 @@ def record_login_failure(username: str) -> int: # Lock after 5 failed attempts (15 minutes) if attempts >= 5: locked_until = ( - datetime.now(timezone.utc) + timedelta(minutes=15) + datetime.now(UTC) + timedelta(minutes=15) ).isoformat() updates["locked_until"] = locked_until 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"): return False locked_until = datetime.fromisoformat(user["locked_until"]) - if datetime.now(timezone.utc) > locked_until: + if datetime.now(UTC) > locked_until: # Lock expired — unlock update_user(username, {"locked_until": None, "failed_attempts": 0}) return False -- 2.54.0 From 8688875fae99b0d40efe4ed120b4e88c205c3a69 Mon Sep 17 00:00:00 2001 From: bruno Date: Tue, 27 Oct 2026 22:57:17 -0400 Subject: [PATCH 14/31] [fix] Corrections ruff CI (UP017/UP041/UP012/I001/F401/FURB162) : backend/bookslm_routes.py --- backend/bookslm_routes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/bookslm_routes.py b/backend/bookslm_routes.py index a0e4ec8..6b2e5bc 100644 --- a/backend/bookslm_routes.py +++ b/backend/bookslm_routes.py @@ -591,7 +591,7 @@ async def api_bookslm_agent( while True: try: kind, item = await asyncio.wait_for(queue.get(), timeout=0.25) - except asyncio.TimeoutError: + except TimeoutError: if run_task.done(): break continue -- 2.54.0 From bdeb2f00b380f442f4b5046127f47504f254b6f0 Mon Sep 17 00:00:00 2001 From: bruno Date: Tue, 27 Oct 2026 22:57:21 -0400 Subject: [PATCH 15/31] [fix] Corrections ruff CI (UP017/UP041/UP012/I001/F401/FURB162) : backend/coffre_crypto.py --- backend/coffre_crypto.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/coffre_crypto.py b/backend/coffre_crypto.py index 438773b..ff92dde 100644 --- a/backend/coffre_crypto.py +++ b/backend/coffre_crypto.py @@ -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: """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) @@ -185,7 +185,7 @@ def decrypt_secret_value( dek: bytes, secret_id: str, version: int, nonce_b64: str, ciphertext_b64: str ) -> str: """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") -- 2.54.0 From 604383f51c842a88279608a96815d7a0d78c5ccf Mon Sep 17 00:00:00 2001 From: bruno Date: Tue, 27 Oct 2026 22:57:25 -0400 Subject: [PATCH 16/31] [fix] Corrections ruff CI (UP017/UP041/UP012/I001/F401/FURB162) : backend/coffre_store.py --- backend/coffre_store.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/backend/coffre_store.py b/backend/coffre_store.py index e731dca..d57d21a 100644 --- a/backend/coffre_store.py +++ b/backend/coffre_store.py @@ -40,19 +40,18 @@ import re import secrets as _secrets import threading import uuid -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from pathlib import Path from backend.coffre_crypto import ( - KEKAbsenteError, + decrypt_secret_value, + encrypt_secret_value, generate_dek, load_kek, parse_kek, reencrypt_secret_value, unwrap_dek, wrap_dek, - encrypt_secret_value, - decrypt_secret_value, ) logger = logging.getLogger("obsigate.coffre_store") @@ -123,7 +122,7 @@ def _valeurs_path(base: Path) -> Path: def _now_iso() -> str: - return datetime.now(timezone.utc).isoformat() + return datetime.now(UTC).isoformat() def _parse_date(value: str | None) -> datetime | None: @@ -140,7 +139,7 @@ def _parse_date(value: str | None) -> datetime | None: except ValueError as exc: raise ValidationError(f"Date invalide : {value!r}") from exc if parsed.tzinfo is None: - parsed = parsed.replace(tzinfo=timezone.utc) + parsed = parsed.replace(tzinfo=UTC) return parsed @@ -149,7 +148,7 @@ def est_expire(expiration: str | None, maintenant: datetime | None = None) -> bo parsed = _parse_date(expiration) if parsed is None: return False - ref = maintenant or datetime.now(timezone.utc) + ref = maintenant or datetime.now(UTC) return parsed <= ref @@ -766,7 +765,7 @@ def create_octroi( ): raise IntrouvableError("Secret introuvable dans ce coffre") 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: _parse_date(expiration) with _lock: -- 2.54.0 From 683ad3ef94a501450d0275be468d7248da06101a Mon Sep 17 00:00:00 2001 From: bruno Date: Tue, 27 Oct 2026 22:57:29 -0400 Subject: [PATCH 17/31] [fix] Corrections ruff CI (UP017/UP041/UP012/I001/F401/FURB162) : backend/export.py --- backend/export.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/export.py b/backend/export.py index 01eec00..c70bd0d 100644 --- a/backend/export.py +++ b/backend/export.py @@ -359,7 +359,7 @@ def export_epub(vault_path: Path, file_path: Path) -> bytes: 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""" @@ -383,7 +383,7 @@ def export_epub(vault_path: Path, file_path: Path) -> bytes: {html_mod.escape(title)} {html_mod.escape(author)} fr - {datetime.datetime.now(datetime.timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')} + {datetime.datetime.now(datetime.UTC).strftime('%Y-%m-%dT%H:%M:%SZ')} -- 2.54.0 From 97132770e4fab49e919228816b1efd6f71158a48 Mon Sep 17 00:00:00 2001 From: bruno Date: Tue, 27 Oct 2026 22:57:32 -0400 Subject: [PATCH 18/31] [fix] Corrections ruff CI (UP017/UP041/UP012/I001/F401/FURB162) : backend/guide_export.py --- backend/guide_export.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/guide_export.py b/backend/guide_export.py index 484f13a..6840644 100644 --- a/backend/guide_export.py +++ b/backend/guide_export.py @@ -355,7 +355,7 @@ def _export_footer(lang: str) -> str: template = loc.get("guide105.export_footer", "") if "%s" not in template and "{" not in template: 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) -- 2.54.0 From 7af7b2c8c2f55a2b4475187d34ed6a4e2608249b Mon Sep 17 00:00:00 2001 From: bruno Date: Tue, 27 Oct 2026 22:57:36 -0400 Subject: [PATCH 19/31] [fix] Corrections ruff CI (UP017/UP041/UP012/I001/F401/FURB162) : backend/indexer.py --- backend/indexer.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/indexer.py b/backend/indexer.py index d5ae560..5f8cf71 100644 --- a/backend/indexer.py +++ b/backend/indexer.py @@ -5,7 +5,7 @@ import os import re import threading from collections.abc import Callable -from datetime import datetime, timezone +from datetime import UTC, datetime from pathlib import Path from typing import Any @@ -581,7 +581,7 @@ def _scan_vault( try: relative = fpath.relative_to(vault_root) 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 # 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 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 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 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) tags: list[str] = [] -- 2.54.0 From 632871ac248ce1adcabe84c0d757b4cf8ebbfd42 Mon Sep 17 00:00:00 2001 From: bruno Date: Tue, 27 Oct 2026 22:57:40 -0400 Subject: [PATCH 20/31] [fix] Corrections ruff CI (UP017/UP041/UP012/I001/F401/FURB162) : backend/notify.py --- backend/notify.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/notify.py b/backend/notify.py index dea7790..8da4e77 100644 --- a/backend/notify.py +++ b/backend/notify.py @@ -25,7 +25,7 @@ import os import smtplib import threading import uuid -from datetime import datetime, timezone +from datetime import UTC, datetime from email.message import EmailMessage from pathlib import Path from typing import Any @@ -182,7 +182,7 @@ def create_channel(name: str, channel_type: str, config: dict[str, Any]) -> dict "type": channel_type, "enabled": True, "config": normalized, - "created_at": datetime.now(timezone.utc).isoformat(), + "created_at": datetime.now(UTC).isoformat(), "last_sent_at": None, "last_error": None, } @@ -287,7 +287,7 @@ def _send_webhook(url: str, title: str, message: str, trigger: str) -> None: "event": trigger, "title": title, "message": message, - "timestamp": datetime.now(timezone.utc).isoformat(), + "timestamp": datetime.now(UTC).isoformat(), "source": "obsigate-notify", }, timeout=10.0, @@ -345,6 +345,6 @@ def _mark_sent(channel_id: str, error: str | None) -> None: channels = _read_channels() for channel in channels: 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 _write_channels(channels) -- 2.54.0 From e158232abaab01f336216a5e0c63f0f8711deafb Mon Sep 17 00:00:00 2001 From: bruno Date: Tue, 27 Oct 2026 22:57:44 -0400 Subject: [PATCH 21/31] [fix] Corrections ruff CI (UP017/UP041/UP012/I001/F401/FURB162) : backend/push.py --- backend/push.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/push.py b/backend/push.py index 5670368..a4d2251 100644 --- a/backend/push.py +++ b/backend/push.py @@ -6,7 +6,7 @@ import base64 import json import logging import os -from datetime import datetime, timezone +from datetime import UTC, datetime from pathlib import Path from typing import Any @@ -185,7 +185,7 @@ async def subscribe_push( "keys": request.subscription.keys, "vault": request.vault, "username": username, - "created_at": datetime.now(timezone.utc).isoformat() + "created_at": datetime.now(UTC).isoformat() } _push_subscriptions.append(subscription) -- 2.54.0 From f5ca656c04d3f2d8a622a3b2d65834f89f1dcaa1 Mon Sep 17 00:00:00 2001 From: bruno Date: Tue, 27 Oct 2026 22:57:47 -0400 Subject: [PATCH 22/31] [fix] Corrections ruff CI (UP017/UP041/UP012/I001/F401/FURB162) : backend/routers/backups.py --- backend/routers/backups.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/routers/backups.py b/backend/routers/backups.py index d2e4cd8..cbd1e2b 100644 --- a/backend/routers/backups.py +++ b/backend/routers/backups.py @@ -19,7 +19,7 @@ Adaptations strictement équivalentes : import logging import os import time -from datetime import datetime, timezone +from datetime import UTC, datetime from pathlib import Path from typing import Any @@ -207,7 +207,7 @@ async def api_backups_list( "file": rel_file, "backup_file": fpath.name, "timestamp": ts, - "datetime": datetime.fromtimestamp(ts, tz=timezone.utc).isoformat(), + "datetime": datetime.fromtimestamp(ts, tz=UTC).isoformat(), "size": fsize, "full_path": str(fpath), }) -- 2.54.0 From 414ec69c0a9259f1c6ee05cc32c2c4bfa456a83f Mon Sep 17 00:00:00 2001 From: bruno Date: Tue, 27 Oct 2026 22:57:50 -0400 Subject: [PATCH 23/31] [fix] Corrections ruff CI (UP017/UP041/UP012/I001/F401/FURB162) : backend/routers/realtime.py --- backend/routers/realtime.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/routers/realtime.py b/backend/routers/realtime.py index af6f63d..9202951 100644 --- a/backend/routers/realtime.py +++ b/backend/routers/realtime.py @@ -49,7 +49,7 @@ async def api_events(current_user=Depends(require_auth)): try: msg = await asyncio.wait_for(queue.get(), timeout=30.0) yield f"event: {msg['event']}\ndata: {msg['data']}\n\n" - except asyncio.TimeoutError: + except TimeoutError: # Keepalive comment yield ": keepalive\n\n" except asyncio.CancelledError: -- 2.54.0 From b322e2d24a3d3ab04b6192b67bc23d94237c8b3b Mon Sep 17 00:00:00 2001 From: bruno Date: Tue, 27 Oct 2026 22:57:53 -0400 Subject: [PATCH 24/31] [fix] Corrections ruff CI (UP017/UP041/UP012/I001/F401/FURB162) : backend/scheduler.py --- backend/scheduler.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/backend/scheduler.py b/backend/scheduler.py index 9aeeae4..2d1a9c0 100644 --- a/backend/scheduler.py +++ b/backend/scheduler.py @@ -26,7 +26,7 @@ import logging import os import threading import uuid -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from pathlib import Path from typing import Any @@ -118,7 +118,7 @@ def _validate_schedule(schedule: dict[str, Any]) -> dict[str, Any]: try: parsed = datetime.fromisoformat(at) if parsed.tzinfo is None: - parsed = parsed.replace(tzinfo=timezone.utc) + parsed = parsed.replace(tzinfo=UTC) except ValueError: raise ValueError("at doit être une date ISO-8601 (ex. 2026-10-05T08:30:00)") from None return {"kind": kind, "at": parsed.isoformat()} @@ -138,7 +138,7 @@ def create_task( """Create a scheduled task. Raises ValueError on invalid action/schedule.""" validated_action = _validate_action(action) validated_schedule = _validate_schedule(schedule) - now = datetime.now(timezone.utc) + now = datetime.now(UTC) with _lock: tasks = _read_tasks() 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: """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: - now = now.replace(tzinfo=timezone.utc) + now = now.replace(tzinfo=UTC) schedule = task.get("schedule", {}) kind = schedule.get("kind") 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: last = datetime.fromisoformat(str(last_run_at)) if last.tzinfo is None: - last = last.replace(tzinfo=timezone.utc) + last = last.replace(tzinfo=UTC) except ValueError: last = None if kind == "interval_hours": @@ -233,11 +233,11 @@ def compute_next_run(task: dict[str, Any], now: datetime | None = None) -> datet try: at = datetime.fromisoformat(str(schedule.get("at"))) if at.tzinfo is None: - at = at.replace(tzinfo=timezone.utc) + at = at.replace(tzinfo=UTC) except ValueError: return now 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 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"} try: 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_error"] = None 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.") return {"task_id": task_id, "ok": True, "result": result} 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_error"] = str(e) 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]]: """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]] = [] for task in _read_tasks(): if not task.get("enabled", True): @@ -323,7 +323,7 @@ def tick(now: datetime | None = None) -> list[dict[str, Any]]: try: next_run = datetime.fromisoformat(str(task.get("next_run_at") or "")) if next_run.tzinfo is None: - next_run = next_run.replace(tzinfo=timezone.utc) + next_run = next_run.replace(tzinfo=UTC) except ValueError: next_run = compute_next_run(task, now) if next_run <= now: -- 2.54.0 From c8dce25e0d6cc910585db429fd4de3ff3ca556c2 Mon Sep 17 00:00:00 2001 From: bruno Date: Tue, 27 Oct 2026 22:57:56 -0400 Subject: [PATCH 25/31] [fix] Corrections ruff CI (UP017/UP041/UP012/I001/F401/FURB162) : backend/services/backups.py --- backend/services/backups.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/services/backups.py b/backend/services/backups.py index 0716262..e1e8283 100644 --- a/backend/services/backups.py +++ b/backend/services/backups.py @@ -14,7 +14,7 @@ import logging import os import shutil import time -from datetime import datetime, timezone +from datetime import UTC, datetime from pathlib import Path 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): continue try: - dt = datetime.fromtimestamp(ts, tz=timezone.utc).isoformat() + dt = datetime.fromtimestamp(ts, tz=UTC).isoformat() except (OSError, OverflowError, ValueError) as ts_err: logger.warning(f"Skipping backup with invalid timestamp {ts}: {ts_err}") continue @@ -219,7 +219,7 @@ def diff_backup( status=500, details={"vault": vault_name, "path": path, "version": ts}, ) 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}" try: -- 2.54.0 From b5d789f02a577007b0549285b5529f38ef178e92 Mon Sep 17 00:00:00 2001 From: bruno Date: Tue, 27 Oct 2026 22:57:59 -0400 Subject: [PATCH 26/31] [fix] Corrections ruff CI (UP017/UP041/UP012/I001/F401/FURB162) : backend/services/recent.py --- backend/services/recent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/services/recent.py b/backend/services/recent.py index 7c4b00f..4129cd2 100644 --- a/backend/services/recent.py +++ b/backend/services/recent.py @@ -117,7 +117,7 @@ def list_recent( for v_name, f in recent: iso_modified = f.get("modified", "") try: - mtime_dt = datetime.fromisoformat(iso_modified.replace("Z", "+00:00")) + mtime_dt = datetime.fromisoformat(iso_modified) mtime_val = mtime_dt.timestamp() except Exception: mtime_val = time.time() -- 2.54.0 From 769e3ca559890088df448a5dff90f60654de1a2b Mon Sep 17 00:00:00 2001 From: bruno Date: Tue, 27 Oct 2026 22:58:02 -0400 Subject: [PATCH 27/31] [fix] Corrections ruff CI (UP017/UP041/UP012/I001/F401/FURB162) : backend/services/vaults.py --- backend/services/vaults.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/backend/services/vaults.py b/backend/services/vaults.py index 8df2672..1303367 100644 --- a/backend/services/vaults.py +++ b/backend/services/vaults.py @@ -7,6 +7,7 @@ Single source of truth consumed by both the REST routes (``/api/vaults``, from __future__ import annotations +from datetime import UTC from pathlib import Path 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]: """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.vault_settings import get_vault_setting @@ -98,7 +99,7 @@ def browse_directory(vault_name: str, path: str = "") -> dict[str, Any]: "type": "file", "size": stat.st_size, "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(), }) except PermissionError: @@ -156,7 +157,7 @@ def list_all_files( ServiceError: ``not_found`` (404) if the directory does not exist, ``permission_denied`` (403) if it cannot be read. """ - from datetime import datetime, timezone + from datetime import datetime from backend.indexer import IGNORED_DIRS @@ -215,7 +216,7 @@ def list_all_files( "vault": vault_name, "size": stat.st_size, "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 "", "tags": _indexed_tags(vault_name, rel_path), } -- 2.54.0 From bc26d1506bbd872b76c10b31acee8e05349efe2f Mon Sep 17 00:00:00 2001 From: bruno Date: Tue, 27 Oct 2026 22:58:05 -0400 Subject: [PATCH 28/31] [fix] Corrections ruff CI (UP017/UP041/UP012/I001/F401/FURB162) : backend/share.py --- backend/share.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/share.py b/backend/share.py index c008d96..73327d8 100644 --- a/backend/share.py +++ b/backend/share.py @@ -11,7 +11,7 @@ import json import logging import secrets import threading -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from pathlib import Path logger = logging.getLogger("obsigate.share") @@ -58,7 +58,7 @@ def create_share( expires_at = None 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 = { "id": token, @@ -66,7 +66,7 @@ def create_share( "vault": vault, "path": path, "created_by": created_by, - "created_at": datetime.now(timezone.utc).isoformat(), + "created_at": datetime.now(UTC).isoformat(), "expires_at": expires_at, "access_count": 0, "last_accessed": None, @@ -86,7 +86,7 @@ def get_share_by_token(token: str) -> dict | None: return None if share.get("expires_at"): expires = datetime.fromisoformat(share["expires_at"]) - if datetime.now(timezone.utc) > expires: + if datetime.now(UTC) > expires: return None return share @@ -98,7 +98,7 @@ def record_access(token: str): share = data["shares"].get(token) if share: 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) -- 2.54.0 From 90da0ac99008a9644887e8045790fd1578263ecb Mon Sep 17 00:00:00 2001 From: bruno Date: Tue, 27 Oct 2026 22:58:08 -0400 Subject: [PATCH 29/31] [fix] Corrections ruff CI (UP017/UP041/UP012/I001/F401/FURB162) : backend/tools/audit.py --- backend/tools/audit.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/tools/audit.py b/backend/tools/audit.py index cf39f16..62279f4 100644 --- a/backend/tools/audit.py +++ b/backend/tools/audit.py @@ -7,7 +7,7 @@ Reuses the application audit log (``data/audit.log``, JSON lines) and adds an from __future__ import annotations -from datetime import datetime, timezone +from datetime import UTC, datetime from typing import Any from backend.audit import _write_entry @@ -41,7 +41,7 @@ def log_tool_call( ) -> None: """Append an ``ai_tool_call`` entry to the audit log.""" _write_entry({ - "timestamp": datetime.now(timezone.utc).isoformat(), + "timestamp": datetime.now(UTC).isoformat(), "action": "ai_tool_call", "username": username, "ip": ip or "unknown", -- 2.54.0 From b6e5f01748a652ad27dc13ce7d0f4631be792a88 Mon Sep 17 00:00:00 2001 From: bruno Date: Tue, 27 Oct 2026 22:58:11 -0400 Subject: [PATCH 30/31] [fix] Corrections ruff CI (UP017/UP041/UP012/I001/F401/FURB162) : backend/watcher.py --- backend/watcher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/watcher.py b/backend/watcher.py index 0e997fc..5f2125b 100644 --- a/backend/watcher.py +++ b/backend/watcher.py @@ -244,7 +244,7 @@ class VaultWatcher: key = event.get('dest') or event['src'] pending[key] = event - except asyncio.TimeoutError: + except TimeoutError: if pending: events_to_process = list(pending.values()) pending.clear() -- 2.54.0 From 01572736c009a27d1e043af315862415977d394c Mon Sep 17 00:00:00 2001 From: bruno Date: Tue, 27 Oct 2026 22:58:14 -0400 Subject: [PATCH 31/31] [fix] Corrections ruff CI (UP017/UP041/UP012/I001/F401/FURB162) : backend/webhooks.py --- backend/webhooks.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/webhooks.py b/backend/webhooks.py index 8e0d102..cd36ae4 100644 --- a/backend/webhooks.py +++ b/backend/webhooks.py @@ -31,7 +31,7 @@ import os import socket import threading import uuid -from datetime import datetime, timezone +from datetime import UTC, datetime from pathlib import Path from urllib.parse import urlparse @@ -275,7 +275,7 @@ def create_webhook( "url": url, "events": [e for e in events if e in VALID_EVENTS], "enabled": True, - "created_at": datetime.now(timezone.utc).isoformat(), + "created_at": datetime.now(UTC).isoformat(), "last_fired_at": None, # #198 : filtres optionnels (None = pas de filtre, reçoit tout). "vault": _normalize_vault_filter(vault), @@ -344,7 +344,7 @@ async def dispatch_webhooks(event_type: str, data: dict): payload = { "event": event_type, - "timestamp": datetime.now(timezone.utc).isoformat(), + "timestamp": datetime.now(UTC).isoformat(), "data": data, } 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})") else: 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: logger.warning(f"Webhook '{wh['name']}' error: {e}") -- 2.54.0