From 3168764369771d12492ab68c9c1b30d9d54ef773 Mon Sep 17 00:00:00 2001 From: dj-oyu <68707227+dj-oyu@users.noreply.github.com> Date: Sun, 22 Feb 2026 10:28:11 +0900 Subject: [PATCH] fix: graceful SSE shutdown and remove forced fullscreen Add Close/Done to StateNotifier so SSE handlers exit on server shutdown instead of blocking indefinitely. Remove tg.expand() to allow MiniApp to open at Telegram's default partial height. Co-Authored-By: Claude Opus 4.6 --- cmd/picoclaw/cmd_gateway.go | 14 ++++++++++---- pkg/miniapp/miniapp.go | 22 +++++++++++++++++++++- pkg/miniapp/static/index.html | 1 - 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/cmd/picoclaw/cmd_gateway.go b/cmd/picoclaw/cmd_gateway.go index 47c889718..c19606f85 100644 --- a/cmd/picoclaw/cmd_gateway.go +++ b/cmd/picoclaw/cmd_gateway.go @@ -198,6 +198,7 @@ func gatewayCmd() { // Mini App setup: register routes and determine TLS mode useTLS := false var tlsCert, tlsKey string + var miniappNotifier *miniapp.StateNotifier if cfg.Channels.Telegram.Enabled { webAppURL := cfg.Channels.Telegram.WebAppURL if webAppURL == "" { @@ -222,9 +223,9 @@ func gatewayCmd() { if webAppURL != "" { provider := &agentLoopDataProvider{loop: agentLoop} sender := &telegramCommandSender{bus: msgBus} - notifier := miniapp.NewStateNotifier() - handler := miniapp.NewHandler(provider, sender, cfg.Channels.Telegram.Token, notifier) - agentLoop.OnStateChange = notifier.Notify + miniappNotifier = miniapp.NewStateNotifier() + handler := miniapp.NewHandler(provider, sender, cfg.Channels.Telegram.Token, miniappNotifier) + agentLoop.OnStateChange = miniappNotifier.Notify handler.RegisterRoutes(healthServer.Mux()) fmt.Printf("✓ Mini App registered at %s\n", webAppURL) } @@ -255,7 +256,12 @@ func gatewayCmd() { fmt.Println("\nShutting down...") cancel() - healthServer.Stop(context.Background()) + if miniappNotifier != nil { + miniappNotifier.Close() + } + shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 3*time.Second) + defer shutdownCancel() + healthServer.Stop(shutdownCtx) deviceService.Stop() heartbeatService.Stop() cronService.Stop() diff --git a/pkg/miniapp/miniapp.go b/pkg/miniapp/miniapp.go index dda3c2eb2..0c46bb45b 100644 --- a/pkg/miniapp/miniapp.go +++ b/pkg/miniapp/miniapp.go @@ -74,11 +74,15 @@ type CommandSender interface { type StateNotifier struct { mu sync.Mutex subs map[chan struct{}]struct{} + done chan struct{} } // NewStateNotifier creates a new StateNotifier. func NewStateNotifier() *StateNotifier { - return &StateNotifier{subs: make(map[chan struct{}]struct{})} + return &StateNotifier{ + subs: make(map[chan struct{}]struct{}), + done: make(chan struct{}), + } } // Subscribe returns a channel that receives a signal on each state change. @@ -97,6 +101,20 @@ func (n *StateNotifier) Unsubscribe(ch chan struct{}) { n.mu.Unlock() } +// Close signals all SSE handlers to exit. +func (n *StateNotifier) Close() { + select { + case <-n.done: + default: + close(n.done) + } +} + +// Done returns a channel that is closed when the notifier is shut down. +func (n *StateNotifier) Done() <-chan struct{} { + return n.done +} + // Notify sends a signal to all subscribers, coalescing rapid notifications. func (n *StateNotifier) Notify() { n.mu.Lock() @@ -279,6 +297,8 @@ func (h *Handler) apiEvents(w http.ResponseWriter, r *http.Request) { select { case <-r.Context().Done(): return + case <-h.notifier.Done(): + return case <-ch: sendSSEIfChanged(w, flusher, "plan", h.provider.GetPlanInfo(), &lastPlan) sendSSEIfChanged(w, flusher, "session", diff --git a/pkg/miniapp/static/index.html b/pkg/miniapp/static/index.html index ae4319d8c..5b776b797 100644 --- a/pkg/miniapp/static/index.html +++ b/pkg/miniapp/static/index.html @@ -458,7 +458,6 @@