Files
sencho/frontend/src/components/FleetView/ReconnectingOverlay.tsx
T
Anso b0b423b234 feat(fleet): reapply Compose configuration without a version update (#1716)
* feat(fleet): reapply Compose configuration without a version update

Add a distinct Fleet Reapply configuration path so Compose-managed nodes can recreate Sencho from the current on-disk project when already up to date, without pulling or rewriting the image reference.

* fix(fleet): confirm remote reapply and close concurrent tracker race

Require confirmation for remote compose reapply, and lock dispatch before the remote POST so a second request cannot overwrite a successful in-flight tracker.

* fix(ui): icon-only Reapply control so Up to date badge can breathe

Collapse the Node updates Reapply label into a tooltip so the status pill no longer wraps in the Status column.

* feat(editor): Save & Reapply self-stack via fleet compose reapply (#1726)

* feat(editor): Save & Reapply self-stack via fleet compose reapply

Eligible admins can apply on-disk Compose edits to Sencho's own stack from the editor using the same confirm, dispatch, and reconnect path as Fleet Node Updates.

* fix(editor): gate Save & Reapply label to self-stack only

Ordinary stacks were labeled Save & Reapply whenever the node was
reapply-eligible. Require the selected file to be the self-stack for the
toolbar label and diff confirm CTA.

* fix(ui): move compose diff action label helper out of dialog module

Keep ComposeDiffPreviewDialog component-only so react-refresh Fast Refresh
lint passes after the Save and reapply stacked merge.
2026-07-28 14:26:46 -04:00

97 lines
4.5 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;
/** Distinguishes version-update copy from compose reapply copy. */
mode?: 'update' | 'reapply';
}
// 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,
mode = 'update',
}: ReconnectingOverlayProps) {
const [elapsed, setElapsed] = useState(0);
const timedOut = elapsed >= RECONNECT_TIMEOUT_SECONDS;
const isReapply = mode === 'reapply';
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]" data-sn-glass="overlay">
<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">
{isReapply
? 'Sencho has not come back online yet. The recreate may still be finishing. Reload to check, or inspect the Docker host if it persists.'
: '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">
{isReapply ? 'Reapplying configuration...' : 'Updating Sencho...'}
</h2>
<p className="text-sm text-muted-foreground max-w-sm">
{isReapply
? 'The server is recreating from its current Compose configuration and restarting. This page will reload automatically.'
: 'The server is pulling the update and restarting. This page will reload automatically.'}
</p>
<p className="text-xs text-muted-foreground tabular-nums">{elapsed}s elapsed</p>
</>
)}
</div>
</div>
);
}