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.
This commit is contained in:
Anso
2026-07-28 14:26:46 -04:00
committed by GitHub
parent 543e4ef256
commit b0b423b234
42 changed files with 1770 additions and 168 deletions
@@ -4,6 +4,8 @@ import { PreDeployScanDialog } from '../stack/PreDeployScanDialog';
import { MissingExternalNetworksDialog } from '../stack/MissingExternalNetworksDialog';
import { UpdateReadinessDialog } from '../stack/UpdateReadinessDialog';
import { SelfStackProtectedDialog } from '../stack/SelfStackProtectedDialog';
import { LocalUpdateConfirmDialog } from '../FleetView/LocalUpdateConfirmDialog';
import { ReconnectingOverlay } from '../FleetView/ReconnectingOverlay';
import { DeleteStackDialog } from './DeleteStackDialog';
import { TakeDownStackDialog } from './TakeDownStackDialog';
import { UnsavedChangesDialog } from './UnsavedChangesDialog';
@@ -12,9 +14,11 @@ import { GitSourcePanel } from '../stack/GitSourcePanel';
import { LogViewer } from '../LogViewer';
import { VulnerabilityScanSheet } from '../VulnerabilityScanSheet';
import { ComposeDiffPreviewDialog } from '@/components/ComposeDiffPreviewDialog';
import { resolveComposeDiffActionLabel } from '@/components/resolveComposeDiffActionLabel';
import type { OverlayState } from './hooks/useOverlayState';
import type { StackActionsHook } from './hooks/useStackActions';
import type { PermissionAction } from '@/context/AuthContext';
import type { useComposeReapplyAction } from '../FleetView/hooks/useComposeReapplyAction';
interface ShellOverlaysProps {
overlayState: OverlayState;
@@ -27,6 +31,8 @@ interface ShellOverlaysProps {
gitSourceOpen: boolean;
setGitSourceOpen: (open: boolean) => void;
canSelfUpdate: boolean;
composeReapply: ReturnType<typeof useComposeReapplyAction>;
canSaveAndReapply: boolean;
canOfferVolumeRemoval: boolean;
onOpenFleetNodeUpdates: () => void;
}
@@ -42,6 +48,8 @@ export function ShellOverlays({
gitSourceOpen,
setGitSourceOpen,
canSelfUpdate,
composeReapply,
canSaveAndReapply,
canOfferVolumeRemoval,
onOpenFleetNodeUpdates,
}: ShellOverlaysProps) {
@@ -57,6 +65,7 @@ export function ShellOverlays({
preDeployAdvisory,
missingExternalNetworks, setMissingExternalNetworks,
selfStackProtectedOpen, setSelfStackProtectedOpen,
composeReapplyCapture, setComposeReapplyCapture,
stackMisconfigScanId, setStackMisconfigScanId,
diffPreview, setDiffPreview, diffPreviewConfirming, setDiffPreviewConfirming,
} = overlayState;
@@ -85,6 +94,32 @@ export function ShellOverlays({
onOpenFleetUpdates={onOpenFleetNodeUpdates}
/>
<LocalUpdateConfirmDialog
open={composeReapplyCapture !== null}
mode="reapply"
nodeType={composeReapplyCapture?.nodeType ?? 'local'}
onOpenChange={(open) => {
if (!open) setComposeReapplyCapture(null);
}}
onConfirm={() => {
const capture = composeReapplyCapture;
setComposeReapplyCapture(null);
if (!capture || composeReapply.dispatching) return;
void composeReapply.runReapply({
nodeId: capture.nodeId,
type: capture.nodeType,
name: capture.nodeName,
});
}}
/>
{composeReapply.reconnecting && (
<ReconnectingOverlay
preUpdateStartedAt={composeReapply.preUpdateStartedAt}
mode="reapply"
/>
)}
<UnsavedChangesDialog
open={!!pendingUnsavedLoad || !!pendingLeaveAction}
onCancel={stackActions.cancelPendingUnsavedLoad}
@@ -197,7 +232,7 @@ export function ShellOverlays({
language={diffPreview?.language ?? 'yaml'}
original={diffPreview?.original ?? ''}
modified={diffPreview?.modified ?? ''}
actionLabel={diffPreview?.mode === 'save-and-deploy' ? 'Save & deploy' : 'Save'}
actionLabel={resolveComposeDiffActionLabel(diffPreview?.mode, canSaveAndReapply)}
confirming={diffPreviewConfirming}
isDarkMode={isDarkMode}
onConfirm={async () => {