From 5cc2f61be0fdb139070660e1cdee826929cd2886 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Sat, 9 May 2026 10:47:21 +0100 Subject: [PATCH] Surface will_fix_later remind-at on dismiss confirm and dismissed rows Slice 18 made will_fix_later a real operational commitment server-side, but the new RemindAt field stayed invisible to operators until the reminder fired a week later. This wires it through the API surface and renders it where the operator decides and where they later revisit. UnifiedFinding (Go and TS) and the Patrol Finding TS shape now carry RemindAt / remind_at; router.go and AddFromAI mirror it like the other user-feedback fields. FindingsPanel previews "Pulse will stay quiet for 7 days, then surface again on " on the dismiss confirmation panel before the operator confirms, badges dismissed-as-will_fix_later rows with "Reminding " in amber, and adds explanatory copy for the other two dismissal reasons so all three paths feel deliberate rather than undifferentiated. --- .../v6/internal/subsystems/agent-lifecycle.md | 8 ++ .../v6/internal/subsystems/ai-runtime.md | 9 +++ .../v6/internal/subsystems/api-contracts.md | 17 ++++- .../subsystems/patrol-intelligence.md | 12 +++ .../subsystems/performance-and-scalability.md | 6 ++ .../internal/subsystems/storage-recovery.md | 7 ++ .../src/api/__tests__/patrol.test.ts | 34 +++++++++ frontend-modern/src/api/ai.ts | 4 + frontend-modern/src/api/patrol.ts | 4 + .../src/components/AI/FindingsPanel.tsx | 41 ++++++++++ .../AI/__tests__/FindingsPanel.test.ts | 30 ++++++++ .../stores/__tests__/aiIntelligence.test.ts | 63 +++++++++++++++ frontend-modern/src/stores/aiIntelligence.ts | 6 ++ internal/ai/unified/alerts.go | 11 +++ internal/ai/unified/alerts_test.go | 76 +++++++++++++++++++ internal/api/router.go | 2 + 16 files changed, 329 insertions(+), 1 deletion(-) diff --git a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md index 155aa0dbb..fbfb60106 100644 --- a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md +++ b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md @@ -917,6 +917,14 @@ profile and assignment columns, but embedded table framing must route through ## Current State +Patrol-finding to unified-finding mirroring in `internal/api/router.go` +also keeps the will_fix_later wake-up deadline (`Finding.RemindAt`) +intact across restarts. Both the live wire-up callback and the +persistence-recovery resync must copy `f.RemindAt` onto the unified +finding so the operator's commitment survives a reboot or process +restart instead of silently lapsing into the canonical findings store +without being mirrored on the API surface. + Linux agent privilege hardening is now part of the installer/runtime contract. The supported full-telemetry systemd agent may still run as `root`, but `cmd/pulse-agent/main.go` must bind health/metrics to loopback by default, diff --git a/docs/release-control/v6/internal/subsystems/ai-runtime.md b/docs/release-control/v6/internal/subsystems/ai-runtime.md index 7544ab490..57788c313 100644 --- a/docs/release-control/v6/internal/subsystems/ai-runtime.md +++ b/docs/release-control/v6/internal/subsystems/ai-runtime.md @@ -1545,3 +1545,12 @@ clearing the dismissal and emitting a `reminded` lifecycle event, and the `dismiss_finding` LLM tool response must communicate the remind-at date so Patrol's conversational explanations stay aligned with the persisted behavior. +The unified-finding mirror in `internal/ai/unified/alerts.go` also carries +that same `RemindAt` field so the API surface preserves the will_fix_later +wake-up deadline across the canonical findings store and the read model. +The `AddFromAI` dedup-merge path must mirror `RemindAt` onto the existing +record (including clearing it when a remind-at wake or undismiss has +already cleared the dismissal in the canonical store), and the TS API +clients in `frontend-modern/src/api/patrol.ts` and +`frontend-modern/src/api/ai.ts` must round-trip the `remind_at` field +verbatim so the operator surface can preview and badge the deadline. diff --git a/docs/release-control/v6/internal/subsystems/api-contracts.md b/docs/release-control/v6/internal/subsystems/api-contracts.md index 442d07f9e..ab130d891 100644 --- a/docs/release-control/v6/internal/subsystems/api-contracts.md +++ b/docs/release-control/v6/internal/subsystems/api-contracts.md @@ -1013,7 +1013,22 @@ the canonical monitored-system blocked payload. same overwrite pattern as `description`, `impact`, and `recommendation`, and the Finding to UnifiedFinding conversion in `internal/api/router.go` must copy `f.PreviousResolvedFixSummary` - alongside the other operator-facing strings + alongside the other operator-facing strings. + The same shape also carries an optional `remind_at` timestamp (ISO + 8601) on both `UnifiedFindingRecord` and Patrol `Finding` shapes. It + is populated only when `dismissed_reason === 'will_fix_later'` and + represents the wake-up deadline at which the next re-detection clears + the dismissal — the operator-facing half of the canonical + `Finding.RemindAt` contract. The store normalizer promotes it to + camelCase `remindAt` on `UnifiedFinding`, the Finding to + UnifiedFinding conversion in `internal/api/router.go` must copy + `f.RemindAt`, and the AddFromAI update branch must mirror it + (including clearing on remind-at wake or undismiss) so the dedup + surface stays consistent with the canonical findings store. The + Findings panel must visibly preview the deadline at dismiss-confirm + time and badge dismissed-as-will_fix_later rows with the pending + remind-at, otherwise the new behavior is invisible until the + reminder fires and the Assistant finding-context request contract, so `/api/ai/chat` payloads carrying `finding_id` may hydrate a structured investigation summary from the unified finding, but raw proposed-fix commands must stay diff --git a/docs/release-control/v6/internal/subsystems/patrol-intelligence.md b/docs/release-control/v6/internal/subsystems/patrol-intelligence.md index 92dceed37..207864e43 100644 --- a/docs/release-control/v6/internal/subsystems/patrol-intelligence.md +++ b/docs/release-control/v6/internal/subsystems/patrol-intelligence.md @@ -1087,3 +1087,15 @@ escalation still wakes any dismissed finding regardless of reason. The `dismiss_finding` LLM tool response surfaces the remind-at date in plain language so Patrol's own conversational explanations stay aligned with this contract. +Patrol's findings panel must also surface that commitment to operators on +the canonical Patrol surface, not only inside the LLM tool response. The +inline dismiss confirmation must preview the will_fix_later remind-at +deadline before the operator confirms (and explain the +`expected_behavior` and `not_an_issue` paths so all three feel +deliberate), and dismissed-as-`will_fix_later` rows must show the pending +`Reminding ` badge in an amber tone so the operator can see their +own commitment without expanding the row. The store-level +`UnifiedFinding.remindAt` field is the canonical source for both +surfaces; render code reads it from the store, the store normalizer +promotes it from `remind_at` on both the unified and patrol-direct +fetch paths. diff --git a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md index 54fc6085e..66f178b2d 100644 --- a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md +++ b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md @@ -1245,3 +1245,9 @@ the canonical owners for desktop and mobile workload column sizing. Global CSS must not reintroduce competing `.workload-table [data-workload-col=…]` width rules or `min-width: max-content` fallbacks that can blow the table out horizontally on Firefox or other desktop browsers. +The same `internal/api/router.go` payload boundary also keeps the +will_fix_later remind-at deadline scoped to a single optional pointer +(`*time.Time`) per finding on both API write paths, so adding the +operational-commitment field does not regress the unified-findings hot +path with a per-row allocation when the dismissal reason is anything +other than `will_fix_later`. diff --git a/docs/release-control/v6/internal/subsystems/storage-recovery.md b/docs/release-control/v6/internal/subsystems/storage-recovery.md index 794231910..abe8ee651 100644 --- a/docs/release-control/v6/internal/subsystems/storage-recovery.md +++ b/docs/release-control/v6/internal/subsystems/storage-recovery.md @@ -947,6 +947,13 @@ bypass the API fail-closed execution gate. ## Current State +The patrol findings-recovery sync in `internal/api/router.go` also keeps +the will_fix_later wake-up deadline alongside the rest of the finding's +durable state when re-hydrating findings from disk into the unified +store. Persisted `Finding.RemindAt` values must round-trip through that +recovery path so an operator commitment recorded before a process +restart is not silently dropped when findings reload. + `StorageSummary.tsx`, `StoragePageSummary.tsx`, and `useStoragePageSummary.ts` now surface `poolsDegraded` and `disksFailing` health indicators alongside pool/disk counts. `RecoverySummary.tsx` gains an aggregate health-state summary diff --git a/frontend-modern/src/api/__tests__/patrol.test.ts b/frontend-modern/src/api/__tests__/patrol.test.ts index 0fb07a888..4120e2ff3 100644 --- a/frontend-modern/src/api/__tests__/patrol.test.ts +++ b/frontend-modern/src/api/__tests__/patrol.test.ts @@ -11,6 +11,7 @@ import { getPatrolRunHistory, getPatrolRunHistoryWithToolCalls, getPatrolRunWithToolCalls, + type Finding as PatrolFinding, } from '@/api/patrol'; import { apiFetchJSON } from '@/utils/apiClient'; @@ -310,4 +311,37 @@ describe('patrol api', () => { }, }); }); + + it('round-trips remind_at on dismissed-as-will_fix_later patrol findings', async () => { + // The backend treats will_fix_later as an operator commitment with a + // wake-up deadline (Finding.RemindAt, default 7 days). The TS API client + // must mirror remind_at verbatim so the surface can preview the deadline + // at dismiss-confirm time and badge the dismissed row with the pending + // reminder. Without this round-trip, the deadline is invisible to the + // operator until the reminder fires a week later. + const willFixLater: PatrolFinding = { + id: 'finding-wfl', + severity: 'warning', + category: 'reliability', + resource_id: 'vm-101', + resource_name: 'db-01', + resource_type: 'vm', + title: 'Disk pressure', + description: 'Pulse will surface this again on the deadline', + detected_at: '2026-05-09T10:00:00Z', + last_seen_at: '2026-05-09T10:05:00Z', + auto_resolved: false, + times_raised: 1, + suppressed: false, + investigation_attempts: 0, + dismissed_reason: 'will_fix_later', + remind_at: '2026-05-16T10:00:00Z', + }; + apiFetchJSONMock.mockResolvedValueOnce([willFixLater] as any); + + const findings = await getPatrolFindings(); + expect(findings).toHaveLength(1); + expect(findings[0]?.dismissed_reason).toBe('will_fix_later'); + expect(findings[0]?.remind_at).toBe('2026-05-16T10:00:00Z'); + }); }); diff --git a/frontend-modern/src/api/ai.ts b/frontend-modern/src/api/ai.ts index bc0708bff..ffe250a9a 100644 --- a/frontend-modern/src/api/ai.ts +++ b/frontend-modern/src/api/ai.ts @@ -463,6 +463,10 @@ export interface UnifiedFindingRecord { user_note?: string; suppressed?: boolean; times_raised?: number; + // remind_at carries the will_fix_later wake-up deadline. When dismissed_reason + // === 'will_fix_later', the finding stays quiet until this timestamp passes; + // afterwards the next re-detection clears the dismissal. + remind_at?: string; status?: string; } diff --git a/frontend-modern/src/api/patrol.ts b/frontend-modern/src/api/patrol.ts index b62eecf27..e06e8dfae 100644 --- a/frontend-modern/src/api/patrol.ts +++ b/frontend-modern/src/api/patrol.ts @@ -43,6 +43,10 @@ export interface Finding { user_note?: string; times_raised: number; suppressed: boolean; + // remind_at carries the will_fix_later wake-up deadline; the finding stays + // quiet on re-detection until this timestamp passes, then the next + // re-detection clears the dismissal and emits a "reminded" lifecycle event. + remind_at?: string; // Investigation fields (Patrol Autonomy) investigation_session_id?: string; investigation_status?: InvestigationStatus; diff --git a/frontend-modern/src/components/AI/FindingsPanel.tsx b/frontend-modern/src/components/AI/FindingsPanel.tsx index 46347a274..e1b59fcef 100644 --- a/frontend-modern/src/components/AI/FindingsPanel.tsx +++ b/frontend-modern/src/components/AI/FindingsPanel.tsx @@ -569,6 +569,23 @@ export const FindingsPanel: Component = (props) => { const formatTime = (isoString: string) => formatRelativeTime(isoString, { compact: true }); + // Mirror the backend's DefaultWillFixLaterRemindAfter (7 days) so the dismiss + // confirmation panel can preview the remind-at date before the operator + // confirms. Kept as a helper instead of a constant so the date stays current + // each time the panel re-renders. + const formatWillFixLaterRemindDate = (): string => { + const remindAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); + try { + return remindAt.toLocaleDateString(undefined, { + weekday: 'short', + month: 'short', + day: 'numeric', + }); + } catch { + return remindAt.toISOString().slice(0, 10); + } + }; + // Get meaningful resolution reason based on finding type const getResolutionReason = (finding: UnifiedFinding): string => { const resolvedTime = finding.resolvedAt ? formatTime(finding.resolvedAt) : ''; @@ -751,6 +768,11 @@ export const FindingsPanel: Component = (props) => { {' · '}({formatIdentifierLabel(finding.dismissedReason)}) + + + {' · '}Reminding {formatTime(finding.remindAt!)} + + {' · '}snoozed until {formatTime(finding.snoozedUntil!)} @@ -1151,6 +1173,25 @@ export const FindingsPanel: Component = (props) => { Dismiss as: {formatIdentifierLabel(dismissReason())} + +

+ Pulse will stay quiet on this for 7 days, then surface it again on{' '} + {formatWillFixLaterRemindDate()}{' '} + if it is still happening. +

+
+ +

+ Pulse will keep this finding visible as acknowledged but won't re-notify + you for it. Severity escalation will still wake it. +

+
+ +

+ Pulse will permanently suppress this and similar findings on this resource. + Use "Expected" or "Later" if the detection itself is correct. +

+