diff --git a/internal/store/accounts.go b/internal/store/accounts.go index 80041d6..86bdfd5 100644 --- a/internal/store/accounts.go +++ b/internal/store/accounts.go @@ -38,7 +38,7 @@ func (s *Store) ListAccountsByTask(ctx context.Context, taskID int64) ([]Account return nil, err } defer rows.Close() - var out []Account + out := []Account{} for rows.Next() { var a Account if err := rows.Scan(&a.ID, &a.TaskID, &a.SrcLogin, &a.SrcPassEnc, &a.DstLogin, &a.DstPassEnc, diff --git a/internal/store/emptylist_test.go b/internal/store/emptylist_test.go new file mode 100644 index 0000000..afb0cf8 --- /dev/null +++ b/internal/store/emptylist_test.go @@ -0,0 +1,38 @@ +package store + +import ( + "context" + "testing" +) + +// Empty lists must serialize as JSON [] (non-nil slice), not null, +// otherwise the frontend crashes on `.length`/`.map`. Regression for +// "Uncaught TypeError: can't access property length, n is null". +func TestListsNeverNilWhenEmpty(t *testing.T) { + s := testStore(t) + ctx := context.Background() + + eps, err := s.ListEndpoints(ctx) + if err != nil { + t.Fatalf("ListEndpoints: %v", err) + } + if eps == nil { + t.Fatal("ListEndpoints returned nil slice on empty DB (would serialize as null)") + } + + tasks, err := s.ListTasks(ctx) + if err != nil { + t.Fatalf("ListTasks: %v", err) + } + if tasks == nil { + t.Fatal("ListTasks returned nil slice on empty DB (would serialize as null)") + } + + accs, err := s.ListAccountsByTask(ctx, 999999) + if err != nil { + t.Fatalf("ListAccountsByTask: %v", err) + } + if accs == nil { + t.Fatal("ListAccountsByTask returned nil slice for no rows (would serialize as null)") + } +} diff --git a/internal/store/endpoints.go b/internal/store/endpoints.go index aa08f07..e0b26ad 100644 --- a/internal/store/endpoints.go +++ b/internal/store/endpoints.go @@ -34,7 +34,7 @@ func (s *Store) ListEndpoints(ctx context.Context) ([]Endpoint, error) { return nil, err } defer rows.Close() - var out []Endpoint + out := []Endpoint{} for rows.Next() { var e Endpoint if err := rows.Scan(&e.ID, &e.RoleLabel, &e.Host, &e.Port, &e.TLSMode); err != nil { diff --git a/internal/store/tasks.go b/internal/store/tasks.go index f4b13d3..c6e07ba 100644 --- a/internal/store/tasks.go +++ b/internal/store/tasks.go @@ -40,7 +40,7 @@ func (s *Store) ListTasks(ctx context.Context) ([]Task, error) { return nil, err } defer rows.Close() - var out []Task + out := []Task{} for rows.Next() { var t Task if err := rows.Scan(&t.ID, &t.Name, &t.SrcEndpointID, &t.DstEndpointID, &t.Status, &t.FolderMapping); err != nil { diff --git a/web/src/pages/Endpoints.tsx b/web/src/pages/Endpoints.tsx index 82c08a9..3f2d2c4 100644 --- a/web/src/pages/Endpoints.tsx +++ b/web/src/pages/Endpoints.tsx @@ -11,7 +11,7 @@ export function Endpoints() { function reload() { listEndpoints() - .then(setEndpoints) + .then((e) => setEndpoints(e ?? [])) .catch((e) => setError(String(e.message || e))) } diff --git a/web/src/pages/Tasks.tsx b/web/src/pages/Tasks.tsx index d534480..b972a70 100644 --- a/web/src/pages/Tasks.tsx +++ b/web/src/pages/Tasks.tsx @@ -13,13 +13,13 @@ export function Tasks() { function reload() { listTasks() - .then(setTasks) + .then((t) => setTasks(t ?? [])) .catch((e: unknown) => setError(e instanceof Error ? e.message : 'Failed to load tasks')) } useEffect(() => { reload() - listEndpoints().then(setEndpoints).catch(() => {}) + listEndpoints().then((e) => setEndpoints(e ?? [])).catch(() => {}) }, []) async function submit(e: FormEvent) {