mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-13 04:06:59 +00:00
d8f73f8203
* feat(fleet): show stack-label filtering in Fleet View on every tier Stack labels and their assignments are a Community feature: the per-node label reads are available to any authenticated user and are already used in the per-node Stacks view. Fleet View, however, only fetched the fleet-wide label palette and per-stack chips when the instance was on a paid tier, so a Community user who had labelled their stacks saw no label dots on node cards and no Tags filter in the Overview toolbar. Drop the paid gate on the fleet label fetch so the palette, the per-stack chips, and the Tags filter render for everyone who has labels. Node-level tag aggregation (used for topology grouping) stays paid and is unchanged. * fix(fleet): surface update-status failures and soften the reconnect timeout Three reliability fixes in the Fleet View update path: - The fleet update-status poll swallowed fetch errors in an empty catch, so a failing poll left a silently stale table with no breadcrumb. It now logs the failure (both thrown errors and non-ok HTTP responses) without toasting on every tick, and keeps the last-known statuses. - The local-update reconnecting overlay declared "Update timed out" after five minutes, which falsely reported failure when a large image pull simply ran longer than the reconnect window. It now shows a non-failure "Taking longer than expected" state with a "Reload to check" action, and the timeout is a named constant that mirrors the backend update timeout. - The fleet overview fan-out already logged a node that failed to report; the update-status and update-all fan-outs now log the rejected node and reason too instead of discarding it. * test(fleet): backfill Fleet View hook, component, and update-tracker coverage Adds unit and component coverage for the previously untested Fleet View surface: all six Overview hooks (overview, update-status, polling cadence, preferences, fleet labels, node labels), the NodeCard, OverviewTab, OverviewToolbar, NodeUpdatesSheet, UpdateStatusBadge, and ReconnectingOverlay components, and the FleetUpdateTrackerService state transitions. * test(fleet): assert update-status poll preserves last-known statuses on failure Adds an explicit case that seeds statuses from a successful poll, then fails the next poll, and verifies the table keeps the seeded statuses (and logs without toasting) rather than relying on the implementation implicitly.
85 lines
3.8 KiB
TypeScript
85 lines
3.8 KiB
TypeScript
import { useState, useEffect } from 'react';
|
|
import { AlertTriangle, Loader2 } from 'lucide-react';
|
|
import { Button } from '@/components/ui/button';
|
|
|
|
interface ReconnectingOverlayProps {
|
|
/** Gateway boot timestamp captured pre-update. Null falls back to offline-then-online detection. */
|
|
preUpdateStartedAt: number | null;
|
|
}
|
|
|
|
// Mirrors the backend UPDATE_TIMEOUT_MS (5 minutes) in routes/fleet.ts. Past
|
|
// this point we stop asserting the update is in flight and hand control back to
|
|
// the operator, but we do not claim failure: a large image pull can legitimately
|
|
// run longer than the auto-reload budget.
|
|
const RECONNECT_TIMEOUT_SECONDS = 5 * 60;
|
|
|
|
export function ReconnectingOverlay({ preUpdateStartedAt }: ReconnectingOverlayProps) {
|
|
const [elapsed, setElapsed] = useState(0);
|
|
const timedOut = elapsed >= RECONNECT_TIMEOUT_SECONDS;
|
|
|
|
useEffect(() => {
|
|
const timer = setInterval(() => setElapsed(s => s + 1), 1000);
|
|
return () => clearInterval(timer);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
if (timedOut) return;
|
|
let sawOffline = false;
|
|
const poll = setInterval(async () => {
|
|
try {
|
|
const res = await fetch('/api/health');
|
|
if (!res.ok) {
|
|
sawOffline = true;
|
|
return;
|
|
}
|
|
const data = await res.json().catch(() => null) as { startedAt?: number } | null;
|
|
const currentStartedAt = typeof data?.startedAt === 'number' ? data.startedAt : null;
|
|
|
|
if (preUpdateStartedAt !== null && currentStartedAt !== null) {
|
|
if (currentStartedAt !== preUpdateStartedAt) {
|
|
window.location.reload();
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Fallback when we don't know the original startedAt: require an offline
|
|
// response first so we don't reload while the old process is still mid-pull.
|
|
if (sawOffline) {
|
|
window.location.reload();
|
|
}
|
|
} catch {
|
|
sawOffline = true;
|
|
}
|
|
}, 3000);
|
|
return () => clearInterval(poll);
|
|
}, [timedOut, preUpdateStartedAt]);
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-[10px] backdrop-saturate-[1.15]">
|
|
<div className="text-center space-y-4">
|
|
{timedOut ? (
|
|
<>
|
|
<AlertTriangle className="w-10 h-10 text-warning mx-auto" strokeWidth={1.5} />
|
|
<h2 className="text-lg font-medium">Taking longer than expected</h2>
|
|
<p className="text-sm text-muted-foreground max-w-sm">
|
|
Sencho has not come back online yet. A large image pull can take a while, so the update may still be finishing. Reload to check, or inspect the Docker host if it persists.
|
|
</p>
|
|
<Button variant="outline" size="sm" onClick={() => window.location.reload()}>
|
|
Reload to check
|
|
</Button>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Loader2 className="w-10 h-10 text-muted-foreground animate-spin mx-auto" strokeWidth={1.5} />
|
|
<h2 className="text-lg font-medium">Updating Sencho...</h2>
|
|
<p className="text-sm text-muted-foreground max-w-sm">
|
|
The server is pulling the latest image and restarting. This page will reload automatically.
|
|
</p>
|
|
<p className="text-xs text-muted-foreground tabular-nums">{elapsed}s elapsed</p>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|