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:
+13
-5
@@ -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 ---------------------------------------------------------
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
} from '$lib/api';
|
||||
import { getTheme, setTheme, type Theme } from '$lib/theme.svelte';
|
||||
import { getDensity, setDensity, type Density } from '$lib/density.svelte';
|
||||
import Skeleton from '$lib/components/ui/Skeleton.svelte';
|
||||
|
||||
let loading = $state(true);
|
||||
let features = $state<AuthFeatures>({ sso_configured: false, oidc_enabled: false, saml_enabled: false });
|
||||
@@ -122,7 +123,7 @@
|
||||
selectedTargets = new Set();
|
||||
try {
|
||||
const result = await listRetentionHosts(hours);
|
||||
hosts = result.hosts;
|
||||
hosts = result.hosts ?? [];
|
||||
} catch (e) {
|
||||
hostsError = e instanceof Error ? e.message : String(e);
|
||||
hosts = [];
|
||||
@@ -304,7 +305,12 @@
|
||||
{#if hostsError}<p class="error">{hostsError}</p>{/if}
|
||||
|
||||
{#if hostsLoading}
|
||||
<p class="muted">Loading hosts…</p>
|
||||
<div class="host-picker-loading" aria-busy="true" aria-live="polite">
|
||||
<span class="sr-only">Loading hosts…</span>
|
||||
<Skeleton height="2.25rem" />
|
||||
<Skeleton height="2.25rem" />
|
||||
<Skeleton height="2.25rem" />
|
||||
</div>
|
||||
{:else if hosts.length === 0}
|
||||
<p class="note">No hosts have logs older than this.</p>
|
||||
{:else}
|
||||
@@ -459,6 +465,18 @@
|
||||
.muted {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
.host-picker-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0 0 0 0);
|
||||
}
|
||||
.note {
|
||||
color: var(--color-text-muted);
|
||||
font-size: var(--text-sm);
|
||||
|
||||
Reference in New Issue
Block a user