mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-24 17:36:42 +00:00
fix: reconcile sticky update indicators with Anatomy preview (#1698)
* fix: reconcile sticky update indicators with Anatomy preview Sidebar, Updates filter, and Fleet treated retained partial/failed scanner has_update as confirmed. Keep raw state for retention/notifications, project confirmed-only to APIs, show distinct incomplete indicators, and clear sticky rows only after an authoritative-negative preview. Closes #1685 * test: align sidebar truncate E2E with failed-over-retained precedence Purple update indicators are confirmed-only; hasUpdate with a failed check correctly shows the failed trailing icon. * fix: clear confirmed update rows on authoritative-negative preview Address audit SF-1/SF-2/SF-3: observation-watermark clears for older ok+has_update rows (DB + memory gens), Fleet checkability parity with backend not_checkable, and Updates chip confirmed-only regressions. * fix: tombstone equal-generation writers on preview clear Advance the per-stack write generation when clearing at the observation watermark so a scanner reserved before preview cannot recreate the row after an authoritative-negative reconcile. * fix: clear sticky updates with digest and tag preview parity Share detection across scanner and preview, keep GET read-only with POST reconcile, gate Apply to digest and rebuild updates, and invalidate the hub fleet cache on clear. * test: set digestUpdate on auto-update checkImage mocks Scheduler and execute routes now gate Compose on digest drift; fixtures that expect an apply need digestUpdate so they exercise the update path. * fix: clear unused lint errors on sticky update branch Drop unused partial helper and fleet invalidate import; keep the CacheService inflight self-ref as let with an eslint exception so tsc stays green. * fix: use inflight holder for CacheService prefer-const Keep generation-aware ownership without a let self-reference that fights ESLint and tsc.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { GitBranch, Loader2, AlertCircle } from 'lucide-react';
|
||||
import type { CheckStatus } from '@/types/imageUpdates';
|
||||
import { isConfirmedImageUpdate } from '@/types/imageUpdates';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import type { Label } from '@/components/label-types';
|
||||
import { cn } from '@/lib/utils';
|
||||
@@ -23,8 +24,8 @@ interface StackRowProps {
|
||||
hasUpdate: boolean;
|
||||
/** Outdated service names for the update tooltip; empty keeps the generic label. */
|
||||
outdatedServices?: string[];
|
||||
// Last image-update check outcome. 'failed' surfaces a muted "couldn't check"
|
||||
// indicator so an undeterminable check is not mistaken for "up to date".
|
||||
// Last image-update check outcome. Incomplete/failed checks with hasUpdate
|
||||
// use a distinct indicator so they are not mistaken for a confirmed update.
|
||||
checkStatus?: CheckStatus;
|
||||
lastError?: string;
|
||||
hasGitPending: boolean;
|
||||
@@ -48,6 +49,36 @@ function RowTooltip({ trigger, label }: { trigger: ReactNode; label: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
function appendErrorDetail(base: string, lastError?: string): string {
|
||||
if (!lastError) return base;
|
||||
return `${base} ${lastError}`;
|
||||
}
|
||||
|
||||
function partialUpdateTooltip(hasUpdate: boolean, lastError?: string): string {
|
||||
if (hasUpdate) {
|
||||
// Neutral copy: partial + hasUpdate can mean newly detected OR retained;
|
||||
// provenance is not persisted on the wire.
|
||||
return appendErrorDetail(
|
||||
'The last check was incomplete; an update was detected or retained, but the full stack could not be verified.',
|
||||
lastError,
|
||||
);
|
||||
}
|
||||
return appendErrorDetail(
|
||||
'The last image-update check was incomplete; update status could not be fully verified.',
|
||||
lastError,
|
||||
);
|
||||
}
|
||||
|
||||
function failedCheckTooltip(hasUpdate: boolean, lastError?: string): string {
|
||||
if (hasUpdate) {
|
||||
return appendErrorDetail(
|
||||
'Previous update status retained; the last check failed.',
|
||||
lastError,
|
||||
);
|
||||
}
|
||||
return lastError ? `Update check failed: ${lastError}` : 'Update check failed';
|
||||
}
|
||||
|
||||
export function StackRow(props: StackRowProps) {
|
||||
const {
|
||||
file, displayName, status, running, total, isBusy, isActive,
|
||||
@@ -55,6 +86,10 @@ export function StackRow(props: StackRowProps) {
|
||||
bulkMode = false, isSelected = false, onToggleSelect,
|
||||
} = props;
|
||||
|
||||
const confirmedUpdate = isConfirmedImageUpdate({ hasUpdate, checkStatus });
|
||||
const partialIncomplete = checkStatus === 'partial';
|
||||
const failedCheck = checkStatus === 'failed';
|
||||
|
||||
const handleClick = () => {
|
||||
if (bulkMode) onToggleSelect?.(file);
|
||||
else onSelect(file);
|
||||
@@ -106,9 +141,9 @@ export function StackRow(props: StackRowProps) {
|
||||
{/* Stack name */}
|
||||
<span className="flex-1 truncate font-mono text-sm min-w-0">{displayName}</span>
|
||||
|
||||
{/* Fixed trailing icon slot: update dot > check-failed > git pending */}
|
||||
{/* Trailing: confirmed update > partial incomplete > failed > git pending */}
|
||||
<span className="w-3.5 h-3.5 flex items-center justify-center shrink-0" data-testid="stack-row-trailing">
|
||||
{hasUpdate ? (
|
||||
{confirmedUpdate ? (
|
||||
<RowTooltip
|
||||
trigger={(
|
||||
<span className="relative inline-flex w-2 h-2" data-testid="stack-trailing-update">
|
||||
@@ -118,10 +153,15 @@ export function StackRow(props: StackRowProps) {
|
||||
)}
|
||||
label={updateAvailableLabel(outdatedServices)}
|
||||
/>
|
||||
) : checkStatus === 'failed' ? (
|
||||
) : partialIncomplete ? (
|
||||
<RowTooltip
|
||||
trigger={<span data-testid="stack-trailing-check-partial"><AlertCircle className="w-3 h-3 text-warning-foreground/80" strokeWidth={1.5} /></span>}
|
||||
label={partialUpdateTooltip(hasUpdate, lastError)}
|
||||
/>
|
||||
) : failedCheck ? (
|
||||
<RowTooltip
|
||||
trigger={<span data-testid="stack-trailing-check-failed"><AlertCircle className="w-3 h-3 text-muted-foreground/70" strokeWidth={1.5} /></span>}
|
||||
label={lastError ? `Update check failed: ${lastError}` : 'Update check failed'}
|
||||
label={failedCheckTooltip(hasUpdate, lastError)}
|
||||
/>
|
||||
) : hasGitPending ? (
|
||||
<RowTooltip
|
||||
|
||||
Reference in New Issue
Block a user