mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-22 08:06:42 +00:00
feat: add posture reasons and review queue to Security overview (#1462)
Add structured posture reasons derived alongside the posture verb in securityPosture.ts so the masthead and Overview tab can answer why the page is red, what to do first, and what clears it. Backend: - derivePostureReasons() returns blocker, review, and info reasons from the same SecurityPostureFacts used by deriveSecurityPosture() - deriveSecurityPosture() depends on derivePostureReasons() internally - Exposure split: public exposure with KEV, fixable, or EPSS >= 0.1 is a blocker; exposure without any of those is a review item - Fully dismissed exposed images produce no posture reason - postureReasons and primaryAction returned by the overview endpoint Frontend: - ReviewQueueCard on the Overview tab with per-row CTAs for blockers - Action summary in masthead subtitle and desktop primary CTA button - Card gated on posture not being Unknown - Backward compatible with older remote nodes
This commit is contained in:
@@ -214,8 +214,15 @@ export function SecurityView({ activeTab, onTabChange, headerActions }: Security
|
||||
|
||||
// The scanner-detections disclaimer rides as an info affordance next to the
|
||||
// scanned-images count rather than a standing caption below the masthead.
|
||||
// When posture is Action needed, the subtitle leads with the action count and
|
||||
// top blocker labels so the operator sees "why red" without opening the page.
|
||||
const blockers = overview?.postureReasons?.filter((r) => r.severity === 'blocker') ?? [];
|
||||
const actionSummary = overview?.posture === 'Action needed' && blockers.length > 0
|
||||
? `${blockers.length} action${blockers.length === 1 ? '' : 's'}: ${blockers.slice(0, 2).map((r) => r.label.toLowerCase()).join(', ')} · `
|
||||
: null;
|
||||
const subtitle = overview ? (
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
{actionSummary ? <span>{actionSummary}</span> : null}
|
||||
<span>
|
||||
{overview.scannedImages} {overview.scannedImages === 1 ? 'image' : 'images'} scanned · scanner {overview.scanner.available ? 'ready' : 'not installed'}
|
||||
</span>
|
||||
@@ -363,7 +370,17 @@ export function SecurityView({ activeTab, onTabChange, headerActions }: Security
|
||||
{ label: 'HIGH', value: String(overview.high), tone: overview.high > 0 ? 'warn' : 'value' },
|
||||
{ label: 'LAST SCAN', value: overview.lastSuccessfulScanAt ? formatTimeAgo(overview.lastSuccessfulScanAt) : 'never', tone: 'subtitle' },
|
||||
] : undefined}
|
||||
/>
|
||||
>
|
||||
{overview?.posture === 'Action needed' && overview.primaryAction ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onTabChange(overview.primaryAction!.targetTab)}
|
||||
className="text-xs font-medium text-brand hover:underline whitespace-nowrap"
|
||||
>
|
||||
{overview.primaryAction.label} →
|
||||
</button>
|
||||
) : null}
|
||||
</PageMasthead>
|
||||
|
||||
<Tabs value={activeTab} onValueChange={(v) => onTabChange(v as SecurityTab)}>
|
||||
<TabsList className="mb-4">
|
||||
|
||||
Reference in New Issue
Block a user