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

+21 -1
View File
@@ -19,7 +19,7 @@ import (
)
//go:generate go run ../../tools/assemble-ui ui
//go:embed ui/index.html ui/marked.min.js ui/fonts/*.woff2
//go:embed ui/index.html ui/marked.min.js ui/sw.js ui/manifest.webmanifest ui/fonts/*.woff2
var uiFS embed.FS
// cmdWeb starts the HTTP server on the given port (default 8090).
@@ -141,6 +141,23 @@ func newWebMux() *http.ServeMux {
w.Header().Set("Cache-Control", "public, max-age=86400")
w.Write(b)
})
// Service worker + manifeste des notifications Web Push (voir push.go / sw.js).
// PUBLICS (aucun secret) et servis en clair à la RACINE : un service worker
// doit venir de l'origine même, et son scope est celui de son URL. no-store
// sur le worker pour qu'une mise à jour de l'image soit toujours reprise.
mux.HandleFunc("/sw.js", func(w http.ResponseWriter, r *http.Request) {
b, _ := uiFS.ReadFile("ui/sw.js")
w.Header().Set("Content-Type", "application/javascript")
w.Header().Set("Cache-Control", "no-store, max-age=0")
w.Header().Set("Service-Worker-Allowed", "/")
w.Write(b)
})
mux.HandleFunc("/manifest.webmanifest", func(w http.ResponseWriter, r *http.Request) {
b, _ := uiFS.ReadFile("ui/manifest.webmanifest")
w.Header().Set("Content-Type", "application/manifest+json")
w.Header().Set("Cache-Control", "public, max-age=3600")
w.Write(b)
})
// Polices embarquées (Inter + JetBrains Mono, sous-ensemble latin). Servies
// par Loki et non par Google Fonts : une instance locale ou derrière un
// réseau fermé doit s'afficher correctement sans appeler un tiers, et sans
@@ -264,6 +281,9 @@ func newWebMux() *http.ServeMux {
api("/api/tasks/pause", handleTasksPause)
api("/api/tasks/run", handleTaskRun)
api("/api/tasks/stop", handleTaskStop) // arrête la tâche en cours (script : via son registre)
api("/api/push/key", handlePushKey) // clé publique VAPID (pour s'abonner)
api("/api/push/subscribe", handlePushSubscribe) // enregistre un abonnement du navigateur
api("/api/push/unsubscribe", handlePushUnsubscribe) // retire un abonnement
api("/api/reasoning-effort", handleReasoningEffort) // intensité réglable depuis la barre de saisie
api("/api/network", handleNetwork) // écoute LAN du moteur + pare-feu (Windows)
api("/api/prefs", handleWebPrefs)