mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-09 18:32:52 +00:00
feat(stacks): surface post-deploy scan attempt status (#1198)
triggerPostDeployScan was fire-and-forget. When Trivy was missing on a
node, when the registry refused the digest lookup, or when a single
image scan threw, the failure went to console.error and the user
never learned. Open the security tab later, see stale data, no
indicator that the scan even tried.
Backend:
- New stack_scan_attempts table (node_id, stack_name, status,
attempted_at, error_message). One row per stack; latest attempt
overwrites the previous one.
- DatabaseService gains recordStackScanAttempt /
getStackScanAttempt / clearStackScanAttempts. Status is one of
'ok' | 'partial' | 'failed' | 'skipped'.
- triggerPostDeployScan in helpers/policyGate.ts now records every
exit path: 'skipped' when Trivy is unavailable or no images to
scan; 'failed' when container enumeration or all images fail;
'partial' when some images scan and others fail; 'ok' on full
success.
- New GET /api/stacks/:name/scan-status returns { status,
attemptedAt, errorMessage } or { status: null } when never tried.
- DELETE /:stackName cleanup chain now clears the row alongside
the existing update-status / auto-update cleanups.
Frontend:
- StackAnatomyPanel fetches /scan-status on stackName change.
- Renders a small warning strip below the update banner when
status !== 'ok' (failed / partial / skipped). Hidden when status
is 'ok' or unknown (never attempted). Title attribute carries
the full error message for hover inspection.
Cross-feature note: the audit doc flagged this as M-6 with a
coordination note for the pending Security feature audit. The
schema kept intentionally narrow (one row per stack, simple
status enum) so the Security audit can extend it (richer history,
per-image-row breakdown, etc.) without a destructive migration.
Resolves M-6 from the stack-management audit.
This commit is contained in:
@@ -249,6 +249,11 @@ export default function StackAnatomyPanel({
|
||||
|
||||
const [gitSource, setGitSource] = useState<GitSourceInfo | null>(null);
|
||||
const [updatePreview, setUpdatePreview] = useState<UpdatePreview | null>(null);
|
||||
const [scanStatus, setScanStatus] = useState<{
|
||||
status: 'ok' | 'partial' | 'failed' | 'skipped' | null;
|
||||
attemptedAt?: number;
|
||||
errorMessage?: string | null;
|
||||
} | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
@@ -290,6 +295,25 @@ export default function StackAnatomyPanel({
|
||||
return () => { cancelled = true; };
|
||||
}, [stackName]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
const run = async () => {
|
||||
try {
|
||||
const res = await apiFetch(`/stacks/${stackName}/scan-status`);
|
||||
if (cancelled) return;
|
||||
if (res.ok) {
|
||||
setScanStatus(await res.json());
|
||||
} else {
|
||||
setScanStatus(null);
|
||||
}
|
||||
} catch {
|
||||
if (!cancelled) setScanStatus(null);
|
||||
}
|
||||
};
|
||||
void run();
|
||||
return () => { cancelled = true; };
|
||||
}, [stackName]);
|
||||
|
||||
const networkName = anatomy && anatomy.networks.length > 0
|
||||
? anatomy.networks[0]
|
||||
: `${stackName}_default`;
|
||||
@@ -510,6 +534,21 @@ export default function StackAnatomyPanel({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{scanStatus && scanStatus.status && scanStatus.status !== 'ok' && (
|
||||
<div
|
||||
className="mx-3 my-2 flex items-start gap-2 rounded-md border border-warning/40 bg-warning/[0.06] px-2 py-1.5 text-[11px] text-warning"
|
||||
role="status"
|
||||
title={scanStatus.errorMessage ?? undefined}
|
||||
>
|
||||
<span className="font-mono text-[9px] uppercase tracking-wide shrink-0 mt-0.5">scan</span>
|
||||
<span className="flex-1">
|
||||
{scanStatus.status === 'failed' && 'Last post-deploy scan failed.'}
|
||||
{scanStatus.status === 'partial' && 'Last post-deploy scan partially failed.'}
|
||||
{scanStatus.status === 'skipped' && 'Post-deploy scan did not run.'}
|
||||
{scanStatus.errorMessage ? ` ${scanStatus.errorMessage}` : ''}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{anatomy && anatomy.services.length > 0 && (
|
||||
<div className="border-t border-muted px-3 py-2 flex items-center justify-between">
|
||||
|
||||
Reference in New Issue
Block a user