mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-11 19:26:56 +00:00
b0b423b234
* 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.
97 lines
4.5 KiB
TypeScript
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>
|
|
);
|
|
}
|