From 2ee317a46eeec046ac18d8c927a68e14670d3f4a Mon Sep 17 00:00:00 2001 From: Jacquin Antoine Date: Sun, 19 Jul 2026 23:57:48 +0200 Subject: [PATCH] =?UTF-8?q?Ajoute=20un=20bouton=20de=20test=20de=20la=20no?= =?UTF-8?q?tification=20ntfy=20depuis=20les=20r=C3=A9glages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Permet de vérifier en un clic que le serveur et le topic ntfy renseignés fonctionnent, avant d'attendre un vrai rappel. Le bouton envoie une notification de test via la même chaîne que les rappels réels (donc tracée dans /logs), et affiche le résultat inline. La fonction send_ntfy retourne désormais un booléen de succès pour permettre ce retour utilisateur. 💘 Generated with Crush Assisted-by: Crush:glm-5.2 --- app/main.py | 35 ++++++++++++++++++++++++++++++++- app/notifications.py | 4 +++- app/templates/settings.html | 33 +++++++++++++++++++++++++++++++ app/tests/test_logs.py | 39 +++++++++++++++++++++++++++++++++++++ 4 files changed, 109 insertions(+), 2 deletions(-) diff --git a/app/main.py b/app/main.py index c1beaa6..ff5eb72 100644 --- a/app/main.py +++ b/app/main.py @@ -24,7 +24,7 @@ from models import ( from calcul import compute_week, minutes_to_hhmm, hhmm_to_minutes, heures_dues from stats import compute_all_stats from export import export_semaine_ods, export_annee_ods -from notifications import reminder_loop +from notifications import reminder_loop, send_ntfy logging.basicConfig( level=getattr(logging, load_config().get("log_level", "DEBUG").upper(), logging.DEBUG), @@ -594,6 +594,39 @@ def settings_save( return RedirectResponse("/settings?saved=1", status_code=303) +@app.post("/settings/test-ntfy", response_class=HTMLResponse) +def settings_test_ntfy(request: Request): + user_id = _get_user(request) + if not user_id: + return RedirectResponse("/login", status_code=302) + + notif = load_notif_config(user_id) + topic = notif.get("ntfy_topic") + server = notif.get("ntfy_server", "https://ntfy.sh") + + if not topic: + logger.warning("test ntfy: pas de topic configuré pour %r", user_id) + return HTMLResponse( + '
' + 'Renseigne et enregistre un topic ntfy avant de tester.' + '
' + ) + + logger.info("test ntfy: envoi de test pour %r sur %r/%r", user_id, server, topic) + ok = send_ntfy( + server, topic, + "Ceci est une notification de test — ta configuration ntfy fonctionne.", + "Test pointeuse", user_id=user_id, + ) + cls = "settings-saved" if ok else "settings-error" + msg = ("Notification envoyée. Vérifie ton téléphone, elle doit arriver en quelques secondes." + if ok else + "Échec de l'envoi. Vérifie le serveur et le topic, et l'optimisation batterie de l'app ntfy.") + return HTMLResponse( + f'
{msg}
' + ) + + @app.post("/settings/plages", response_class=HTMLResponse) def settings_plages_save( request: Request, diff --git a/app/notifications.py b/app/notifications.py index e301d80..4957081 100644 --- a/app/notifications.py +++ b/app/notifications.py @@ -11,7 +11,7 @@ logger = logging.getLogger("pointeuse.notifications") CHECK_INTERVAL_SECONDS = 300 -def send_ntfy(server: str, topic: str, message: str, title: str, user_id: str | None = None): +def send_ntfy(server: str, topic: str, message: str, title: str, user_id: str | None = None) -> bool: req = urllib.request.Request( f"{server}/{topic}", data=message.encode("utf-8"), @@ -23,11 +23,13 @@ def send_ntfy(server: str, topic: str, message: str, title: str, user_id: str | 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}") + return True 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 + return False def _todays_pointage(user_id: str, today: datetime) -> dict: diff --git a/app/templates/settings.html b/app/templates/settings.html index a729cbb..6dd1e47 100644 --- a/app/templates/settings.html +++ b/app/templates/settings.html @@ -103,6 +103,16 @@ +
+ + +
@@ -202,5 +212,28 @@ border-color: var(--accent); background: rgba(47,95,160,.06); } +.settings-test-row { + display: flex; + align-items: center; + gap: .75rem; + flex-wrap: wrap; + max-width: 420px; +} +.settings-test-btn { + flex-shrink: 0; +} +#ntfy-test-result { + flex: 1; + min-width: 0; +} +#ntfy-test-result:empty { display: none; } +#ntfy-test-result:not(:empty) { + font-family: var(--mono); + font-size: 12px; + padding: .5rem .8rem; + border-left: 3px solid var(--muted); + background: var(--ground); + line-height: 1.45; +} {% endblock %} diff --git a/app/tests/test_logs.py b/app/tests/test_logs.py index ec4a930..64a32a3 100644 --- a/app/tests/test_logs.py +++ b/app/tests/test_logs.py @@ -146,3 +146,42 @@ def test_invalid_log_type_rejected(): assert False, "doit lever une AssertionError" except AssertionError: pass + + +# ── Bouton de test ntfy (/settings/test-ntfy) ──────────────────────────────── + +def test_test_ntfy_requires_auth(): + unauth = TestClient(main.app) + r = unauth.post("/settings/test-ntfy", follow_redirects=False) + assert r.status_code == 302 + assert "/login" in r.headers["location"] + + +def test_test_ntfy_without_topic_returns_error(): + # Alice n'a pas de topic configuré → message d'erreur + models.save_notif_config("alice", ntfy_topic="", ntfy_server="https://ntfy.sh") + r = client.post("/settings/test-ntfy", cookies=COOKIES) + assert r.status_code == 200 + assert "settings-error" in r.text + assert "topic" in r.text.lower() + + +def test_test_ntfy_with_bad_server_returns_failure(): + # Topic renseigné mais serveur injoignable → échec + models.save_notif_config("alice", ntfy_topic="topic-test", ntfy_server="http://127.0.0.1:1") + r = client.post("/settings/test-ntfy", cookies=COOKIES) + assert r.status_code == 200 + assert "settings-error" in r.text + assert "Échec" in r.text + # L'entrée de log d'échec doit être présente + logs = models.load_logs("alice") + assert any(l["event"] == "échec" and l["type"] == "notif" for l in logs) + + +def test_test_ntfy_logs_to_journal(): + """Le bouton de test doit écrire dans le journal (même en échec).""" + models.save_notif_config("alice", ntfy_topic="topic-test-2", ntfy_server="http://127.0.0.1:1") + client.post("/settings/test-ntfy", cookies=COOKIES) + logs = models.load_logs("alice") + # L'entrée la plus récente doit être l'échec du test (message contient "Test pointeuse") + assert any(l["event"] == "échec" and "Test pointeuse" in l["message"] for l in logs)