fix(deploy): Checkpoints — preserve STOPPED history, fix filter + placement
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 2m4s
CI / docker (push) Successful in 1m15s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 41s

- Backend: rename deleteTerminalByAppAndEnvironment → deleteFailedByAppAndEnvironment.
  STOPPED rows were being wiped on every redeploy, so Checkpoints was always empty.
  Now only FAILED rows are pruned; STOPPED deployments are retained as restorable
  checkpoints (they still carry deployed_config_snapshot from their RUNNING window).
- UI filter: any deployment with a snapshot is a checkpoint (was RUNNING|DEGRADED only,
  which excluded the main case — the previous blue/green deployment now in STOPPED).
- UI placement: Checkpoints disclosure now renders inside IdentitySection, matching
  the design spec.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-23 10:26:46 +02:00
parent 007597715a
commit c6aef5ab35
11 changed files with 49 additions and 30 deletions

View File

@@ -15,15 +15,11 @@ export function Checkpoints({ deployments, versions, currentDeploymentId, onRest
const [open, setOpen] = useState(false);
const versionMap = new Map(versions.map((v) => [v.id, v]));
// Deployments that reached COMPLETE stage and captured a snapshot (RUNNING or DEGRADED).
// Exclude the currently-running one.
// Any deployment that captured a snapshot is restorable — that covers RUNNING,
// DEGRADED, and STOPPED (blue/green swap previous, user-stopped). Exclude the
// currently-running one and anything without a snapshot (FAILED, STARTING).
const checkpoints = deployments
.filter(
(d) =>
d.deployedAt &&
(d.status === 'RUNNING' || d.status === 'DEGRADED') &&
d.id !== currentDeploymentId,
)
.filter((d) => d.deployedConfigSnapshot && d.id !== currentDeploymentId)
.sort((a, b) => (b.deployedAt ?? '').localeCompare(a.deployedAt ?? ''));
return (

View File

@@ -1,4 +1,4 @@
import { useRef } from 'react';
import { useRef, type ReactNode } from 'react';
import { SectionHeader, Input, MonoText, Button } from '@cameleer/design-system';
import type { App, AppVersion } from '../../../api/queries/admin/apps';
import type { Environment } from '../../../api/queries/admin/environments';
@@ -25,11 +25,12 @@ interface IdentitySectionProps {
stagedJar: File | null;
onStagedJarChange: (file: File | null) => void;
deploying: boolean;
children?: ReactNode;
}
export function IdentitySection({
mode, environment, app, currentVersion,
name, onNameChange, stagedJar, onStagedJarChange, deploying,
name, onNameChange, stagedJar, onStagedJarChange, deploying, children,
}: IdentitySectionProps) {
const fileInputRef = useRef<HTMLInputElement>(null);
const slug = app?.slug ?? slugify(name);
@@ -109,6 +110,7 @@ export function IdentitySection({
)}
</div>
</div>
{children}
</div>
);
}

View File

@@ -467,7 +467,7 @@ export default function AppDeploymentPage() {
</div>
</div>
{/* ── Identity & Artifact ── */}
{/* ── Identity & Artifact (with Checkpoints for deployed apps) ── */}
<IdentitySection
mode={mode}
environment={env}
@@ -478,17 +478,16 @@ export default function AppDeploymentPage() {
stagedJar={stagedJar}
onStagedJarChange={setStagedJar}
deploying={deploymentInProgress}
/>
{/* ── Checkpoints (deployed apps only) ── */}
{app && (
<Checkpoints
deployments={deployments}
versions={versions}
currentDeploymentId={currentDeployment?.id ?? null}
onRestore={handleRestore}
/>
)}
>
{app && (
<Checkpoints
deployments={deployments}
versions={versions}
currentDeploymentId={currentDeployment?.id ?? null}
onRestore={handleRestore}
/>
)}
</IdentitySection>
{/* ── Config tabs ── */}
<div className={styles.tabGroup}>