diff --git a/app/main.py b/app/main.py index 7e7b2d1..b80e43a 100644 --- a/app/main.py +++ b/app/main.py @@ -19,6 +19,7 @@ from models import ( load_notif_config, save_notif_config, find_user_by_token, save_presence, load_auth, save_auth, find_user_by_reset_token, load_plages, save_plages, heures_jour_min, + append_log, load_logs, ) from calcul import compute_week, minutes_to_hhmm, hhmm_to_minutes, heures_dues from stats import compute_all_stats @@ -637,6 +638,7 @@ def presence_arrivee(token: str): return JSONResponse({"error": "invalid token"}, status_code=404) logger.info("présence: arrivée détectée pour %r", user_id) save_presence(user_id, present=True, since=datetime.now().isoformat()) + append_log(user_id, "presence", "arrivée", "Présence détectée par Tasker") return {"ok": True} @@ -648,6 +650,22 @@ def presence_depart(token: str): return JSONResponse({"error": "invalid token"}, status_code=404) logger.info("présence: départ détecté pour %r", user_id) save_presence(user_id, present=False, since=datetime.now().isoformat()) + append_log(user_id, "presence", "départ", "Départ détecté par Tasker") return {"ok": True} +@app.get("/logs", response_class=HTMLResponse) +def logs_page(request: Request): + user_id = _get_user(request) + if not user_id: + return RedirectResponse("/login", status_code=302) + + logs = load_logs(user_id) + return templates.TemplateResponse("logs.html", { + "request": request, + "current_user": user_id, + "logs": logs, + "filter": request.query_params.get("filter", "all"), + }) + + diff --git a/app/models.py b/app/models.py index b9abf74..3e024db 100644 --- a/app/models.py +++ b/app/models.py @@ -1,6 +1,7 @@ """Flat-file storage: JSON files per week for pointages, one file for congés.""" import json import secrets +from datetime import datetime from pathlib import Path from calcul import DEFAULT_PLAGES @@ -254,3 +255,47 @@ def toggle_conge(date: str, type_conge: str, user_id: str = ""): conges.sort(key=lambda c: (c["date"], c["type"])) save_conges(conges, user_id) + + +# ── Journaux (notifications + présence) ─────────────────────────────────────── + +LOG_TYPES = ("notif", "presence") +MAX_LOGS = 500 + + +def _logs_file(user_id: str) -> Path: + return _user_root(user_id) / "logs.json" + + +def append_log(user_id: str, entry_type: str, event: str, message: str = "", ts: str | None = None): + """Ajoute une entrée au journal de l'utilisateur (rotation sur MAX_LOGS).""" + assert entry_type in LOG_TYPES, f"type de journal invalide: {entry_type!r}" + f = _logs_file(user_id) + logs = [] + if f.exists(): + try: + logs = json.loads(f.read_text()) + except json.JSONDecodeError: + logs = [] + logs.append({ + "ts": ts or datetime.now().isoformat(timespec="seconds"), + "type": entry_type, + "event": event, + "message": message, + }) + if len(logs) > MAX_LOGS: + logs = logs[-MAX_LOGS:] + _ensure_user_dirs(user_id) + f.write_text(json.dumps(logs, indent=2, ensure_ascii=False)) + + +def load_logs(user_id: str) -> list[dict]: + """Retourne les entrées du journal, des plus récentes aux plus anciennes.""" + f = _logs_file(user_id) + if not f.exists(): + return [] + try: + logs = json.loads(f.read_text()) + except json.JSONDecodeError: + return [] + return list(reversed(logs)) diff --git a/app/notifications.py b/app/notifications.py index b2c2ac1..e301d80 100644 --- a/app/notifications.py +++ b/app/notifications.py @@ -4,14 +4,14 @@ import logging import urllib.request from datetime import datetime -from models import list_users, load_notif_config, load_presence, load_week_pointages +from models import list_users, load_notif_config, load_presence, load_week_pointages, append_log logger = logging.getLogger("pointeuse.notifications") CHECK_INTERVAL_SECONDS = 300 -def send_ntfy(server: str, topic: str, message: str, title: str): +def send_ntfy(server: str, topic: str, message: str, title: str, user_id: str | None = None): req = urllib.request.Request( f"{server}/{topic}", data=message.encode("utf-8"), @@ -21,8 +21,12 @@ def send_ntfy(server: str, topic: str, message: str, title: str): try: urllib.request.urlopen(req, timeout=10).close() logger.info("notif ntfy envoyée: server=%r topic=%r title=%r", server, topic, title) + if user_id: + append_log(user_id, "notif", "envoyée", f"{title} — {message}") except Exception: logger.exception("échec de l'envoi ntfy: server=%r topic=%r title=%r", server, topic, title) + if user_id: + append_log(user_id, "notif", "échec", f"{title} — {message}") # best-effort: a missed reminder isn't worth crashing the loop @@ -55,10 +59,18 @@ async def check_user(user_id: str, now: datetime): if presence.get("present") and not jour.get("matin_entree"): logger.info("rappel 'pense à pointer' pour %r", user_id) - await asyncio.to_thread(send_ntfy, server, topic, "Tu es dans les locaux mais pas encore pointé ce matin.", "⏰ Pense à pointer") + await asyncio.to_thread( + send_ntfy, server, topic, + "Tu es dans les locaux mais pas encore pointé ce matin.", + "⏰ Pense à pointer", user_id, + ) elif not presence.get("present") and jour.get("matin_entree") and not jour.get("aprem_sortie"): logger.info("rappel 'pense à dépointer' pour %r", user_id) - await asyncio.to_thread(send_ntfy, server, topic, "Tu as quitté les locaux sans dépointer la sortie.", "⏰ Pense à dépointer") + await asyncio.to_thread( + send_ntfy, server, topic, + "Tu as quitté les locaux sans dépointer la sortie.", + "⏰ Pense à dépointer", user_id, + ) async def reminder_loop(): diff --git a/app/templates/base.html b/app/templates/base.html index b00946c..258c5d1 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -1032,6 +1032,7 @@ {% if current_user is defined and current_user %} Semaine courante Statistiques + Journaux Réglages {{ current_user }} Déconnexion diff --git a/app/templates/logs.html b/app/templates/logs.html new file mode 100644 index 0000000..1e68289 --- /dev/null +++ b/app/templates/logs.html @@ -0,0 +1,141 @@ +{% extends "base.html" %} +{% block content %} +
+
+ Journaux +
+ Notifications ntfy et détections de présence (Tasker) — {{ current_user }} +
+
+
+ +
+ {% set counts = {'all': logs|length, 'notif': logs | selectattr('type', 'equalto', 'notif') | list | length, 'presence': logs | selectattr('type', 'equalto', 'presence') | list | length} %} + + Tout {{ counts.all }} + + + Notifications {{ counts.notif }} + + + Présence {{ counts.presence }} + +
+ +{% set visible = logs if filter == 'all' else logs | selectattr('type', 'equalto', filter) | list %} + +{% if visible %} +
+ {% for entry in visible %} +
+
{{ entry.ts }}
+
{{ 'Notification' if entry.type == 'notif' else 'Présence' }}
+
+ {{ entry.event }} + {% if entry.message %}{{ entry.message }}{% endif %} +
+
+ {% endfor %} +
+{% else %} +
Aucun événement enregistré pour le moment.
+{% endif %} + + +{% endblock %}