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. +

+