From cb549a8803d0309f4fb91f6facac5c1c57f0f2c6 Mon Sep 17 00:00:00 2001 From: Joshua King Date: Fri, 5 Jun 2026 20:14:13 -0400 Subject: [PATCH] fix(dashboard): keep SSE alive + seed camera list via REST MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dashboard showed "No Cameras Connected" despite the API returning the camera: - middleware.Timeout + http.Server.WriteTimeout (10s) cancelled the long-lived /api/v1/events/stream every 10s, before any 30s status event could arrive — so the SSE-fed store never populated. Drop the global request timeout and set WriteTimeout=0 (closed-LAN kiosk). - The SPA never seeded from GET /api/v1/cameras (SSE only pushes on change). Fetch the list once on mount and setCameras(). Co-Authored-By: Claude Opus 4.8 --- cmd/server/main.go | 15 +++++++++------ src/App.tsx | 10 +++++++++- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/cmd/server/main.go b/cmd/server/main.go index d2ee263..dd27687 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -78,7 +78,9 @@ func main() { r.Use(middleware.RealIP) r.Use(middleware.Logger) r.Use(middleware.Recoverer) - r.Use(middleware.Timeout(cfg.WriteTimeout)) + // No global request timeout: it cancels the long-lived SSE stream + // (/api/v1/events/stream) — that's why the dashboard never received + // camera events. Closed-LAN kiosk, so dropping it is fine. // Health check (no auth) r.Get("/health", func(w http.ResponseWriter, r *http.Request) { @@ -94,11 +96,12 @@ func main() { // Create server httpServer := &http.Server{ - Addr: ":" + cfg.Port, - Handler: r, - ReadTimeout: cfg.ReadTimeout, - WriteTimeout: cfg.WriteTimeout, - IdleTimeout: cfg.IdleTimeout, + Addr: ":" + cfg.Port, + Handler: r, + ReadTimeout: cfg.ReadTimeout, + // WriteTimeout intentionally 0: SSE responses are long-lived and a + // write deadline would terminate them mid-stream. + IdleTimeout: cfg.IdleTimeout, } // Graceful shutdown diff --git a/src/App.tsx b/src/App.tsx index 801a544..efc9494 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import { useState, useCallback, useMemo } from 'react' +import { useState, useCallback, useMemo, useEffect } from 'react' import { Camera, Play, Square, Wifi, WifiOff, AlertTriangle } from 'lucide-react' import { useSSE } from './hooks/useSSE' import { useCameraStore } from './store/useCameraStore' @@ -15,6 +15,14 @@ function App() { // SSE connection + live store const { connectionState } = useSSE() + // Seed the list once on mount via the REST API. SSE only pushes on change, + // so without this the dashboard is empty until the next status event. + useEffect(() => { + api.getCameras() + .then((list) => useCameraStore.getState().setCameras(list)) + .catch(() => { /* SSE will fill in shortly */ }) + }, []) + // Subscribe to full camera state — dashboard needs every change const camerasMap = useCameraStore((s) => s.cameras) const cameras = useMemo(() => Array.from(camerasMap.values()), [camerasMap])