diff --git a/docs/features/compose-doctor.mdx b/docs/features/compose-doctor.mdx index b12d04ee..66ddffc3 100644 --- a/docs/features/compose-doctor.mdx +++ b/docs/features/compose-doctor.mdx @@ -58,9 +58,9 @@ The summary card at the top of the Doctor tab reflects the overall outcome of th | **High risk** | Highest active finding is high risk | Amber card with a count of active findings by severity | | **Warning** | Highest active finding is a warning | Blue card with a count of active findings by severity | | **Info** | All active findings are informational | Muted card with a count of active findings by severity | -| **Acknowledged** | Every finding from the last run has been acknowledged | Muted card noting all findings are acknowledged | +| **All clear · findings acknowledged** | Model rendered; every finding from the last run has been acknowledged | Green card with "No active findings remain. One or more detected issues were reviewed and acknowledged by an authorized operator." | -The summary card also shows when the run happened and who triggered it: "ran 5 minutes ago by admin". If any findings are acknowledged, the line adds a count: "3 active (1 blocker · 2 warning) · 2 acknowledged". +The summary card also shows when the run happened and who triggered it: "ran 5 minutes ago by admin". When active findings remain alongside acknowledgements, the line adds a count: "3 active (1 blocker · 2 warning) · 2 acknowledged". A small colored dot appears on the **Doctor** tab label when the last run's active findings include a blocker (red) or a high-risk finding (amber). Click the **X** on the summary card to dismiss it; this also clears the tab dot. The dismissal is remembered per stack and node, and clears itself automatically the moment the finding set changes (a rule clearing, a new rule firing, or a severity change), so a stale dismissal can never hide a genuinely new problem. It never clears just by opening the tab or re-running preflight to the same result, and it does not sync across browsers or teammates: it is stored in your browser only. diff --git a/frontend/src/components/stack/PreflightPanel.test.tsx b/frontend/src/components/stack/PreflightPanel.test.tsx index 5406da48..962461f3 100644 --- a/frontend/src/components/stack/PreflightPanel.test.tsx +++ b/frontend/src/components/stack/PreflightPanel.test.tsx @@ -81,7 +81,65 @@ describe('PreflightPanel', () => { render(); const status = await screen.findByTestId('preflight-status'); expect(status).toHaveAttribute('data-status', 'pass'); - expect(status).toHaveTextContent(/all clear/i); + expect(status).toHaveTextContent('all clear'); + expect(status).not.toHaveTextContent(/findings acknowledged/i); + expect(status).toHaveTextContent('No issues found in the effective model.'); + }); + + it('renders all-clear · findings acknowledged when every finding is acknowledged', async () => { + vi.mocked(apiFetch).mockResolvedValue(jsonRes(report({ + status: 'pass', + acknowledgedCount: 1, + findings: [{ + ruleId: 'privileged', + severity: 'high', + title: 'Privileged container', + message: 'runs privileged', + service: 'web', + acknowledged: true, + }], + }))); + render(); + const status = await screen.findByTestId('preflight-status'); + expect(status).toHaveAttribute('data-status', 'pass'); + expect(status).toHaveTextContent('all clear · findings acknowledged'); + expect(status).toHaveTextContent( + 'No active findings remain. One or more detected issues were reviewed and acknowledged by an authorized operator.', + ); + expect(status.className).toContain('border-success/40'); + expect(status.className).toContain('bg-success/[0.06]'); + expect(status.className).toContain('text-success'); + expect(status.querySelector('svg.lucide-check')).not.toBeNull(); + expect(status.querySelector('svg.lucide-shield-check')).toBeNull(); + expect(screen.getByTestId('preflight-acknowledged-section')).toBeInTheDocument(); + }); + + it('keeps the severity summary when active findings remain alongside acknowledgements', async () => { + vi.mocked(apiFetch).mockResolvedValue(jsonRes(report({ + status: 'high', + highestSeverity: 'high', + acknowledgedCount: 1, + findings: [ + { ruleId: 'privileged', severity: 'high', title: 'Privileged container', message: 'runs privileged', service: 'web' }, + { + ruleId: 'image-latest', + severity: 'warning', + title: 'Image uses a moving tag', + message: 'latest tag', + service: 'web', + acknowledged: true, + }, + ], + }))); + render(); + const status = await screen.findByTestId('preflight-status'); + expect(status).toHaveAttribute('data-status', 'high'); + expect(status).toHaveTextContent('high risk'); + expect(status).not.toHaveTextContent(/all clear/i); + expect(status).toHaveTextContent('1 active'); + expect(status).toHaveTextContent('1 acknowledged'); + expect(screen.getByText('Privileged container')).toBeInTheDocument(); + expect(screen.getByTestId('preflight-acknowledged-section')).toBeInTheDocument(); }); it('keeps All Clear when only inherited-healthcheck notes remain', async () => { diff --git a/frontend/src/components/stack/PreflightPanel.tsx b/frontend/src/components/stack/PreflightPanel.tsx index 6a5662b8..df274e93 100644 --- a/frontend/src/components/stack/PreflightPanel.tsx +++ b/frontend/src/components/stack/PreflightPanel.tsx @@ -1,7 +1,7 @@ import { useEffect, useMemo, useState } from 'react'; import { Check, TriangleAlert, ShieldAlert, Info, RefreshCw, Stethoscope, X, - ChevronDown, ChevronRight, ShieldCheck, type LucideIcon, + ChevronDown, ChevronRight, type LucideIcon, } from 'lucide-react'; import { apiFetch } from '@/lib/api'; import { cn } from '@/lib/utils'; @@ -83,8 +83,16 @@ function summaryMeta( line: report.renderError ?? 'Sencho could not render the effective Compose model.', }; } - if (report.activeCount === 0 && report.acknowledgedCount === 0) { - return { label: 'all clear', icon: Check, tone: 'border-success/40 bg-success/[0.06] text-success', line: 'No issues found in the effective model.' }; + if (report.activeCount === 0) { + const acknowledgedOnly = report.acknowledgedCount > 0; + return { + label: acknowledgedOnly ? 'all clear · findings acknowledged' : 'all clear', + icon: Check, + tone: 'border-success/40 bg-success/[0.06] text-success', + line: acknowledgedOnly + ? 'No active findings remain. One or more detected issues were reviewed and acknowledged by an authorized operator.' + : 'No issues found in the effective model.', + }; } const meta = SEVERITY_META[report.activeHighestSeverity ?? 'info']; const activeParts = GROUP_ORDER @@ -95,7 +103,7 @@ function summaryMeta( const line = report.acknowledgedCount > 0 ? `${report.activeCount} active${activeParts ? ` (${activeParts})` : ''} · ${report.acknowledgedCount} acknowledged` : (activeParts || `${report.activeCount} active`); - return { label: report.activeCount === 0 ? 'acknowledged' : meta.label, icon: report.activeCount === 0 ? ShieldCheck : meta.icon, tone: report.activeCount === 0 ? 'border-muted bg-card/40 text-stat-subtitle' : meta.tone, line }; + return { label: meta.label, icon: meta.icon, tone: meta.tone, line }; } function expiryComboboxOptions(finding: PreflightFinding): ComboboxOption[] {