Fix log retention Settings section getting stuck on "Loading hosts…"

Root cause: handleHosts and partitionTargets both declared their result
slices with `var`, so an empty result (no logs old enough yet, or every
requested target blocked by a floor) marshaled to JSON `null` instead of
`[]` on fields without `omitempty`. The frontend's `.length` access on
that `null` threw mid-render, which is why this shipped with the spinner
stuck forever instead of the empty state ever painting -- production is
freshly deployed with nothing yet older than the default 30-day cutoff,
so every user hit this on first load.

Also replaces the static "Loading hosts…" text with the existing
shimmer Skeleton component for real visual feedback, and adds `?? []`
fallbacks in api.ts as a second line of defense.
This commit is contained in:
2026-08-21 17:08:50 -07:00
parent 653e4efa76
commit 9e21ea17bb
4 changed files with 111 additions and 12 deletions
+13 -5
View File
@@ -521,24 +521,32 @@ export type LogRetentionDeleteResult = {
blocked_targets?: BlockedTarget[];
};
// The `?? []` fallbacks below are a second line of defense, matching
// listUsers' pattern above -- api/logretention/handler.go now always
// sends `[]` rather than `null` for these fields (see its partitionTargets
// and handleHosts comments), but a null response should degrade to an
// empty list here rather than crash the page's `.length` accesses if
// that guarantee ever regresses.
export function listRetentionHosts(olderThanHours: number): Promise<RetentionHostsResult> {
return request(`/logs/retention/hosts?older_than_hours=${olderThanHours}`, { credentials: 'include' });
return request<RetentionHostsResult>(`/logs/retention/hosts?older_than_hours=${olderThanHours}`, {
credentials: 'include'
}).then((r) => ({ ...r, hosts: r.hosts ?? [] }));
}
export function previewLogDeletion(olderThanHours: number, targets: HostService[]): Promise<LogRetentionPreview> {
return request('/logs/retention/preview', {
return request<LogRetentionPreview>('/logs/retention/preview', {
method: 'POST',
credentials: 'include',
body: JSON.stringify({ older_than_hours: olderThanHours, targets })
});
}).then((r) => ({ ...r, targets: r.targets ?? [] }));
}
export function deleteLogsOlderThan(olderThanHours: number, targets: HostService[]): Promise<LogRetentionDeleteResult> {
return request('/logs/retention/delete', {
return request<LogRetentionDeleteResult>('/logs/retention/delete', {
method: 'POST',
credentials: 'include',
body: JSON.stringify({ older_than_hours: olderThanHours, targets })
});
}).then((r) => ({ ...r, deleted_targets: r.deleted_targets ?? [] }));
}
// --- alerting ---------------------------------------------------------