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 <noreply@anthropic.com>
This commit is contained in:
dj-oyu 2026-02-22 10:28:11 +09:00
parent cd6a837da3
commit 3168764369
3 changed files with 31 additions and 6 deletions

View file

@ -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()

View file

@ -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",

View file

@ -458,7 +458,6 @@
<script>
const tg = window.Telegram.WebApp;
tg.ready();
tg.expand();
const API_BASE = location.origin;
let initData = tg.initData || '';