Notifications Web Push (+ manifeste PWA)

Reprise d'AJEAN : le serveur pousse une notification vers les navigateurs
abonnés, directement via leur service de push — donc app fermée et téléphone
verrouillé, là où une notification côté page ne peut rien (un onglet caché
relâche son flux SSE).

Deux déclencheurs : la fin d'un tour utilisateur (sauf interruption par le
bouton stop : celui qui a coupé est devant l'écran) et — ajout propre à Loki
— la fin d'une TÂCHE PLANIFIÉE, succès comme échec. C'est le cas qui compte
le plus : une tâche tourne justement quand personne ne regarde.

Clés VAPID générées à la première demande et rangées dans la base ;
abonnements persistés et purgés quand le service de push répond 404/410.
Corps de notification générique, sans extrait de réponse : elle transite par
Apple ou Google. /sw.js et /manifest.webmanifest sont servis à la racine
(un service worker doit venir de l'origine) ; le worker ne fait QUE recevoir
les push, sans cache — mettre l'UI en cache servirait une interface périmée
après une mise à jour de l'image.

Interrupteur dans Réglages → Mode agent, à armer sur chaque appareil. L'UI
dit ce qui manque plutôt que d'échouer : HTTPS requis, notifications
bloquées, ou iPhone à ajouter d'abord à l'écran d'accueil.

Nouvelle dépendance : github.com/SherClockHolmes/webpush-go (RFC 8291).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K6CAgoLJzufeA8rZTSpSpY
This commit is contained in:
Claude committed 2026-09-22 13:37:16 +00:00
1 parent 74fb1fc781
commit e9ec8ae3a8
13 files changed
+712 -2

No files matched your search

+11
View File
@@ -285,4 +285,15 @@ func recordTaskEnd(id string, start time.Time, report string, err error) {
cur.LastReport = report
}
_ = saveTask(cur)
// Notification Web Push : une tâche planifiée tourne SANS personne devant
// l'écran — c'est le cas où être prévenu compte le plus. Détaché (l'envoi
// HTTP ne doit pas retenir le planificateur) et silencieux sur une
// interruption manuelle : celui qui a cliqué « stop » est déjà là.
if hasPushSubs() && !errors.Is(err, context.Canceled) {
title, body := "Loki · tâche terminée", cur.Name
if err != nil {
title = "Loki · tâche en échec"
}
go sendPushToAll(title, body)
}
}