From 421d1e5423e5b83b7923ea8c3b133ad520cd5f60 Mon Sep 17 00:00:00 2001 From: rcourtman <8825017+rcourtman@users.noreply.github.com> Date: Wed, 7 Oct 2026 16:17:50 +0100 Subject: [PATCH 1/2] Remove test-only Patrol summary presenters The Patrol summary card was deleted in 27ba85ddc6 (2026-06-23), but most of the presenters in utils/patrolSummaryPresentation.ts stayed exported. A TypeScript import graph rooted at src/index.tsx (static and dynamic imports, re-exports) shows reachable code imports only two of them: getPatrolRecencyPresentation for the header recency line and getPatrolVerificationPresentation for the findings empty state. Apart from two positive pins removed here, ?raw and readFileSync guards reference the rest only in negative not.toContain checks, which stay. getPatrolAssessmentPresentation, getPatrolAssessmentShellPresentation and getPatrolSummaryMetricState lost their last caller with the card. getPatrolSummaryPresentation, getPatrolScoreChipLabel and getPatrolAssessmentAction had lost theirs in May, the PATROL_NO_ISSUES_LABEL export in March, and getPatrolNoIssuesPresentation and getPatrolCompactAssessmentLabel never had a production caller. They go along with their types and the private helpers that only they used, which a noUnusedLocals check found (repeated until it reported nothing). The same card deletion left getPatrolConfigurationFailureInlineDetails in PatrolIntelligenceHeader.tsx formatting an inline save-failure state the header no longer renders, so it goes too. 88 presenter test cases and 2 header cases go with them. v8 coverage before and after, matched by function and source text, shows no statement or branch in the surviving code losing coverage. The JavaScript output is unchanged. Of 187 production dist files, 93 are byte-identical and 93 differ only in chunk-hash references. The CSS drops one rule, .bg-emerald-50/60, which only the deleted assessment shell's class string generated. The bundle-size check passes. The header recency line and the findings empty state read the same before and after, at desktop and phone widths. The contracts described the removed presenters as live. patrol-intelligence, frontend-primitives and api-contracts now say that no shared presenter derives an assessment, shell, score chip or next step today, and they keep those rules as conditions for any returning surface. They also correct claims that no longer match the code: - the recency line says "checked N resources" for any completed run and never "verified", and labels the latest completed run of any type "Last check"; - the findings empty state treats any coverage factor as incomplete coverage, even after a clean full run, and the health summary picks that empty state's title, not the workspace heading; - a save that is not ready raises a warning toast, not an inline "Patrol control needs attention" state. --- .../v6/internal/subsystems/api-contracts.md | 40 +- .../subsystems/frontend-primitives.md | 46 +- .../subsystems/patrol-intelligence.md | 205 ++--- .../9b9e1c752ea2726b.json | 39 + .../patrol/PatrolIntelligenceHeader.tsx | 20 - .../PatrolIntelligenceHeader.test.ts | 33 - .../frontendResourceTypeBoundaries.test.ts | 6 - ...patrolSummaryPresentation.coverage.test.ts | 720 +----------------- .../patrolSummaryPresentation.test.ts | 562 +------------- .../src/utils/patrolSummaryPresentation.ts | 560 +------------- 10 files changed, 203 insertions(+), 2028 deletions(-) create mode 100644 frontend-modern/browser-verification/9b9e1c752ea2726b.json diff --git a/docs/release-control/v6/internal/subsystems/api-contracts.md b/docs/release-control/v6/internal/subsystems/api-contracts.md index 4be492e8c1..f6943a66d2 100644 --- a/docs/release-control/v6/internal/subsystems/api-contracts.md +++ b/docs/release-control/v6/internal/subsystems/api-contracts.md @@ -3192,7 +3192,7 @@ a new API state machine, queue contract, or verification-accounting field. regressions keep the current findings empty state out of green all-clear copy even when the active finding count is zero and the latest summary score is otherwise healthy - and the Patrol Current work assessment behavior, so the same governed AI summary contract decides whether the workspace leads with verified health, issues detected, coverage incomplete, or another attention state instead of letting count-only page fragments emit a stale `No issues found` conclusion + and the Patrol Current work empty-state behavior, so once runtime state (running, blocked, disabled, unavailable) and stale coverage have taken precedence, the governed AI summary's overall health, together with run history, decides whether the current-findings empty state reads `No current issues`, `Check needed`, or `Patrol needs review` instead of letting count-only page fragments emit a stale `No issues found` conclusion and the Patrol Current work copy boundary, so the API-owned health, finding, run, and control facts remain semantic inputs while browser-visible empty and descriptive text stays operator-facing: what will appear there, @@ -3250,8 +3250,8 @@ a new API state machine, queue contract, or verification-accounting field. without adding a new API payload or page-level proof surface; those process details remain available in expanded context, selected run records, Assistant handoff context, or API diagnostics when they are actually needed - and the Patrol verification summary derived from run history, so the page also states whether recent Patrol evidence came from a successful full patrol or only from scoped/erroring runs instead of leaving verification scope implicit - and the same-day activity-mix explanation derived from that governed run history, so when a recent full patrol is followed by alert-triggered or anomaly-triggered scoped work the verification surface can explain the mix directly instead of reconstructing it from page-local timing heuristics + and the Patrol verification posture derived from run history (`getPatrolVerificationPresentation`), which today only withholds the current-findings empty state's all-clear when the latest completed full patrol ended with errors or only targeted or follow-up runs completed; the page shows no aggregate verification-posture summary (expanded run-history entries still show each run's own scope and coverage), and any surface that adds one must take it from that run-history posture rather than page-local heuristics + and the same-day activity-mix label that posture computes from the same governed run history, which no surface renders today; one that does must explain a recent full patrol followed by alert-triggered or anomaly-triggered scoped work from that label instead of reconstructing the mix from page-local timing heuristics and the Patrol status recency split, so `last_patrol_at` remains reserved for completed full Patrol sweeps while scoped runs and verification checks advance `last_activity_at` without claiming a fresh full-estate verification pass and the monitor-context Patrol coverage posture boundary, so Proxmox overview and other monitor-first launch pages must not consume API-owned @@ -3458,9 +3458,9 @@ a new API state machine, queue contract, or verification-accounting field. 17. Keep Patrol intelligence summary transport semantics single-voiced: the canonical overall-health payload and Patrol run-history payload together must support one primary assessment plus one explicit verification explanation, and frontend consumers must not need to derive a second compact assessment or verification verdict row from the same payloads beneath the primary assessment strip. That same transport split supports Patrol assessment and action metadata for any consumer without adding another API field (the Patrol page renders - no assessment strip today): the frontend summary - contract derives compact state from existing overall-health, run-history, - active finding, runtime, and pending-approval facts, so the API remains the + no assessment strip today): a consumer that needs compact state derives + it from existing overall-health, run-history, active finding, runtime, and + pending-approval facts (no shared frontend presenter does so today), so the API remains the source of facts while the configured LLM owns next-step reasoning. Those action references map back to existing API-backed Patrol controls and approval/finding filters; summary transport must not become a new execution @@ -4161,17 +4161,19 @@ the authoritative analysis outcome. plumbing. The same page-header recency line also reads `PatrolRecencyPresentation.resourcesCheckedLabel` — derived by - `getPatrolRecencyPresentation` from - `PatrolRunRecord.resources_checked` and the latest completed run outcome — + `getPatrolRecencyPresentation` from the latest completed run's + `PatrolRunRecord.resources_checked` — so the operator sees coverage alongside recency without needing a - new API surface. The label stays optional when coverage is zero, says - verified only for successful full patrols, and uses neutral checked wording - for errored full patrols or scoped activity. - The Patrol Current work empty-state copy uses that same run-history-backed - coverage truth when reconciling AI summary coverage factors: a successful - full run with non-zero `resources_checked` suppresses stale - coverage-incomplete wording, while scoped, missing, zero-coverage, or errored - runs still allow the coverage caveat to surface. + new API surface. The label stays optional when coverage is zero and reads + `checked N resources` for any completed run, whatever its type or outcome; + it never says verified. + The Patrol current-findings empty state does not reconcile AI summary + coverage factors against run history: outside runtime and stale-coverage + states, any `coverage` factor in `overall_health` withholds the all-clear, + even after a successful full run + with non-zero `resources_checked`, and run history adds its own caveat when + the latest completed full run ended with errors or only targeted or + follow-up runs completed. 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 action commands must stay @@ -4297,10 +4299,10 @@ the authoritative analysis outcome. control level and paid Patrol settings currently allowed by runtime entitlements. 11. Treat Patrol summary supporting metrics as readouts, not reinterpretations: when frontend consumers derive cards such as active findings, criticals, warnings, or fixes from the canonical payloads, those cards must stay numeric and must not synthesize new assessment labels like `Issues detected` or verification labels like `Partial verification` beneath the primary summary contract -12. Treat active Patrol runtime transport as compatible with factual activity surfaces: when the runtime is currently running, frontend consumers may surface in-progress activity context, but they must not replace the activity strip with a second assessment verdict derived from runtime state alone -13. Treat Patrol recency as a singular transport-driven fact: once header metadata, verification copy, or the findings footer already present the governed Patrol timing context, frontend summary consumers must not derive an extra timing pill from the same payloads inside any primary summary card +12. Treat active Patrol runtime transport as compatible with factual activity surfaces: when the runtime is currently running, frontend consumers may surface in-progress activity context, but they must not add a second assessment verdict derived from runtime state alone +13. Treat Patrol recency as a singular transport-driven fact: the page-header recency line presents the governed Patrol timing context, so frontend summary consumers must not derive an extra timing pill from the same payloads inside any primary summary card that returns 14. Treat Patrol findings counts as a singular supporting surface as well: when the summary shell already exposes count cards for active findings, warnings, criticals, and fixes, the primary assessment card must not repeat those same payload-derived counts as secondary badges -15. Treat Patrol schedule and recency as header-owned metadata on the main Patrol page: findings empty-state consumers should not receive or restate `next_patrol_at`, `last_patrol_at`, `last_activity_at`, or interval timing once those transport fields are already presented by the primary header and verification shell +15. Treat Patrol schedule and recency as header-owned metadata on the main Patrol page: findings empty-state consumers should not receive or restate `next_patrol_at`, `last_patrol_at`, `last_activity_at`, or interval timing once those transport fields are already presented by the page header 16. Keep recovery payload filters canonical across `/api/recovery/rollups`, `/api/recovery/points`, `/api/recovery/series`, and `/api/recovery/facets`: when `internal/api/recovery_handlers.go` adds a governed recovery filter or display field such as provider-neutral `itemType`, the same normalized transport must land across all four endpoints and the contract tests must pin both outbound payload shape and accepted query aliases in the same slice 17. Keep recovery platform-query vocabulary canonical across that same `/api/recovery/*` surface: operator-facing transport must emit `platform` as the canonical query field, accepted legacy `provider` aliases must remain compatibility-only input, and `internal/api/contract_test.go` must pin that fallback behavior in the same slice as any handler change 18. Keep recovery payload platform vocabulary canonical across that same `/api/recovery/*` surface: point payloads must expose `platform`, rollup payloads must expose `platforms`, and any compatibility `provider` / `providers` aliases must remain secondary fallback fields rather than replacing the shared response model diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index b848351479..7707b99374 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -4499,12 +4499,12 @@ The recency line beside the header actions also renders coverage alongside time when the canonical `getPatrolRecencyPresentation` helper returns `resourcesCheckedLabel` from the latest completed run. Render code must gate on `` (truthy) so -zero-coverage runs do not surface a misleading coverage phrase, failed or -scoped runs use neutral checked wording, and only successful full patrols read -as verified. Any Patrol assessment shell must pass the same run-history facts -into `getPatrolAssessmentPresentation` so assessment coverage caveats do not -contradict the header's verified full-run coverage state; the page renders no -assessment shell today. +zero-coverage runs do not surface a misleading coverage phrase. The label +reads `checked N resources` for every completed run with a positive count +(full, targeted, follow-up, or errored) and never says `verified`. The page +renders no assessment shell today; if one returns, its coverage caveats must +come from the same run-history facts so they do not contradict the coverage +the header line shows. The same header row may surface `Trigger status` when `getPatrolTriggerStatusSummary` returns a runtime-relevant value from the Patrol status payload. That text is page-owned operational metadata inside the @@ -4916,10 +4916,10 @@ That same summary shell should also keep the shared Pulse surface neutral: severity belongs in compact accents, inline readouts, and badges rather than turning the whole assessment into a tinted warning banner, nested card, or hero-style block that breaks the surrounding operator workflow. -That same summary-shell rule also applies to timing metadata: if the header, -verification card, or findings footer already presents the governed Patrol -activity timestamp, the summary chip row must not add another recency badge -that competes with those owned timing surfaces. +That same summary-shell rule also applies to timing metadata: the header +recency line already presents the governed Patrol activity timestamp, so a +returning summary chip row must not add another recency badge that competes +with it. The same default-readout rule applies to collapsed Patrol issue rows: `MetadataBadge` may carry severity, recurrence, and active decision/work states, but the default Patrol page must not render raw lifecycle or investigation @@ -5294,8 +5294,8 @@ than hardcoded gray palettes; non-gray typed tones may retain their state color vocabulary so success, warning, danger, info, and platform-adjacent metadata do not collapse into visually identical chips. Patrol run-history labels follow this state-badge boundary: -Patrol may derive the status label and typed variant in -`patrolRunPresentation.ts` or `patrolSummaryPresentation.ts`, but +Patrol derives the status label and typed variant in +`patrolRunPresentation.ts`, but `RunHistoryEntry.tsx` must render visible state badges through `StatusIndicatorBadge` rather than `runStatus.badgeClass` or a local span. The shared segmented selector now follows that same owner split. @@ -6800,19 +6800,19 @@ same-day scoped follow-up work, that summary shell should also carry a compact activity-mix explanation rather than forcing operators to infer why Patrol looked busy from a second competing status band. That explanation belongs on the verification surface itself when operators are -reconciling `Recently verified` copy against same-day scoped Patrol bursts; the +reconciling `Recently checked` copy against same-day scoped Patrol bursts; the supporting activity context may complement the readout, but it is not sufficient as the only explanation path. -That same shell rule also owns Patrol recency labels. Shared Patrol header and -status-shell surfaces must keep `Last full patrol` tied only to the full-sweep -transport fact and use `Last activity` for scoped or verification work instead -of collapsing both timestamps back into a generic `Last run` label. Coverage -phrases on those recency surfaces must come from the Patrol recency presenter -instead of hardcoding verified wording in the shell. -That same run-history ownership applies to assessment caveats: Patrol summary -shells should not present `Recent coverage is incomplete` when the shared -recency/verification helpers already prove a successful full patrol with -non-zero resource coverage. +Patrol recency labels and coverage phrases come from the Patrol recency +presenter (`getPatrolRecencyPresentation`), never from hardcoded shell copy. +The header labels the latest completed run of any type `Last check`; without +run history it uses `Last check` for the `last_patrol_at` full-sweep transport +fact and `Last activity` when `last_activity_at` is newer, and it never +collapses them into a generic `Last run` label. Its coverage phrase reads +`checked N resources` and never says `verified`. +If a summary shell returns, it should not present `Recent coverage is +incomplete` when run history shows a successful full patrol with non-zero +resource coverage. That same Patrol shell ownership includes refresh affordance state: `frontend-modern/src/features/patrol/usePatrolIntelligenceState.ts` must keep operator refresh controls generation-aware, timeout-bounded, and separate from diff --git a/docs/release-control/v6/internal/subsystems/patrol-intelligence.md b/docs/release-control/v6/internal/subsystems/patrol-intelligence.md index 79374829db..92f205d675 100644 --- a/docs/release-control/v6/internal/subsystems/patrol-intelligence.md +++ b/docs/release-control/v6/internal/subsystems/patrol-intelligence.md @@ -827,11 +827,12 @@ clear`, `Found N new issues`, `Fixed N issues`, `N issues still open`, or and raw tool traces are forensic context and must stay secondary to what Patrol did. 2. Keep Patrol-specific copy and badge logic inside the governed Patrol presentation helpers instead of page-local branches - Patrol assessment copy must not present an all-clear health prediction while - active Patrol findings or Patrol runtime issues are still present. The - canonical summary helper owns that conflict resolution so the visible - assessment title, description, and compact metrics all speak from the same - current findings state. Patrol-owned runtime issues must stay + Patrol renders no assessment copy today. If it returns, it must not present + an all-clear health prediction while active Patrol findings or Patrol + runtime issues are still present, and one shared presenter must own that + conflict resolution so the visible assessment title, description, and + compact metrics all speak from the same current findings state. + Patrol-owned runtime issues must stay distinct from infrastructure findings in assessment copy rather than being described as infrastructure warning findings about Patrol itself. Historical Patrol trust regressions must also suppress a green all-clear in @@ -842,13 +843,24 @@ clear`, `Found N new issues`, `Fixed N issues`, `N issues still open`, or tolerance and otherwise allows two configured Patrol intervals before warning, so deliberately slower schedules are not mislabeled while an old default-schedule result cannot remain green indefinitely. - Assessment coverage caveats must also reconcile against current run-history - proof: a stale coverage factor or prediction must not claim recent coverage - is incomplete when the latest completed full Patrol run successfully checked - real resources. - Recency coverage copy must also come from the shared presentation helper: - use verified wording only for successful full patrols, and use neutral - checked wording for failed full patrols or scoped activity. + No assessment readout renders today, so nothing reconciles a coverage + caveat against run history: once runtime states (running, blocked, + disabled, unavailable) and stale coverage have taken precedence, the + current-findings empty state withholds the all-clear (`Check needed`) + whenever the overall-health summary carries any + `coverage` factor, including the backend `Recent Patrol errors` factor that + can outlast a successful full Patrol run until three consecutive clean full + runs lead the coverage window or the erroring runs leave it + (`summarizeRecentPatrolCoverage` in `internal/ai/intelligence.go`), and + whenever the latest completed full run ended with errors or only targeted or + follow-up runs completed. If an assessment readout returns, its coverage + caveat must reconcile against current run-history proof: a stale coverage + factor or prediction must not claim recent coverage is incomplete when the + latest completed full Patrol run successfully checked real resources. + Recency coverage copy comes from the shared presentation helper + (`getPatrolRecencyPresentation`): `checked N resources` for the latest + completed run with a positive count, whatever its type or error state. It + never says `verified`. 3. Update this contract whenever a new Patrol-specific page, store, helper, or presentation component becomes canonical runtime surface area 4. Keep retired hosted-model and trial-like Patrol acquisition copy out of the normal self-hosted GA app. Patrol may parse legacy transport fields, but @@ -1571,12 +1583,20 @@ compatible `handoff_metadata.kind=patrol_configuration_failure` plus only the runtime-failure boolean needed for drawer/session presentation, so they restore as a Patrol control issue without carrying raw provider, credential, command, or retry payloads into the browser. -Successful provider-model saves that return -`patrol_readiness.status=not_ready` are still Patrol control issues, not silent -successes: the Patrol popover must keep the saved provider/model visible, -render a `Patrol control needs attention` inline state with the returned -readiness cause and summary; any Assistant handoff from that state must -describe a Patrol control issue rather than a save failure. +Successful Pulse Intelligence settings saves whose returned +`patrol_readiness.status` is `not_ready` or `warning` are still Patrol control +issues, not silent successes: Settings > Provider & Models, Patrol, Assistant +and Service context keep the saved non-secret values in the form and raise a +warning +toast (` settings saved, but Patrol is not ready` or `... is degraded`) +with the readiness summary, provider and model instead of a success toast +(`getAISettingsPatrolReadinessSaveMessage` in +`frontend-modern/src/components/Settings/useAISettingsState.ts`). A Patrol +on/off toggle save whose response reports `not_ready` raises the fixed warning +`Patrol setting was saved, but Patrol is not ready to run.` in either +direction, because readiness is evaluated independently of `patrol_enabled`. +Patrol renders no inline control-issue state or Assistant handoff for either +case. A separate browser proof in `tests/integration/tests/78-monitor-first-patrol-workbench.spec.ts` keeps the authenticated launch workbench monitor-first. When the runtime has monitored @@ -1675,55 +1695,49 @@ must not show Pro trial CTAs, upgrade links, paid helper copy, or a plan-lock upsell banner in the default Patrol workflow unless hosted mode, an explicit commercial handoff, or an active entitlement makes those actions relevant. That degraded empty-state copy must also interpret the finding state rather -than simply replaying the primary assessment sentence verbatim: when coverage +than replaying an `overall_health.prediction` sentence verbatim: when coverage is incomplete, the findings panel should tell the operator to run Patrol to complete coverage before trusting the all-clear. The page must not duplicate the assessment prediction as a second independent status surface above current issues. -Secondary metric strips must not render `No issues found` when the same -governed overall-health summary says coverage is incomplete or health still -requires attention; compact assessment derivation belongs to the shared -`frontend-modern/src/utils/patrolSummaryPresentation.ts` helper for Assistant, -history, and explicit context consumers rather than the default Patrol page. -Any explicit context assessment shell must also stay inside the shared Pulse -card language. It should use the same neutral `bg-surface` page-card base as -adjacent Pulse surfaces, carrying severity through compact header accents, icon -treatments, and badges instead of tinting an entire full-width assessment panel -as a one-off warning banner. -That supporting score chip must also avoid overstating infrastructure truth. -When the current state is dominated by incomplete coverage or Patrol-owned -runtime failures, the chip should read as an `Assessment` grade rather than an -`Health` grade; `Health` belongs to verified healthy infrastructure states. -That same helper also owns explicit assessment explanations. Assessment -consumers must not pair an `Issues detected` headline with a raw coverage-only -`overall_health.prediction` sentence from a separate source; when active -findings and incomplete verification are both true, the assessment should -describe both in one canonical message. -That same assessment contract must also distinguish Patrol-owned runtime -findings from infrastructure findings. When the only active Patrol findings are -synthetic Patrol service/runtime conditions such as the `ai-service` -provider-credit failure, the top assessment should read as a Patrol runtime -issue rather than implying infrastructure issues were detected across the -estate. When there is exactly one active Patrol runtime finding, that same -assessment copy should name the concrete runtime failure, such as -`Provider billing or quota issue`, instead of reducing the state to a generic count of -runtime findings. -That primary assessment must also expose a single visible recommended next -step derived from the same Patrol summary contract, not a page-local helper. -`frontend-modern/src/utils/patrolSummaryPresentation.ts` owns that decision -from pending governed approvals, Patrol runtime issues, active infrastructure -findings, verification posture, and the current assessment tone. Pending -approvals take priority over general triage, coverage-incomplete states must -ask for full verification before any all-clear claim, runtime impairment must -point the operator back to restoring Patrol visibility, and verified healthy -states should fall back to continued scheduled monitoring rather than -inviting generic Assistant chat. -When that recommendation has an immediate governed operator path, the same -helper must also declare the bounded action kind. The summary card may map -those action kinds only to existing Patrol controls: run a full Patrol, -review approval-scoped findings, review active findings, or open Patrol -provider settings. It must not -invent page-local remediation, approval, or execution authority from +No shared presenter derives a compact assessment, assessment shell, score +chip, assessment explanation, or recommended next step today. The helpers that +did (`getPatrolAssessmentPresentation`, `getPatrolAssessmentShellPresentation`, +`getPatrolScoreChipLabel`, `getPatrolCompactAssessmentLabel`, +`getPatrolSummaryMetricState`, `getPatrolAssessmentAction` and their siblings) +have had no production caller since the Patrol summary card was deleted +(several lost theirs months earlier) and are gone; +`frontend-modern/src/utils/patrolSummaryPresentation.ts` now owns only the +header recency line (`getPatrolRecencyPresentation`) and the run-history +verification posture the current-findings empty state reads +(`getPatrolVerificationPresentation`). If a summary card, metric strip, or +assessment readout returns, it must derive its state from one shared +Patrol-owned presenter rather than page-local logic, and keep these rules. +A secondary metric strip must not render `No issues found` while the governed +overall-health summary says coverage is incomplete or health still requires +attention. An assessment shell stays inside the shared Pulse card language: +the neutral `bg-surface` page-card base, with severity carried by compact +header accents, icon treatments, and badges rather than a tinted full-width +warning banner. A supporting score chip reads as an `Assessment` grade rather +than a `Health` grade when incomplete coverage or Patrol-owned runtime failures +dominate; `Health` belongs to verified healthy infrastructure states. An +`Issues detected` headline is never paired with a raw coverage-only +`overall_health.prediction` sentence from a separate source; active findings +and incomplete verification are described together in one message. When the +only active Patrol findings are synthetic Patrol service/runtime conditions +such as the `ai-service` provider-credit failure, the readout names a Patrol +runtime issue rather than implying infrastructure issues across the estate, +and with exactly one such finding it names the concrete failure, such as +`Provider billing or quota issue`. Any recommended next step comes from that +same presenter: pending approvals take priority over general triage, +coverage-incomplete states ask for a full check before any all-clear, runtime +impairment points the operator back to restoring Patrol visibility, and +healthy states fall back to continued scheduled monitoring rather than +inviting generic Assistant chat. When that recommendation has an immediate +governed operator path, the presenter declares a bounded action kind that maps +only to existing Patrol controls (run a full Patrol, review approval-scoped +findings, review active findings, or open Patrol provider settings), never to +page-local remediation, approval, or execution authority invented from recommendation text. If a summary card returns, that same runtime-owned assessment must expose the fix path directly. When the primary Patrol issue is a Patrol runtime/provider @@ -1801,8 +1815,8 @@ dismiss, resolve, or suppress controls for them. The correct operator path is to fix Patrol provider configuration in Pulse Intelligence > Provider & Models settings and rerun Patrol, optionally adding context notes, rather than hiding the runtime issue. -That same runtime-versus-infrastructure split must carry through the summary -metrics strip as well. When Patrol-owned runtime issues are active, the +If a summary metrics strip returns, that same runtime-versus-infrastructure +split must carry through it. When Patrol-owned runtime issues are active, the supporting metrics must stop counting them under generic infrastructure `Warnings` or `Active findings`; the strip should break out `Runtime issues` separately and reserve infrastructure finding counts for actual estate issues. @@ -1810,16 +1824,18 @@ The findings list must respect that same trust priority. When Patrol-owned runtime issues share a severity tier with ordinary infrastructure findings, the runtime issue should sort first within that tier so Patrol blindness is not buried under same-severity estate warnings. -The summary recency chip must follow the same governed scope distinction. When -the latest completed activity was only a scoped run, the summary should label -that timestamp as `Last activity` instead of `Last patrol`; `Last full patrol` -belongs only to the most recent completed full Patrol run. -That same distinction is transport-backed. `last_patrol_at` names the last -completed full Patrol sweep, while `last_activity_at` may advance on scoped -work or fix-verification checks without claiming a new full verification pass. -That same recency contract also applies to the header metadata row. The top -header must not revert to a generic `Last:` timestamp when the rest of Patrol -is explicitly distinguishing activity from full verification recency. +Patrol renders no summary recency chip today; the page-header recency line is +the only Patrol recency surface, and `getPatrolRecencyPresentation` labels it. +When run history holds a completed run, the line reads `Last check` for the +most recent one of any type: full, targeted, or follow-up. Otherwise it falls +back to the status transport: `Last check` with `last_patrol_at` when that is +the only timestamp, when it is at least as recent as `last_activity_at`, or when +only the activity timestamp fails to parse; otherwise `Last activity` with +`last_activity_at`. The transport keeps the +distinction: `last_patrol_at` names the last completed full Patrol sweep, +while `last_activity_at` may advance on scoped work or fix-verification checks +without claiming a new full verification pass. The header must not revert to +a generic `Last:` timestamp. If Patrol renders a summary surface again, it must also avoid reintroducing a second compact assessment or verification layer beneath the primary card. Supporting metric strips belong to counts and outcomes such as active findings, critical findings, @@ -1828,8 +1844,8 @@ verification labels in a second row that competes with the primary governed assessment and verification copy above. The same supporting-chip rule applies to timing: such a primary summary card may show health and active-finding support, but it should not add another recency -pill once header metadata, verification, and findings footer already carry the -governed activity/verification timestamps. +pill, because the header recency line already carries the governed activity +timestamp. That same Patrol-owned run-history surface must keep platform-backed system counts canonical too. When the backend distinguishes API-backed TrueNAS systems from unified-agent hosts in Patrol run history, the run-history chips @@ -1844,16 +1860,20 @@ split also applies to findings counts: the primary assessment card may keep health as supporting context, but active findings, warning counts, and critical counts belong to the supporting metric strip rather than being repeated as duplicate badges inside the primary card. -That same summary surface must also explain what Patrol actually checked -without turning coverage mechanics into the default product language. Recent -run history should drive a visible check summary that tells the operator -whether Patrol recently completed a clean broad check, only ran targeted alert -or anomaly-triggered checks, or ended its most recent broad check with errors, -so the page does not leave trust and coverage as implicit background knowledge. -When same-day run history shows both a recent broad check and a burst of -targeted or follow-up activity, that same surface should expose the recent +Patrol renders no check summary today. `getPatrolVerificationPresentation` +still classifies recent run history (a clean full check, a full check that +ended with errors, only targeted or follow-up checks, or no completed check) +and computes a same-day activity-mix label, but its only reader is the +current-findings empty state, which uses just its warning tone to withhold the +all-clear; the header recency line carries the visible coverage phrase. If a +check summary returns with that summary surface, it must explain what Patrol +actually checked without turning coverage mechanics into the default product +language: whether Patrol recently completed a clean broad check, only ran +targeted alert or anomaly-triggered checks, or ended its most recent broad +check with errors. When same-day run history shows both a recent broad check +and a burst of targeted or follow-up activity, that surface should expose the activity mix in check language instead of asking operators to reconcile a -`Recently verified` headline with a busy Patrol strip elsewhere on the page. +`Recently checked` headline with a busy Patrol strip elsewhere on the page. Fix-verification checks belong to that same explanation layer as follow-up checks, not as evidence of a fresh full-estate sweep. The same hierarchy keeps correlations, recent changes, and policy posture off @@ -1880,8 +1900,9 @@ activity facts belong in run history, selected finding details, or explicit secondary context, not as a second Patrol verdict label. That same activity explanation should handle noisy Patrol behavior concretely. When same-day history shows a mix of full sweeps, verification checks, and -scoped alert- or anomaly-triggered patrols, the verification/activity surface -should expose a compact breakdown of those run categories instead of leaving +scoped alert- or anomaly-triggered patrols, a returning verification/activity +surface should expose a compact breakdown of those run categories (Patrol +renders none today) instead of leaving operators to infer why Patrol looked busy from an undifferentiated run count alone. The same operational layer should also surface scoped-trigger state directly. @@ -2326,10 +2347,10 @@ focused on product title, recency, and route controls instead of repeating the same trust counters above the workspace tabs. The same page-header recency line must also surface the coverage signal from the most recent completed run via -`PatrolRecencyPresentation.resourcesChecked`, populated by -`getPatrolRecencyPresentation` from -`PatrolRunRecord.resources_checked`. The render reads "Last full -patrol: 3m ago — checked 47 resources" so operators see both +`PatrolRecencyPresentation.resourcesCheckedLabel` (raw count in +`resourcesChecked`), populated by `getPatrolRecencyPresentation` from +`PatrolRunRecord.resources_checked`. The render reads "Last check: +3m ago · checked 47 resources" so operators see both temporal recency and coverage in one line. The field stays optional (omitted when zero) so a degenerate run that completed without checking any resources does not render a misleading "checked 0 diff --git a/frontend-modern/browser-verification/9b9e1c752ea2726b.json b/frontend-modern/browser-verification/9b9e1c752ea2726b.json new file mode 100644 index 0000000000..8d83c7cfbe --- /dev/null +++ b/frontend-modern/browser-verification/9b9e1c752ea2726b.json @@ -0,0 +1,39 @@ +{ + "version": 1, + "base_sha": "cb5ffc2efe2b7d821f59eb26406d0616cf62e23d", + "verified_at": "2026-10-07T14:53:12Z", + "result": "passed", + "changed_paths": [ + "frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx", + "frontend-modern/src/utils/patrolSummaryPresentation.ts" + ], + "content_sha256": { + "frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx": "c7a603bb6741502acb0972bd9be241de089bf8106e88042ab6a16a8f4932ef48", + "frontend-modern/src/utils/patrolSummaryPresentation.ts": "6e7e9dd5a6e49c12aa90431bbad3650272798a9f216630fcc02392887d73a262" + }, + "routes": [ + "/patrol", + "/patrol (Activity > Finding options and history > Open work)" + ], + "viewports": [ + { + "width": 1440, + "height": 1000 + }, + { + "width": 390, + "height": 844 + } + ], + "states": [ + "Embedded production builds (vite build into internal/api/frontend-modern/dist, go build) of the parent tree (cb5ffc2efe) and this tree, each on a private DEMO_MODE mock stack with a fresh data dir; same Playwright script against both.", + "Header recency line from getPatrolRecencyPresentation: clean full run 'Last check: 4m ago \u00b7 checked 1377 resources'; targeted-only run '... \u00b7 checked 3 resources'; full run with 2 errors '... \u00b7 checked 40 resources'. No 'verified' wording in any state.", + "Findings empty state through getPatrolVerificationPresentation (findings, runs and overall_health stubbed via page.route): clean full run + grade A -> 'No current issues / Checked 1377 resources.'; targeted-only run -> 'Check needed / Run Patrol to check everything and refresh open work.'; full run with errors -> 'Check needed'; grade A with a 'Recent Patrol errors' coverage factor after a clean full run -> 'Check needed' (pre-existing behaviour, filed separately).", + "Parent and this tree produced identical recency and empty-state text in all 8 scenario x viewport cases; no horizontal overflow; no console errors.", + "Dist proof: 187 files each side; 93 byte-identical, 93 differ only in chunk-hash references, index CSS drops exactly one rule (.bg-emerald-50/60, used only by the deleted assessment shell); check-bundle-size.mjs passes." + ], + "interactions": [ + "Demo sign-in, navigate to /patrol, read the header recency line, open the Activity tab, expand 'Finding options and history', read the Open work empty state; desktop mouse and phone touch contexts.", + "Screenshots of the header and empty state captured and inspected at both widths." + ] +} diff --git a/frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx b/frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx index 490cdf1ab6..6158fbf26f 100644 --- a/frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx +++ b/frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx @@ -23,7 +23,6 @@ import { getPatrolTriggerStatusSummary } from '@/utils/patrolRunPresentation'; import { getPatrolSetupAction } from '@/utils/patrolRuntimeActions'; import { getPatrolRecencyPresentation } from '@/utils/patrolSummaryPresentation'; import { PATROL_CONTROL_ANCHOR, PATROL_OPERATIONS_LOOP_ANCHOR } from '@/routing/resourceLinks'; -import type { PatrolConfigurationFailureInput } from './patrolInvestigationContextModel'; import { getPatrolAutonomyAvailabilityPresentation } from './patrolAutonomyAvailability'; import { PATROL_AUTONOMY_POLICY_PRESENTATION } from './patrolControlPresentation'; import { PATROL_AUTONOMY_EXPERIENCE } from './patrolHomePresentation'; @@ -35,25 +34,6 @@ import { PatrolAutopilotAcknowledgementDialog } from './PatrolAutopilotAcknowled export { PATROL_AUTONOMY_POLICY_PRESENTATION } from './patrolControlPresentation'; -const isNonEmptyConfigurationDetail = (value?: string | null): value is string => - Boolean(value?.trim()); - -export function getPatrolConfigurationFailureInlineDetails( - failure: PatrolConfigurationFailureInput, -): string[] { - const readiness = failure.readiness ?? null; - const codeAndCause = [failure.code, readiness?.cause || failure.blockedCause] - .filter(isNonEmptyConfigurationDetail) - .join(' · '); - - return [ - codeAndCause || undefined, - readiness?.summary ? `Setup: ${readiness.summary}` : undefined, - readiness?.provider ? `Provider: ${readiness.provider}` : undefined, - readiness?.model ? `Model: ${readiness.model}` : undefined, - ].filter(isNonEmptyConfigurationDetail); -} - export function getPatrolAutopilotExpiry(expiresAt?: string | null): Date | null { if (!expiresAt?.trim()) return null; const expiry = new Date(expiresAt); diff --git a/frontend-modern/src/features/patrol/__tests__/PatrolIntelligenceHeader.test.ts b/frontend-modern/src/features/patrol/__tests__/PatrolIntelligenceHeader.test.ts index 81e50c2cc1..f6a97156a9 100644 --- a/frontend-modern/src/features/patrol/__tests__/PatrolIntelligenceHeader.test.ts +++ b/frontend-modern/src/features/patrol/__tests__/PatrolIntelligenceHeader.test.ts @@ -5,7 +5,6 @@ import { describe, expect, it } from 'vitest'; import { PATROL_AUTONOMY_POLICY_PRESENTATION, getPatrolAutopilotExpiry, - getPatrolConfigurationFailureInlineDetails, } from '../PatrolIntelligenceHeader'; import { getPatrolAlertAnalysisAvailabilityPresentation, @@ -36,38 +35,6 @@ describe('PatrolIntelligenceHeader', () => { ); }); - it('keeps Patrol mode readiness context visible inline', () => { - expect( - getPatrolConfigurationFailureInlineDetails({ - message: 'Patrol mode could not be saved.', - code: 'patrol_readiness_not_ready', - readiness: { - status: 'not_ready', - cause: 'model_unsupported_tools', - summary: - 'The selected Patrol model is a reasoning-only model family that commonly does not emit tool calls.', - provider: 'openrouter', - model: 'openrouter:deepseek/deepseek-r1', - }, - }), - ).toEqual([ - 'patrol_readiness_not_ready · model_unsupported_tools', - 'Setup: The selected Patrol model is a reasoning-only model family that commonly does not emit tool calls.', - 'Provider: openrouter', - 'Model: openrouter:deepseek/deepseek-r1', - ]); - }); - - it('falls back to the blocked cause when readiness cause is absent', () => { - expect( - getPatrolConfigurationFailureInlineDetails({ - message: 'Patrol mode could not be saved.', - code: 'patrol_autonomy_pro_required', - blockedCause: 'license_required', - }), - ).toEqual(['patrol_autonomy_pro_required · license_required']); - }); - it('keeps trigger runtime status in the header chrome after setup', () => { expect(headerSource).toContain('getPatrolTriggerStatusSummary'); expect(headerSource).toContain('state.patrolStatus()?.trigger_status'); diff --git a/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts b/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts index dd58e2deef..f7d939d3cb 100644 --- a/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts +++ b/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts @@ -478,7 +478,6 @@ import systemLogsPresentationSource from '@/utils/systemLogsPresentation.ts?raw' import patrolEmptyStatePresentationSource from '@/utils/patrolEmptyStatePresentation.ts?raw'; import approvalRiskPresentationSource from '@/utils/approvalRiskPresentation.ts?raw'; import patrolRunPresentationSource from '@/utils/patrolRunPresentation.ts?raw'; -import patrolSummaryPresentationSource from '@/utils/patrolSummaryPresentation.ts?raw'; import aiCostDashboardSource from '@/components/AI/AICostDashboard.tsx?raw'; import modelSelectorSource from '@/components/AI/Chat/ModelSelector.tsx?raw'; import aiModelPickerSource from '@/components/shared/AIModelPicker.tsx?raw'; @@ -3571,8 +3570,6 @@ describe('frontend resource type boundaries', () => { expect(patrolIntelligenceSurfaceSource).not.toContain( 'inline-flex items-center rounded-full border px-2.5 py-1 text-[11px] font-semibold uppercase tracking-[0.16em]', ); - expect(patrolSummaryPresentationSource).toContain('badgeVariant:'); - expect(patrolSummaryPresentationSource).not.toContain('badgeClass:'); expect(patrolIntelligenceWorkspaceSource).toContain('MetadataBadge'); // Work-group badge tones resolve in the patrolControlPresentation adapter // (getPatrolWorkspaceWorkGroups), never inline in the workspace component. @@ -3661,9 +3658,6 @@ describe('frontend resource type boundaries', () => { expect(patrolIntelligenceSurfaceSource).not.toContain( "summaryStats().fixedCount > 0\n ? 'bg-green-50 dark:bg-green-900/25 border-green-200 dark:border-green-800'", ); - expect(patrolSummaryPresentationSource).toContain( - 'export function getPatrolSummaryPresentation', - ); expect(patrolIntelligenceSurfaceSource).not.toContain( "(patrolStatus()?.quickstart_credits_remaining ?? 0) > 0\n ? 'bg-blue-50 dark:bg-blue-950/25 border-blue-200 dark:border-blue-800 text-blue-700 dark:text-blue-300'", ); diff --git a/frontend-modern/src/utils/__tests__/patrolSummaryPresentation.coverage.test.ts b/frontend-modern/src/utils/__tests__/patrolSummaryPresentation.coverage.test.ts index f22223c566..e7f422154d 100644 --- a/frontend-modern/src/utils/__tests__/patrolSummaryPresentation.coverage.test.ts +++ b/frontend-modern/src/utils/__tests__/patrolSummaryPresentation.coverage.test.ts @@ -1,47 +1,13 @@ import { describe, expect, it } from 'vitest'; import type { PatrolRunRecord } from '@/api/patrol'; -import type { IntelligenceHealthScore } from '@/types/aiIntelligence'; import { - getPatrolAssessmentPresentation, - getPatrolAssessmentShellPresentation, - getPatrolCompactAssessmentLabel, getPatrolRecencyPresentation, - getPatrolSummaryMetricState, getPatrolVerificationPresentation, } from '@/utils/patrolSummaryPresentation'; -// --------------------------------------------------------------------------- -// Fixture builders — minimal, typed objects that satisfy the module's -// internal PatrolAssessmentFinding / PatrolRunRecord / IntelligenceHealthScore -// shapes without needing the non-exported PatrolAssessmentFinding alias. -// --------------------------------------------------------------------------- - -type AssessmentFinding = NonNullable< - Parameters[0]['activeFindings'] ->[number]; - -function makeFinding(overrides: Partial = {}): AssessmentFinding { - return { - resourceId: 'vm-100', - resourceName: 'web-1', - title: 'Disk nearly full', - severity: 'warning', - status: 'active', - ...overrides, - }; -} - -function makeRuntimeFinding(overrides: Partial = {}): AssessmentFinding { - return makeFinding({ - resourceId: 'ai-service', - resourceName: 'Pulse Patrol Service', - title: 'Pulse Patrol: Provider connection issue', - ...overrides, - }); -} - +// Minimal typed PatrolRunRecord fixtures. function makeRun(overrides: Partial = {}): PatrolRunRecord { return { id: 'run-1', @@ -73,26 +39,6 @@ function makeRun(overrides: Partial = {}): PatrolRunRecord { }; } -function makeHealth(overrides: Partial = {}): IntelligenceHealthScore { - return { - score: 90, - grade: 'A', - trend: 'stable', - factors: [], - prediction: 'Infrastructure is healthy with no significant issues detected.', - ...overrides, - }; -} - -function coverageFactor() { - return { - name: 'Patrol coverage incomplete', - impact: -0.35, - description: 'Patrol coverage is incomplete.', - category: 'coverage', - }; -} - function successfulFullRun(resourcesChecked = 50): PatrolRunRecord { return makeRun({ type: 'patrol', @@ -102,668 +48,6 @@ function successfulFullRun(resourcesChecked = 50): PatrolRunRecord { }); } -// =========================================================================== -// getPatrolAssessmentShellPresentation — covers all SemanticTone map entries -// and the fallback for an unknown tone. -// =========================================================================== - -describe('getPatrolAssessmentShellPresentation', () => { - it('maps the success tone to the emerald shell', () => { - expect(getPatrolAssessmentShellPresentation('success')).toEqual({ - headerClass: 'bg-emerald-50/60 dark:bg-emerald-950/30', - badgeVariant: 'success', - iconClass: 'text-emerald-600 dark:text-emerald-300', - iconContainerClass: - 'border-emerald-200 bg-emerald-50 dark:border-emerald-800 dark:bg-emerald-950/40', - }); - }); - - it('maps the error tone to the red shell with a danger badge', () => { - expect(getPatrolAssessmentShellPresentation('error')).toEqual({ - headerClass: 'bg-red-50/70 dark:bg-red-950/30', - badgeVariant: 'danger', - iconClass: 'text-red-600 dark:text-red-300', - iconContainerClass: 'border-red-200 bg-red-50 dark:border-red-800 dark:bg-red-950/40', - }); - }); - - it('maps an explicit info tone to the blue shell', () => { - expect(getPatrolAssessmentShellPresentation('info')).toEqual({ - headerClass: 'bg-blue-50/70 dark:bg-blue-950/30', - badgeVariant: 'info', - iconClass: 'text-blue-600 dark:text-blue-300', - iconContainerClass: 'border-blue-200 bg-blue-50 dark:border-blue-800 dark:bg-blue-950/40', - }); - }); - - it('falls back to the info shell for an unrecognised tone string', () => { - expect(getPatrolAssessmentShellPresentation('critical' as never)).toEqual( - getPatrolAssessmentShellPresentation('info'), - ); - }); -}); - -// =========================================================================== -// getPatrolSummaryMetricState — covers classifyActiveFindings severity -// branches and default fallbacks. -// =========================================================================== - -describe('getPatrolSummaryMetricState', () => { - it('flags primarySeverity as critical when an infrastructure critical finding is active', () => { - const state = getPatrolSummaryMetricState({ - activeFindings: [ - makeFinding({ severity: 'critical', resourceId: 'vm-1', title: 'Host down' }), - ], - }); - expect(state.primarySeverity).toBe('critical'); - expect(state.primaryValue).toBe(1); - expect(state.criticalValue).toBe(1); - expect(state.secondarySeverity).toBe('warning'); - }); - - it('flags secondarySeverity as critical when the runtime issue is critical', () => { - const state = getPatrolSummaryMetricState({ - activeFindings: [makeRuntimeFinding({ severity: 'critical' })], - }); - expect(state.secondarySeverity).toBe('critical'); - expect(state.secondaryValue).toBe(1); - expect(state.criticalValue).toBe(1); - expect(state.primaryLabel).toBe('Infrastructure findings'); - }); - - it('defaults fixedValue to 0 when fixedCount is omitted', () => { - expect(getPatrolSummaryMetricState({}).fixedValue).toBe(0); - }); - - it('ignores findings whose status is not active', () => { - const state = getPatrolSummaryMetricState({ - activeFindings: [ - makeFinding({ status: 'resolved', severity: 'critical' }), - makeRuntimeFinding({ status: 'dismissed', severity: 'critical' }), - ], - }); - expect(state.primaryValue).toBe(0); - expect(state.secondaryValue).toBe(0); - expect(state.criticalValue).toBe(0); - }); - - it('combines mixed infrastructure and runtime severities into the critical total', () => { - const state = getPatrolSummaryMetricState({ - activeFindings: [ - makeFinding({ severity: 'critical', resourceId: 'vm-1' }), - makeRuntimeFinding({ severity: 'critical' }), - makeFinding({ severity: 'warning', resourceId: 'vm-2' }), - ], - }); - expect(state.criticalValue).toBe(2); - expect(state.primarySeverity).toBe('critical'); - expect(state.secondarySeverity).toBe('critical'); - }); -}); - -// =========================================================================== -// getPatrolCompactAssessmentLabel — covers formatIssueLabel singular/plural, -// totalActive fallback, health-score suppression by absence. -// =========================================================================== - -describe('getPatrolCompactAssessmentLabel', () => { - it('lists both critical and warning infrastructure findings when both are active', () => { - expect( - getPatrolCompactAssessmentLabel({ - assessmentLabel: 'Issues detected', - activeFindings: [ - makeFinding({ severity: 'critical', resourceId: 'vm-1', title: 'Host down' }), - makeFinding({ severity: 'warning', resourceId: 'vm-2', title: 'Disk full' }), - ], - }), - ).toBe('1 critical issue · 1 warning issue'); - }); - - it('pluralises infrastructure critical issues', () => { - expect( - getPatrolCompactAssessmentLabel({ - assessmentLabel: 'Issues detected', - activeFindings: [ - makeFinding({ severity: 'critical', resourceId: 'vm-1' }), - makeFinding({ severity: 'critical', resourceId: 'vm-2' }), - ], - }), - ).toBe('2 critical issues'); - }); - - it('pluralises Patrol runtime issues when more than one is active', () => { - expect( - getPatrolCompactAssessmentLabel({ - assessmentLabel: 'Issues detected', - activeFindings: [ - makeRuntimeFinding({ severity: 'warning', title: 'Pulse Patrol: Issue A' }), - makeRuntimeFinding({ severity: 'warning', title: 'Pulse Patrol: Issue B' }), - ], - }), - ).toBe('2 Patrol runtime issues'); - }); - - it('falls back to the totalActive count when classification yields no parts', () => { - expect( - getPatrolCompactAssessmentLabel({ - assessmentLabel: 'Issues detected', - totalActive: 3, - activeFindings: [makeFinding({ status: 'resolved', severity: 'warning' })], - }), - ).toBe('3 active issues'); - }); - - it('omits the health-score segment when overallHealth is absent', () => { - expect( - getPatrolCompactAssessmentLabel({ - assessmentLabel: 'No active issues', - }), - ).toBe('No active issues'); - }); - - it('rounds the health score to the nearest whole number', () => { - expect( - getPatrolCompactAssessmentLabel({ - assessmentLabel: 'No active issues', - overallHealth: makeHealth({ score: 72.4 }), - }), - ).toBe('No active issues · health score 72/100'); - }); - - it('pluralises historical regressions', () => { - expect( - getPatrolCompactAssessmentLabel({ - assessmentLabel: 'No active issues', - historicalRegressionCount: 3, - }), - ).toBe('No active issues · 3 past regressions'); - }); - - it('combines findings, regressions, and health score into one compact label', () => { - expect( - getPatrolCompactAssessmentLabel({ - assessmentLabel: 'Issues detected', - overallHealth: makeHealth({ score: 60, grade: 'C' }), - activeFindings: [makeFinding({ severity: 'critical', resourceId: 'vm-1' })], - historicalRegressionCount: 1, - }), - ).toBe('1 critical issue · 1 past regression · health score 60/100'); - }); -}); - -// =========================================================================== -// getPatrolAssessmentPresentation — runtime-state branches, critical-finding -// branches, coverage-gap health tones (getHealthSummaryTone), and -// getCoverageDescription fallback. -// =========================================================================== - -describe('getPatrolAssessmentPresentation — runtime state', () => { - it('maps the blocked state to the paused runtime presentation', () => { - expect(getPatrolAssessmentPresentation({ runtimeState: 'blocked' })).toEqual({ - title: 'Patrol paused', - description: 'Patrol cannot check infrastructure until the blocking condition is cleared.', - eyebrow: 'Patrol paused', - compactLabel: 'Patrol paused', - tone: 'warning', - }); - }); - - it('maps the disabled state to the disabled runtime presentation', () => { - expect(getPatrolAssessmentPresentation({ runtimeState: 'disabled' })).toEqual({ - title: 'Patrol disabled', - description: 'Enable Patrol to resume checks.', - eyebrow: 'Patrol disabled', - compactLabel: 'Patrol disabled', - tone: 'info', - }); - }); - - it('maps the unavailable state to the unavailable runtime presentation', () => { - expect(getPatrolAssessmentPresentation({ runtimeState: 'unavailable' })).toEqual({ - title: 'Patrol unavailable', - description: 'Patrol is not ready yet. Check Provider & Models and runtime availability.', - eyebrow: 'Patrol unavailable', - compactLabel: 'Patrol unavailable', - tone: 'error', - }); - }); - - it('passes the blocked reason through to the assessment description', () => { - expect( - getPatrolAssessmentPresentation({ - runtimeState: 'blocked', - blockedReason: ' provider offline ', - }).description, - ).toBe('provider offline'); - }); -}); - -describe('getPatrolAssessmentPresentation — critical findings', () => { - it('reports critical issues detected for infrastructure critical findings', () => { - expect( - getPatrolAssessmentPresentation({ - criticalFindings: 1, - activeFindings: [ - makeFinding({ severity: 'critical', resourceId: 'vm-1', title: 'Host down' }), - ], - }), - ).toEqual({ - title: 'Critical issues detected', - description: - 'Patrol surfaced 1 active critical finding in your infrastructure. Review the active findings for more detail.', - eyebrow: 'Status', - compactLabel: 'Issues detected', - tone: 'error', - }); - }); - - it('reports a critical Patrol runtime issue when only a runtime finding is critical', () => { - expect( - getPatrolAssessmentPresentation({ - criticalFindings: 1, - activeFindings: [makeRuntimeFinding({ severity: 'critical' })], - }), - ).toEqual({ - title: 'Critical Patrol runtime issue', - description: - 'Patrol is currently blocked by a critical runtime issue: Provider connection issue. Review the Patrol runtime issue for more detail.', - eyebrow: 'Status', - compactLabel: 'Patrol runtime issue', - tone: 'error', - }); - }); -}); - -describe('getPatrolAssessmentPresentation — coverage gap health tones (getHealthSummaryTone)', () => { - it('uses the error tone when health grade is D', () => { - expect( - getPatrolAssessmentPresentation({ - overallHealth: makeHealth({ - score: 45, - grade: 'D', - factors: [coverageFactor()], - prediction: 'Coverage is incomplete.', - }), - }), - ).toMatchObject({ title: 'Coverage incomplete', tone: 'error' }); - }); - - it('uses the error tone when health grade is F', () => { - expect( - getPatrolAssessmentPresentation({ - overallHealth: makeHealth({ - score: 25, - grade: 'F', - factors: [coverageFactor()], - prediction: 'Coverage is incomplete.', - }), - }), - ).toMatchObject({ title: 'Coverage incomplete', tone: 'error' }); - }); - - it('uses the warning tone when health grade is C with a coverage gap', () => { - expect( - getPatrolAssessmentPresentation({ - overallHealth: makeHealth({ - score: 70, - grade: 'C', - factors: [coverageFactor()], - prediction: 'Coverage is incomplete.', - }), - }), - ).toMatchObject({ title: 'Coverage incomplete', tone: 'warning' }); - }); -}); - -describe('getPatrolAssessmentPresentation — getCoverageDescription fallback', () => { - it('falls back to the default message when the prediction is whitespace-only', () => { - expect( - getPatrolAssessmentPresentation({ - overallHealth: makeHealth({ - score: 50, - grade: 'D', - factors: [coverageFactor()], - prediction: ' ', - }), - }).description, - ).toBe('Patrol has not finished a current check.'); - }); -}); - -describe('getPatrolAssessmentPresentation — health requires attention', () => { - it('uses a usable prediction as the description for non-A health', () => { - expect( - getPatrolAssessmentPresentation({ - overallHealth: makeHealth({ - score: 80, - grade: 'B', - prediction: 'Some warnings need review.', - }), - }), - ).toEqual({ - title: 'Health requires attention', - description: 'Some warnings need review.', - eyebrow: 'Status', - compactLabel: 'Health requires attention', - tone: 'warning', - }); - }); - - it('suppresses a coverage-gap prediction after a verified full run', () => { - expect( - getPatrolAssessmentPresentation({ - overallHealth: makeHealth({ - score: 80, - grade: 'B', - factors: [coverageFactor()], - prediction: 'Coverage is incomplete. Run Patrol to check everything.', - }), - runs: [successfulFullRun()], - }), - ).toEqual({ - title: 'Health requires attention', - description: 'Patrol still needs attention.', - eyebrow: 'Status', - compactLabel: 'Health requires attention', - tone: 'warning', - }); - }); - - it('falls back when the health prediction is empty', () => { - expect( - getPatrolAssessmentPresentation({ - overallHealth: makeHealth({ score: 80, grade: 'B', prediction: '' }), - }).description, - ).toBe('Patrol still needs attention.'); - }); - - it('keeps a non-coverage-gap prediction even after a verified full run', () => { - expect( - getPatrolAssessmentPresentation({ - overallHealth: makeHealth({ - score: 80, - grade: 'B', - factors: [coverageFactor()], - prediction: 'A few warnings were detected.', - }), - runs: [successfulFullRun()], - }).description, - ).toBe('A few warnings were detected.'); - }); -}); - -describe('getPatrolAssessmentPresentation — default success', () => { - it('returns the default success shell when no health is provided', () => { - expect(getPatrolAssessmentPresentation({})).toEqual({ - title: 'No active issues detected', - description: 'Infrastructure is healthy with no significant issues detected.', - eyebrow: 'Status', - compactLabel: 'No active issues', - tone: 'success', - }); - }); - - it('falls back to the default description when grade-A health has an empty prediction', () => { - expect( - getPatrolAssessmentPresentation({ - overallHealth: makeHealth({ grade: 'A', prediction: '' }), - }).description, - ).toBe('Infrastructure is healthy with no significant issues detected.'); - }); -}); - -// =========================================================================== -// getFindingAssessmentDescription (private) — exercised through -// getPatrolAssessmentPresentation. Covers single-runtime-issue branches, -// joinAssessmentParts with 1/2/3+ parts, formatRuntimeIssueCount, -// getRuntimeFindingSummaryLabel, shouldUsePrediction, -// predictionReadsAsAllClear, predictionReadsAsCoverageGap, -// criticalFindings/warningFindings fallbacks. -// =========================================================================== - -describe('getFindingAssessmentDescription — single runtime issue', () => { - it('uses the critical-severity runtime summary for a single critical runtime issue', () => { - expect( - getPatrolAssessmentPresentation({ - criticalFindings: 1, - activeFindings: [makeRuntimeFinding({ severity: 'critical' })], - }).description, - ).toContain('Patrol is currently blocked by a critical runtime issue'); - }); - - it('appends the coverage-gap caveat for a single critical runtime issue', () => { - expect( - getPatrolAssessmentPresentation({ - criticalFindings: 1, - overallHealth: makeHealth({ - score: 50, - grade: 'D', - factors: [coverageFactor()], - prediction: 'Coverage is incomplete.', - }), - activeFindings: [makeRuntimeFinding({ severity: 'critical' })], - }).description, - ).toBe( - 'Patrol is currently blocked by a critical runtime issue: Provider connection issue. Recent coverage is incomplete. Run Patrol to check everything.', - ); - }); - - it('uses a usable prediction for a single runtime issue', () => { - expect( - getPatrolAssessmentPresentation({ - warningFindings: 1, - overallHealth: makeHealth({ - score: 85, - grade: 'B', - prediction: 'A runtime hiccup was observed.', - }), - activeFindings: [makeRuntimeFinding({ severity: 'warning' })], - }).description, - ).toBe('A runtime hiccup was observed.'); - }); - - it('falls back to "a Patrol runtime issue" when the runtime finding has no title', () => { - expect( - getPatrolAssessmentPresentation({ - warningFindings: 1, - activeFindings: [makeRuntimeFinding({ title: '' })], - }).description, - ).toBe( - 'Patrol has an active runtime issue: a Patrol runtime issue. Review the Patrol runtime issue for more detail.', - ); - }); - - it('keeps an unnormalised runtime title that has no Pulse Patrol prefix', () => { - expect( - getPatrolAssessmentPresentation({ - warningFindings: 1, - activeFindings: [makeRuntimeFinding({ title: 'Some custom runtime error' })], - }).description, - ).toContain('Some custom runtime error'); - }); -}); - -describe('getFindingAssessmentDescription — multi-finding joinAssessmentParts', () => { - it('joins two summary parts with "and"', () => { - expect( - getPatrolAssessmentPresentation({ - warningFindings: 2, - activeFindings: [ - makeFinding({ severity: 'critical', resourceId: 'vm-1', title: 'Host down' }), - makeFinding({ severity: 'warning', resourceId: 'vm-2', title: 'Disk full' }), - ], - }).description, - ).toBe( - 'Patrol surfaced 1 active critical finding in your infrastructure and 1 active warning finding in your infrastructure. Review the active findings for more detail.', - ); - }); - - it('joins four summary parts with an Oxford comma', () => { - expect( - getPatrolAssessmentPresentation({ - criticalFindings: 2, - activeFindings: [ - makeFinding({ severity: 'critical', resourceId: 'vm-1', title: 'Host down' }), - makeFinding({ severity: 'warning', resourceId: 'vm-2', title: 'Disk full' }), - makeRuntimeFinding({ severity: 'critical' }), - makeRuntimeFinding({ - severity: 'warning', - title: 'Pulse Patrol: Slow response', - }), - ], - }).description, - ).toBe( - 'Patrol surfaced 1 active critical finding in your infrastructure, 1 active warning finding in your infrastructure, 1 active critical Patrol runtime issue, and 1 active Patrol runtime issue. Review the active findings for more detail.', - ); - }); - - it('uses the criticalFindings fallback when no active findings classify', () => { - expect( - getPatrolAssessmentPresentation({ - criticalFindings: 3, - activeFindings: [makeFinding({ status: 'resolved', severity: 'critical' })], - }).description, - ).toBe( - 'Patrol surfaced 3 active critical findings. Review the active findings for more detail.', - ); - }); - - it('uses the warningFindings fallback when no active findings classify and no criticals exist', () => { - expect( - getPatrolAssessmentPresentation({ - warningFindings: 2, - }).description, - ).toBe( - 'Patrol surfaced 2 active warning findings. Review the active findings for more detail.', - ); - }); -}); - -describe('predictionReadsAsAllClear (via getFindingAssessmentDescription)', () => { - it.each([ - ['healthy with no significant issue'], - ['no significant issues detected'], - ['no active issues'], - ['no issues detected'], - ['all clear'], - ['ALL CLEAR'], // case-insensitive - ])('suppresses the all-clear prediction "%s" for a single runtime issue', (prediction) => { - const description = getPatrolAssessmentPresentation({ - warningFindings: 1, - overallHealth: makeHealth({ score: 95, grade: 'A', prediction }), - activeFindings: [makeRuntimeFinding({ severity: 'warning' })], - }).description; - expect(description).not.toBe(prediction); - expect(description).toContain('Patrol has an active runtime issue'); - }); - - it('returns false for an empty prediction so the runtime summary is used', () => { - const description = getPatrolAssessmentPresentation({ - warningFindings: 1, - overallHealth: makeHealth({ score: 95, grade: 'A', prediction: '' }), - activeFindings: [makeRuntimeFinding({ severity: 'warning' })], - }).description; - expect(description).toContain('Review the Patrol runtime issue for more detail.'); - }); -}); - -describe('predictionReadsAsCoverageGap (via getPatrolAssessmentPresentation)', () => { - it.each([ - ['coverage is incomplete'], - ['coverage incomplete'], - ['not fully verified'], - ['limited to scoped runs'], - ['limited to targeted'], - ['runs encountered errors'], - ['ended with errors'], - ['current issue list'], - ['current full issue list'], - ['current health may be incomplete'], - ['summary may be incomplete'], - ])('suppresses the coverage-gap prediction "%s" after a verified full run', (prediction) => { - const description = getPatrolAssessmentPresentation({ - overallHealth: makeHealth({ - score: 80, - grade: 'B', - factors: [coverageFactor()], - prediction, - }), - runs: [successfulFullRun()], - }).description; - expect(description).toBe('Patrol still needs attention.'); - }); -}); - -// =========================================================================== -// hasSuccessfulFullCoverageRun (private) — exercised through -// getPatrolAssessmentPresentation's coverage-gap branch. -// =========================================================================== - -describe('hasSuccessfulFullCoverageRun (via coverage-gap branch)', () => { - it('treats a full run with errors as not successful so the coverage gap stays', () => { - expect( - getPatrolAssessmentPresentation({ - overallHealth: makeHealth({ - score: 70, - grade: 'C', - factors: [coverageFactor()], - prediction: 'Coverage is incomplete.', - }), - runs: [ - makeRun({ - type: 'patrol', - resources_checked: 50, - error_count: 3, - status: 'error', - }), - ], - }), - ).toMatchObject({ title: 'Coverage incomplete' }); - }); - - it('treats a full run with zero resources as not successful so the coverage gap stays', () => { - expect( - getPatrolAssessmentPresentation({ - overallHealth: makeHealth({ - score: 70, - grade: 'C', - factors: [coverageFactor()], - prediction: 'Coverage is incomplete.', - }), - runs: [makeRun({ type: 'patrol', resources_checked: 0, error_count: 0 })], - }), - ).toMatchObject({ title: 'Coverage incomplete' }); - }); - - it('treats a scoped-only run as not a full coverage run', () => { - expect( - getPatrolAssessmentPresentation({ - overallHealth: makeHealth({ - score: 70, - grade: 'C', - factors: [coverageFactor()], - prediction: 'Coverage is incomplete.', - }), - runs: [makeRun({ type: 'scoped', resources_checked: 5, error_count: 0 })], - }), - ).toMatchObject({ title: 'Coverage incomplete' }); - }); - - it('clears the coverage gap after a successful full run', () => { - expect( - getPatrolAssessmentPresentation({ - overallHealth: makeHealth({ - score: 85, - grade: 'B', - factors: [coverageFactor()], - prediction: 'A few warnings were detected.', - }), - runs: [successfulFullRun(60)], - }), - ).toMatchObject({ title: 'Health requires attention' }); - }); -}); - // =========================================================================== // getPatrolVerificationPresentation — runtime-state branches, full-run-with- // errors, zero-resource full run, limited-run variants (verification/scoped/ @@ -1033,7 +317,7 @@ describe('getVerificationActivityMixLabel (via getPatrolVerificationPresentation // =========================================================================== // getPatrolRecencyPresentation — timestamp fallback logic branches, -// formatRecencyResourcesCheckedLabel singular verified case. +// formatRecencyResourcesCheckedLabel singular case. // =========================================================================== describe('getPatrolRecencyPresentation — timestamp fallback logic', () => { diff --git a/frontend-modern/src/utils/__tests__/patrolSummaryPresentation.test.ts b/frontend-modern/src/utils/__tests__/patrolSummaryPresentation.test.ts index 633d9711bf..e0031892ad 100644 --- a/frontend-modern/src/utils/__tests__/patrolSummaryPresentation.test.ts +++ b/frontend-modern/src/utils/__tests__/patrolSummaryPresentation.test.ts @@ -1,568 +1,10 @@ import { describe, expect, it } from 'vitest'; import { - getPatrolAssessmentPresentation, - getPatrolAssessmentAction, - getPatrolAssessmentShellPresentation, - getPatrolCompactAssessmentLabel, getPatrolRecencyPresentation, - getPatrolScoreChipLabel, - getPatrolSummaryMetricState, getPatrolVerificationPresentation, - getPatrolNoIssuesPresentation, - getPatrolSummaryPresentation, - PATROL_NO_ISSUES_LABEL, } from '@/utils/patrolSummaryPresentation'; -describe('getPatrolSummaryPresentation', () => { - it('returns active critical presentation', () => { - expect(getPatrolSummaryPresentation('critical', true)).toEqual({ - iconClass: 'text-red-500 dark:text-red-400', - iconContainerClass: 'bg-red-50 dark:bg-red-900/25 border-red-200 dark:border-red-800', - valueClass: 'text-red-600 dark:text-red-400', - }); - }); - - it('returns quiet presentation when the metric is zero', () => { - expect(getPatrolSummaryPresentation('warning', false)).toEqual({ - iconClass: 'text-muted', - iconContainerClass: 'bg-surface border-border', - valueClass: 'text-muted', - }); - }); - - it('exports canonical patrol empty-state copy', () => { - expect(PATROL_NO_ISSUES_LABEL).toBe('No active issues'); - }); - - it('promotes coverage gaps into the primary patrol assessment state', () => { - expect( - getPatrolAssessmentPresentation({ - overallHealth: { - score: 70, - grade: 'C', - trend: 'stable', - factors: [ - { - name: 'Patrol coverage incomplete', - impact: -0.35, - description: 'Patrol coverage is incomplete.', - category: 'coverage', - }, - ], - prediction: 'Patrol coverage is incomplete.', - }, - }), - ).toEqual({ - title: 'Coverage incomplete', - description: 'Patrol coverage is incomplete.', - eyebrow: 'Status', - compactLabel: 'Coverage incomplete', - tone: 'warning', - }); - }); - - it('reports issues detected when active warning findings exist', () => { - expect( - getPatrolAssessmentPresentation({ - overallHealth: { - score: 84, - grade: 'B', - trend: 'stable', - factors: [], - prediction: 'Warnings require review.', - }, - warningFindings: 2, - }), - ).toEqual({ - title: 'Issues detected', - description: 'Warnings require review.', - eyebrow: 'Status', - compactLabel: 'Issues detected', - tone: 'warning', - }); - }); - - it('surfaces an active running Patrol instead of a stale all-clear summary', () => { - expect( - getPatrolAssessmentPresentation({ - runtimeState: 'running', - overallHealth: { - score: 100, - grade: 'A', - trend: 'stable', - factors: [], - prediction: 'Infrastructure is healthy with no significant issues detected.', - }, - }), - ).toEqual({ - title: 'Patrol running', - description: 'Patrol is checking your infrastructure now.', - eyebrow: 'Status', - compactLabel: 'Patrol is checking now', - tone: 'info', - }); - }); - - it('does not reuse an all-clear health prediction while active infrastructure findings exist', () => { - expect( - getPatrolAssessmentPresentation({ - overallHealth: { - score: 95, - grade: 'A', - trend: 'stable', - factors: [], - prediction: 'Infrastructure is healthy with no significant issues detected.', - }, - warningFindings: 1, - activeFindings: [ - { - status: 'active', - severity: 'warning', - resourceId: 'vm-100', - resourceName: 'web-1', - title: 'Backup age drift', - }, - ] as never, - }), - ).toEqual({ - title: 'Issues detected', - description: - 'Patrol surfaced 1 active warning finding in your infrastructure. Review the active findings for more detail.', - eyebrow: 'Status', - compactLabel: 'Issues detected', - tone: 'warning', - }); - }); - - it('does not reuse an all-clear health prediction while a Patrol runtime issue is active', () => { - expect( - getPatrolAssessmentPresentation({ - overallHealth: { - score: 95, - grade: 'A', - trend: 'stable', - factors: [], - prediction: 'Infrastructure is healthy with no significant issues detected.', - }, - warningFindings: 1, - activeFindings: [ - { - status: 'active', - severity: 'warning', - resourceId: 'ai-service', - resourceName: 'Pulse Patrol Service', - title: 'Pulse Patrol: Provider billing or quota issue', - }, - ] as never, - }), - ).toEqual({ - title: 'Patrol runtime issue', - description: - 'Patrol has an active runtime issue: Provider billing or quota issue. Review the Patrol runtime issue for more detail.', - eyebrow: 'Status', - compactLabel: 'Patrol runtime issue', - tone: 'warning', - }); - }); - - it('combines active findings with the coverage caveat when both are present', () => { - expect( - getPatrolAssessmentPresentation({ - overallHealth: { - score: 60, - grade: 'C', - trend: 'stable', - factors: [ - { - name: 'Patrol coverage incomplete', - impact: -0.35, - description: 'Patrol coverage is incomplete.', - category: 'coverage', - }, - ], - prediction: - 'Recent Patrol runs encountered errors, so the current health summary may be incomplete.', - }, - warningFindings: 1, - }), - ).toEqual({ - title: 'Issues detected', - description: - 'Patrol surfaced 1 active warning finding. Recent coverage is incomplete. Run Patrol to check everything.', - eyebrow: 'Status', - compactLabel: 'Issues detected', - tone: 'warning', - }); - }); - - it('drops stale coverage caveats after a successful full patrol verified resources', () => { - expect( - getPatrolAssessmentPresentation({ - overallHealth: { - score: 85, - grade: 'B', - trend: 'stable', - factors: [ - { - name: 'Patrol coverage incomplete', - impact: -0.35, - description: 'Patrol coverage is incomplete.', - category: 'coverage', - }, - ], - prediction: - 'Recent Patrol activity only covered targeted checks and ended with errors. Run Patrol to check everything.', - }, - warningFindings: 1, - activeFindings: [ - { - status: 'active', - severity: 'warning', - resourceId: 'backup-1', - resourceName: 'delly', - title: 'Backup failed', - }, - ] as never, - runs: [ - { - completed_at: '2026-03-12T10:00:00Z', - type: 'patrol', - resources_checked: 58, - error_count: 0, - status: 'issues_found', - }, - ] as never, - }), - ).toEqual({ - title: 'Issues detected', - description: - 'Patrol surfaced 1 active warning finding in your infrastructure. Review the active findings for more detail.', - eyebrow: 'Status', - compactLabel: 'Issues detected', - tone: 'warning', - }); - }); - - it('keeps Patrol runtime issues distinct from infrastructure warning findings', () => { - expect( - getPatrolAssessmentPresentation({ - overallHealth: { - score: 65, - grade: 'C', - trend: 'stable', - factors: [ - { - name: 'Patrol coverage incomplete', - impact: -0.35, - description: 'Patrol coverage is incomplete.', - category: 'coverage', - }, - ], - prediction: - 'Recent Patrol activity only covered targeted checks and ended with errors. Run Patrol to check everything.', - }, - warningFindings: 2, - activeFindings: [ - { - status: 'active', - severity: 'warning', - resourceId: 'ai-service', - resourceName: 'Pulse Patrol Service', - title: 'Pulse Patrol: Provider connection issue', - }, - { - status: 'active', - severity: 'warning', - resourceId: 'backup-1', - resourceName: 'delly', - title: 'Backup failed', - }, - ] as never, - }), - ).toEqual({ - title: 'Issues detected', - description: - 'Patrol surfaced 1 active warning finding in your infrastructure and 1 active Patrol runtime issue. Recent coverage is incomplete. Run Patrol to check everything.', - eyebrow: 'Status', - compactLabel: 'Issues detected', - tone: 'warning', - }); - }); - - it('classifies patrol-owned service failures as runtime issues instead of infrastructure issues', () => { - expect( - getPatrolAssessmentPresentation({ - overallHealth: { - score: 60, - grade: 'C', - trend: 'stable', - factors: [ - { - name: 'Patrol coverage incomplete', - impact: -0.35, - description: 'Patrol coverage is incomplete.', - category: 'coverage', - }, - ], - prediction: - 'Recent Patrol activity only covered targeted checks and ended with errors. Run Patrol to check everything.', - }, - warningFindings: 1, - activeFindings: [ - { - status: 'active', - severity: 'warning', - resourceId: 'ai-service', - resourceName: 'Pulse Patrol Service', - title: 'Pulse Patrol: Provider billing or quota issue', - }, - ] as never, - }), - ).toEqual({ - title: 'Patrol runtime issue', - description: - 'Patrol has an active runtime issue: Provider billing or quota issue. Recent coverage is incomplete. Run Patrol to check everything.', - eyebrow: 'Status', - compactLabel: 'Patrol runtime issue', - tone: 'warning', - }); - }); - - it('uses assessment labeling for coverage or runtime-limited score states', () => { - expect( - getPatrolScoreChipLabel({ - overallHealth: { - score: 60, - grade: 'C', - trend: 'stable', - factors: [ - { - name: 'Patrol coverage incomplete', - impact: -0.35, - description: 'Patrol coverage is incomplete.', - category: 'coverage', - }, - ], - prediction: 'Patrol coverage is incomplete.', - }, - activeFindings: [ - { - status: 'active', - severity: 'warning', - resourceId: 'ai-service', - resourceName: 'Pulse Patrol Service', - title: 'Pulse Patrol: Provider billing or quota issue', - }, - ] as never, - }), - ).toBe('Assessment'); - }); - - it('offers a direct Patrol provider settings action for Patrol runtime issues', () => { - expect( - getPatrolAssessmentAction({ - activeFindings: [ - { - status: 'active', - severity: 'warning', - resourceId: 'ai-service', - resourceName: 'Pulse Patrol Service', - title: 'Pulse Patrol: Provider billing or quota issue', - }, - ] as never, - }), - ).toEqual({ - label: 'Check Patrol model', - href: '/settings/pulse-intelligence/patrol', - }); - }); - - it('uses the shared neutral shell with a warning accent for degraded patrol summaries', () => { - expect(getPatrolAssessmentShellPresentation('warning')).toEqual({ - headerClass: 'bg-amber-50/70 dark:bg-amber-950/30', - badgeVariant: 'warning', - iconClass: 'text-amber-600 dark:text-amber-300', - iconContainerClass: 'border-amber-200 bg-amber-50 dark:border-amber-800 dark:bg-amber-950/40', - }); - }); - - it('defaults patrol shell styling to the informational accent', () => { - expect(getPatrolAssessmentShellPresentation()).toEqual({ - headerClass: 'bg-blue-50/70 dark:bg-blue-950/30', - badgeVariant: 'info', - iconClass: 'text-blue-600 dark:text-blue-300', - iconContainerClass: 'border-blue-200 bg-blue-50 dark:border-blue-800 dark:bg-blue-950/40', - }); - }); - - it('does not offer an AI settings action for infrastructure findings', () => { - expect( - getPatrolAssessmentAction({ - activeFindings: [ - { - status: 'active', - severity: 'warning', - resourceId: 'vm-100', - resourceName: 'web-1', - title: 'Disk nearly full', - }, - ] as never, - }), - ).toBeUndefined(); - }); - - it('splits infrastructure findings from patrol runtime issues in supporting metrics', () => { - expect( - getPatrolSummaryMetricState({ - activeFindings: [ - { - status: 'active', - severity: 'warning', - resourceId: 'ai-service', - resourceName: 'Pulse Patrol Service', - title: 'Pulse Patrol: Provider billing or quota issue', - }, - ] as never, - fixedCount: 0, - }), - ).toEqual({ - primaryLabel: 'Infrastructure findings', - primaryValue: 0, - primarySeverity: 'warning', - secondaryLabel: 'Runtime issues', - secondaryValue: 1, - secondarySeverity: 'warning', - criticalLabel: 'Critical', - criticalValue: 0, - fixedLabel: 'Fixed', - fixedValue: 0, - }); - }); - - it('keeps active findings and warnings labels when only infrastructure findings are active', () => { - expect( - getPatrolSummaryMetricState({ - activeFindings: [ - { - status: 'active', - severity: 'warning', - resourceId: 'vm-101', - resourceName: 'db-01', - title: 'Disk nearly full', - }, - ] as never, - fixedCount: 2, - }), - ).toEqual({ - primaryLabel: 'Active findings', - primaryValue: 1, - primarySeverity: 'warning', - secondaryLabel: 'Warnings', - secondaryValue: 1, - secondarySeverity: 'warning', - criticalLabel: 'Critical', - criticalValue: 0, - fixedLabel: 'Fixed', - fixedValue: 2, - }); - }); - - it('keeps health labeling for verified healthy states', () => { - expect( - getPatrolScoreChipLabel({ - overallHealth: { - score: 100, - grade: 'A', - trend: 'stable', - factors: [], - prediction: 'Infrastructure is healthy with no significant issues detected.', - }, - activeFindings: [], - }), - ).toBe('Health'); - }); - - it('names compact assessment evidence instead of exposing bare counters', () => { - expect( - getPatrolCompactAssessmentLabel({ - assessmentLabel: 'No active issues', - overallHealth: { - score: 95, - grade: 'A', - trend: 'stable', - factors: [], - prediction: 'Infrastructure is healthy with no significant issues detected.', - }, - activeFindings: [], - historicalRegressionCount: 1, - }), - ).toBe('No active issues · 1 past regression · health score 95/100'); - }); - - it('uses issue nouns for compact active finding summaries', () => { - expect( - getPatrolCompactAssessmentLabel({ - assessmentLabel: 'Issues detected', - overallHealth: { - score: 84, - grade: 'B', - trend: 'stable', - factors: [], - prediction: 'Warnings require review.', - }, - activeFindings: [ - { - status: 'active', - severity: 'critical', - resourceId: 'vm-100', - resourceName: 'web-1', - title: 'High CPU usage', - }, - { - status: 'active', - severity: 'warning', - resourceId: 'ai-service', - resourceName: 'Pulse Patrol Service', - title: 'Pulse Patrol: Provider connection issue', - }, - ] as never, - }), - ).toBe('1 critical issue · 1 Patrol runtime issue · health score 84/100'); - }); - - it('can suppress stale health scores while Patrol is still running', () => { - expect( - getPatrolCompactAssessmentLabel({ - assessmentLabel: 'Patrol is checking now', - overallHealth: { - score: 65, - grade: 'C', - trend: 'stable', - factors: [], - prediction: 'Previous assessment needs attention.', - }, - includeHealthScore: false, - }), - ).toBe('Patrol is checking now'); - }); - - it('keeps no-issues copy only for fully healthy patrol states', () => { - expect( - getPatrolNoIssuesPresentation({ - overallHealth: { - score: 100, - grade: 'A', - trend: 'stable', - factors: [], - prediction: 'Infrastructure is healthy with no significant issues detected.', - }, - }), - ).toEqual({ - label: 'No active issues', - tone: 'success', - }); - }); - +describe('Patrol recency and verification presentation', () => { it('reports a recent successful Patrol check', () => { expect( getPatrolVerificationPresentation({ @@ -935,7 +377,7 @@ describe('getPatrolSummaryPresentation', () => { it('omits resourcesChecked when the most recent completed run reports zero coverage', () => { // A run that completed without checking any resources (e.g. an early - // failure or a no-op trigger fire) should not surface a "verified 0 + // failure or a no-op trigger fire) should not surface a "checked 0 // resources" line on the page header — that reads as an alarm signal // when it's just a degenerate run. The presentation must omit the // field instead of returning resourcesChecked: 0 so render code's diff --git a/frontend-modern/src/utils/patrolSummaryPresentation.ts b/frontend-modern/src/utils/patrolSummaryPresentation.ts index 58473b9bbb..8b892487d6 100644 --- a/frontend-modern/src/utils/patrolSummaryPresentation.ts +++ b/frontend-modern/src/utils/patrolSummaryPresentation.ts @@ -1,64 +1,10 @@ import type { PatrolRunRecord, PatrolRuntimeState } from '@/api/patrol'; -import type { UnifiedFinding } from '@/stores/aiIntelligence'; -import type { IntelligenceHealthScore } from '@/types/aiIntelligence'; -import { - isPatrolRuntimeFinding, - normalizePatrolRuntimeFindingLabel, -} from '@/utils/aiFindingPresentation'; import { formatPatrolActivityBreakdown, getPatrolActivityBreakdown, } from '@/utils/patrolRunPresentation'; -import { getPatrolProviderSettingsAction } from '@/utils/patrolRuntimeActions'; import type { SemanticTone } from '@/utils/semanticTonePresentation'; import { getPatrolRuntimePresentation } from '@/utils/patrolRuntimePresentation'; -import type { StatusIndicatorVariant } from '@/utils/status'; - -export type PatrolSummaryTone = 'critical' | 'warning' | 'success'; - -export interface PatrolSummaryPresentation { - iconClass: string; - iconContainerClass: string; - valueClass: string; -} - -export interface PatrolNoIssuesPresentation { - label: string; - tone: SemanticTone; -} - -export interface PatrolAssessmentPresentation { - title: string; - description: string; - eyebrow: string; - compactLabel: string; - tone: SemanticTone; -} - -export interface PatrolAssessmentShellPresentation { - headerClass: string; - badgeVariant: StatusIndicatorVariant; - iconClass: string; - iconContainerClass: string; -} - -export interface PatrolAssessmentAction { - label: string; - href: string; -} - -export interface PatrolSummaryMetricState { - primaryLabel: string; - primaryValue: number; - primarySeverity: 'critical' | 'warning'; - secondaryLabel: string; - secondaryValue: number; - secondarySeverity: 'critical' | 'warning'; - criticalLabel: string; - criticalValue: number; - fixedLabel: string; - fixedValue: number; -} export interface PatrolVerificationPresentation { title: string; @@ -73,386 +19,13 @@ export interface PatrolRecencyPresentation { label: string; timestamp?: string; // resourcesChecked is the raw coverage signal for the most recent - // completed run. resourcesCheckedLabel is the operator-facing phrase; it - // says "verified" only for successful full checks and uses "checked" for - // limited or errored activity. + // completed run. resourcesCheckedLabel is the operator-facing phrase: + // "checked N resources" for any completed run with a positive count, + // whatever its type or error state. It never says "verified". resourcesChecked?: number; resourcesCheckedLabel?: string; } -type PatrolAssessmentFinding = Pick< - UnifiedFinding, - 'resourceId' | 'resourceName' | 'title' | 'severity' | 'status' ->; - -export interface PatrolCompactAssessmentLabelInput { - assessmentLabel: string; - overallHealth?: IntelligenceHealthScore; - activeFindings?: PatrolAssessmentFinding[]; - totalActive?: number; - historicalRegressionCount?: number; - includeHealthScore?: boolean; -} - -export const PATROL_NO_ISSUES_LABEL = 'No active issues'; -const PATROL_STATUS_EYEBROW = 'Status'; - -const QUIET_ICON_CONTAINER = 'bg-surface border-border'; -const QUIET_ICON = 'text-muted'; -const QUIET_VALUE = 'text-muted'; -const ACTIVE_PRESENTATION: Record = { - critical: { - iconClass: 'text-red-500 dark:text-red-400', - iconContainerClass: 'bg-red-50 dark:bg-red-900/25 border-red-200 dark:border-red-800', - valueClass: 'text-red-600 dark:text-red-400', - }, - success: { - iconClass: 'text-green-500 dark:text-green-400', - iconContainerClass: 'bg-green-50 dark:bg-green-900/25 border-green-200 dark:border-green-800', - valueClass: 'text-green-600 dark:text-green-400', - }, - warning: { - iconClass: 'text-amber-500 dark:text-amber-400', - iconContainerClass: 'bg-amber-50 dark:bg-amber-900/25 border-amber-200 dark:border-amber-800', - valueClass: 'text-amber-600 dark:text-amber-400', - }, -}; - -const ASSESSMENT_SHELL_PRESENTATION: Record = { - success: { - headerClass: 'bg-emerald-50/60 dark:bg-emerald-950/30', - badgeVariant: 'success', - iconClass: 'text-emerald-600 dark:text-emerald-300', - iconContainerClass: - 'border-emerald-200 bg-emerald-50 dark:border-emerald-800 dark:bg-emerald-950/40', - }, - warning: { - headerClass: 'bg-amber-50/70 dark:bg-amber-950/30', - badgeVariant: 'warning', - iconClass: 'text-amber-600 dark:text-amber-300', - iconContainerClass: 'border-amber-200 bg-amber-50 dark:border-amber-800 dark:bg-amber-950/40', - }, - error: { - headerClass: 'bg-red-50/70 dark:bg-red-950/30', - badgeVariant: 'danger', - iconClass: 'text-red-600 dark:text-red-300', - iconContainerClass: 'border-red-200 bg-red-50 dark:border-red-800 dark:bg-red-950/40', - }, - info: { - headerClass: 'bg-blue-50/70 dark:bg-blue-950/30', - badgeVariant: 'info', - iconClass: 'text-blue-600 dark:text-blue-300', - iconContainerClass: 'border-blue-200 bg-blue-50 dark:border-blue-800 dark:bg-blue-950/40', - }, -}; - -export function getPatrolSummaryPresentation( - tone: PatrolSummaryTone, - active: boolean, -): PatrolSummaryPresentation { - if (!active) { - return { - iconClass: QUIET_ICON, - iconContainerClass: QUIET_ICON_CONTAINER, - valueClass: QUIET_VALUE, - }; - } - return ACTIVE_PRESENTATION[tone]; -} - -export function getPatrolAssessmentShellPresentation( - tone: SemanticTone = 'info', -): PatrolAssessmentShellPresentation { - return ASSESSMENT_SHELL_PRESENTATION[tone] || ASSESSMENT_SHELL_PRESENTATION.info; -} - -function getHealthSummaryTone(overallHealth: IntelligenceHealthScore): SemanticTone { - return overallHealth.grade === 'D' || overallHealth.grade === 'F' ? 'error' : 'warning'; -} - -function getCoverageDescription(overallHealth: IntelligenceHealthScore | undefined): string { - return overallHealth?.prediction?.trim() || 'Patrol has not finished a current check.'; -} - -function formatFindingCount(count: number, severity: 'critical' | 'warning'): string { - return `${count} active ${severity} finding${count === 1 ? '' : 's'}`; -} - -function formatRuntimeIssueCount(count: number, severity: 'critical' | 'warning'): string { - const severityLabel = severity === 'critical' ? 'critical ' : ''; - return `${count} active ${severityLabel}Patrol runtime issue${count === 1 ? '' : 's'}`; -} - -function getRuntimeFindingSummaryLabel(finding: PatrolAssessmentFinding | undefined): string { - const title = String(finding?.title || '').trim(); - if (!title) { - return 'a Patrol runtime issue'; - } - - return normalizePatrolRuntimeFindingLabel(title) || title; -} - -function classifyActiveFindings(activeFindings: PatrolAssessmentFinding[] | undefined) { - const runtimeCritical = (activeFindings ?? []).filter( - (finding) => - finding.status === 'active' && - finding.severity === 'critical' && - isPatrolRuntimeFinding(finding), - ).length; - const runtimeWarning = (activeFindings ?? []).filter( - (finding) => - finding.status === 'active' && - finding.severity === 'warning' && - isPatrolRuntimeFinding(finding), - ).length; - const infrastructureCritical = (activeFindings ?? []).filter( - (finding) => - finding.status === 'active' && - finding.severity === 'critical' && - !isPatrolRuntimeFinding(finding), - ).length; - const infrastructureWarning = (activeFindings ?? []).filter( - (finding) => - finding.status === 'active' && - finding.severity === 'warning' && - !isPatrolRuntimeFinding(finding), - ).length; - - return { - runtimeCritical, - runtimeWarning, - infrastructureCritical, - infrastructureWarning, - runtimeTotal: runtimeCritical + runtimeWarning, - infrastructureTotal: infrastructureCritical + infrastructureWarning, - }; -} - -export function getPatrolSummaryMetricState(args: { - activeFindings?: PatrolAssessmentFinding[]; - fixedCount?: number; -}): PatrolSummaryMetricState { - const classified = classifyActiveFindings(args.activeFindings); - const hasRuntimeIssues = classified.runtimeTotal > 0; - - return { - primaryLabel: hasRuntimeIssues ? 'Infrastructure findings' : 'Active findings', - primaryValue: classified.infrastructureTotal, - primarySeverity: classified.infrastructureCritical > 0 ? 'critical' : 'warning', - secondaryLabel: hasRuntimeIssues ? 'Runtime issues' : 'Warnings', - secondaryValue: hasRuntimeIssues ? classified.runtimeTotal : classified.infrastructureWarning, - secondarySeverity: classified.runtimeCritical > 0 ? 'critical' : 'warning', - criticalLabel: 'Critical', - criticalValue: classified.infrastructureCritical + classified.runtimeCritical, - fixedLabel: 'Fixed', - fixedValue: args.fixedCount ?? 0, - }; -} - -export function getPatrolScoreChipLabel(args: { - overallHealth?: IntelligenceHealthScore; - activeFindings?: PatrolAssessmentFinding[]; -}): string { - const classified = classifyActiveFindings(args.activeFindings); - const hasCoverageGap = Boolean( - args.overallHealth?.factors.some((factor) => factor.category === 'coverage'), - ); - - if (hasCoverageGap || (classified.infrastructureTotal === 0 && classified.runtimeTotal > 0)) { - return 'Assessment'; - } - - return 'Health'; -} - -function formatIssueLabel(count: number, label: string): string { - return `${count} ${label} issue${count === 1 ? '' : 's'}`; -} - -export function getPatrolCompactAssessmentLabel(input: PatrolCompactAssessmentLabelInput): string { - const classified = classifyActiveFindings(input.activeFindings); - const parts: string[] = []; - - if (classified.infrastructureCritical > 0) { - parts.push(formatIssueLabel(classified.infrastructureCritical, 'critical')); - } - - if (classified.infrastructureWarning > 0) { - parts.push(formatIssueLabel(classified.infrastructureWarning, 'warning')); - } - - if (classified.runtimeTotal > 0) { - parts.push( - `${classified.runtimeTotal} Patrol runtime issue${classified.runtimeTotal === 1 ? '' : 's'}`, - ); - } - - if (parts.length === 0 && (input.totalActive ?? 0) > 0) { - parts.push(formatIssueLabel(input.totalActive ?? 0, 'active')); - } - - if (parts.length === 0) { - parts.push(input.assessmentLabel); - } - - const historicalRegressionCount = input.historicalRegressionCount ?? 0; - if (historicalRegressionCount > 0) { - parts.push( - `${historicalRegressionCount} past regression${historicalRegressionCount === 1 ? '' : 's'}`, - ); - } - - if (input.includeHealthScore !== false && input.overallHealth) { - parts.push(`health score ${Math.round(input.overallHealth.score)}/100`); - } - - return parts.join(' · '); -} - -export function getPatrolAssessmentAction(args: { - activeFindings?: PatrolAssessmentFinding[]; -}): PatrolAssessmentAction | undefined { - const classified = classifyActiveFindings(args.activeFindings); - if (classified.infrastructureTotal === 0 && classified.runtimeTotal > 0) { - return getPatrolProviderSettingsAction(); - } - - return undefined; -} - -function joinAssessmentParts(parts: string[]): string { - if (parts.length <= 1) return parts[0] ?? ''; - if (parts.length === 2) return `${parts[0]} and ${parts[1]}`; - return `${parts.slice(0, -1).join(', ')}, and ${parts.at(-1)}`; -} - -function predictionReadsAsAllClear(prediction: string | undefined): boolean { - const normalized = String(prediction || '') - .trim() - .toLowerCase(); - if (!normalized) return false; - - return ( - normalized.includes('healthy with no significant issue') || - normalized.includes('no significant issues detected') || - normalized.includes('no active issues') || - normalized.includes('no issues detected') || - normalized.includes('all clear') - ); -} - -function predictionReadsAsCoverageGap(prediction: string | undefined): boolean { - const normalized = String(prediction || '') - .trim() - .toLowerCase(); - if (!normalized) return false; - - return ( - normalized.includes('coverage is incomplete') || - normalized.includes('coverage incomplete') || - normalized.includes('not fully verified') || - normalized.includes('limited to scoped runs') || - normalized.includes('limited to targeted') || - normalized.includes('runs encountered errors') || - normalized.includes('ended with errors') || - normalized.includes('current issue list') || - normalized.includes('current full issue list') || - normalized.includes('current health may be incomplete') || - normalized.includes('summary may be incomplete') - ); -} - -function hasSuccessfulFullCoverageRun(runs: PatrolRunRecord[] | undefined): boolean { - const recentFullRun = (runs ?? []) - .filter((run) => isCompletedPatrolRun(run)) - .find((run) => isFullPatrolRun(run)); - return Boolean( - recentFullRun && !hasRunErrors(recentFullRun) && (recentFullRun.resources_checked || 0) > 0, - ); -} - -function getFindingAssessmentDescription(args: { - criticalFindings?: number; - warningFindings?: number; - overallHealth?: IntelligenceHealthScore; - activeFindings?: PatrolAssessmentFinding[]; - runs?: PatrolRunRecord[]; -}): string { - const criticalFindings = args.criticalFindings ?? 0; - const warningFindings = args.warningFindings ?? 0; - const hasVerifiedFullCoverage = hasSuccessfulFullCoverageRun(args.runs); - const hasCoverageGapFactor = Boolean( - args.overallHealth?.factors.some((factor) => factor.category === 'coverage'), - ); - const hasCoverageGap = hasCoverageGapFactor && !hasVerifiedFullCoverage; - const classified = classifyActiveFindings(args.activeFindings); - const activeRuntimeFindings = (args.activeFindings ?? []).filter( - (finding) => finding.status === 'active' && isPatrolRuntimeFinding(finding), - ); - const shouldUsePrediction = (prediction: string | undefined): prediction is string => { - if (!prediction) return false; - if (predictionReadsAsAllClear(prediction)) return false; - return !(hasVerifiedFullCoverage && predictionReadsAsCoverageGap(prediction)); - }; - - if (classified.infrastructureTotal === 0 && classified.runtimeTotal === 1) { - const runtimeFinding = activeRuntimeFindings[0]; - const runtimeLabel = getRuntimeFindingSummaryLabel(runtimeFinding); - const runtimeSummary = - runtimeFinding?.severity === 'critical' - ? `Patrol is currently blocked by a critical runtime issue: ${runtimeLabel}.` - : `Patrol has an active runtime issue: ${runtimeLabel}.`; - - if (hasCoverageGap) { - return `${runtimeSummary} Recent coverage is incomplete. Run Patrol to check everything.`; - } - - const prediction = args.overallHealth?.prediction?.trim(); - if (shouldUsePrediction(prediction)) { - return prediction; - } - - return `${runtimeSummary} Review the Patrol runtime issue for more detail.`; - } - - const findingSummaryParts: string[] = []; - - if (classified.infrastructureCritical > 0) { - findingSummaryParts.push( - `${formatFindingCount(classified.infrastructureCritical, 'critical')} in your infrastructure`, - ); - } - if (classified.infrastructureWarning > 0) { - findingSummaryParts.push( - `${formatFindingCount(classified.infrastructureWarning, 'warning')} in your infrastructure`, - ); - } - if (classified.runtimeCritical > 0) { - findingSummaryParts.push(formatRuntimeIssueCount(classified.runtimeCritical, 'critical')); - } - if (classified.runtimeWarning > 0) { - findingSummaryParts.push(formatRuntimeIssueCount(classified.runtimeWarning, 'warning')); - } - - const findingSummary = - findingSummaryParts.length > 0 - ? `Patrol surfaced ${joinAssessmentParts(findingSummaryParts)}.` - : criticalFindings > 0 - ? `Patrol surfaced ${formatFindingCount(criticalFindings, 'critical')}.` - : `Patrol surfaced ${formatFindingCount(warningFindings, 'warning')}.`; - - if (hasCoverageGap) { - return `${findingSummary} Recent coverage is incomplete. Run Patrol to check everything.`; - } - - const prediction = args.overallHealth?.prediction?.trim(); - if (shouldUsePrediction(prediction)) { - return prediction; - } - - return `${findingSummary} Review the active findings for more detail.`; -} - function normalizeRunType(type: string | undefined): string { return String(type || '') .trim() @@ -517,121 +90,6 @@ function formatRecencyResourcesCheckedLabel(run: PatrolRunRecord): string | unde return `checked ${resourcesChecked} resource${resourcesChecked === 1 ? '' : 's'}`; } -export function getPatrolAssessmentPresentation(args: { - overallHealth?: IntelligenceHealthScore; - runtimeState?: PatrolRuntimeState; - blockedReason?: string; - criticalFindings?: number; - warningFindings?: number; - activeFindings?: PatrolAssessmentFinding[]; - runs?: PatrolRunRecord[]; -}): PatrolAssessmentPresentation { - const classified = classifyActiveFindings(args.activeFindings); - const hasVerifiedFullCoverage = hasSuccessfulFullCoverageRun(args.runs); - - if (args.runtimeState === 'running') { - return { - title: 'Patrol running', - description: 'Patrol is checking your infrastructure now.', - eyebrow: PATROL_STATUS_EYEBROW, - compactLabel: 'Patrol is checking now', - tone: 'info', - }; - } - - if ( - args.runtimeState === 'blocked' || - args.runtimeState === 'disabled' || - args.runtimeState === 'unavailable' - ) { - const runtime = getPatrolRuntimePresentation(args.runtimeState, args.blockedReason); - return { - title: runtime.label, - description: runtime.description, - eyebrow: runtime.title, - compactLabel: runtime.label, - tone: runtime.tone, - }; - } - - if ((args.criticalFindings ?? 0) > 0) { - if (classified.infrastructureTotal === 0 && classified.runtimeTotal > 0) { - return { - title: 'Critical Patrol runtime issue', - description: getFindingAssessmentDescription(args), - eyebrow: PATROL_STATUS_EYEBROW, - compactLabel: 'Patrol runtime issue', - tone: 'error', - }; - } - - return { - title: 'Critical issues detected', - description: getFindingAssessmentDescription(args), - eyebrow: PATROL_STATUS_EYEBROW, - compactLabel: 'Issues detected', - tone: 'error', - }; - } - - if ((args.warningFindings ?? 0) > 0) { - if (classified.infrastructureTotal === 0 && classified.runtimeTotal > 0) { - return { - title: 'Patrol runtime issue', - description: getFindingAssessmentDescription(args), - eyebrow: PATROL_STATUS_EYEBROW, - compactLabel: 'Patrol runtime issue', - tone: 'warning', - }; - } - - return { - title: 'Issues detected', - description: getFindingAssessmentDescription(args), - eyebrow: PATROL_STATUS_EYEBROW, - compactLabel: 'Issues detected', - tone: 'warning', - }; - } - - if ( - args.overallHealth?.factors.some((factor) => factor.category === 'coverage') && - !hasVerifiedFullCoverage - ) { - return { - title: 'Coverage incomplete', - description: getCoverageDescription(args.overallHealth), - eyebrow: PATROL_STATUS_EYEBROW, - compactLabel: 'Coverage incomplete', - tone: getHealthSummaryTone(args.overallHealth), - }; - } - - if (args.overallHealth && args.overallHealth.grade !== 'A') { - const prediction = args.overallHealth.prediction?.trim(); - return { - title: 'Health requires attention', - description: - prediction && !(hasVerifiedFullCoverage && predictionReadsAsCoverageGap(prediction)) - ? prediction - : 'Patrol still needs attention.', - eyebrow: PATROL_STATUS_EYEBROW, - compactLabel: 'Health requires attention', - tone: getHealthSummaryTone(args.overallHealth), - }; - } - - return { - title: 'No active issues detected', - description: - args.overallHealth?.prediction?.trim() || - 'Infrastructure is healthy with no significant issues detected.', - eyebrow: PATROL_STATUS_EYEBROW, - compactLabel: PATROL_NO_ISSUES_LABEL, - tone: 'success', - }; -} - export function getPatrolVerificationPresentation(args: { runs?: PatrolRunRecord[]; runtimeState?: PatrolRuntimeState; @@ -780,15 +238,3 @@ export function getPatrolRecencyPresentation(args: { label: 'Last activity', }; } - -export function getPatrolNoIssuesPresentation(args: { - overallHealth?: IntelligenceHealthScore; - runtimeState?: PatrolRuntimeState; - blockedReason?: string; -}): PatrolNoIssuesPresentation { - const assessment = getPatrolAssessmentPresentation(args); - return { - label: assessment.compactLabel, - tone: assessment.tone, - }; -} From 6d8dce2caeeab1ecb1e49b025240e6f7af368abc Mon Sep 17 00:00:00 2001 From: rcourtman <8825017+rcourtman@users.noreply.github.com> Date: Wed, 7 Oct 2026 16:35:14 +0100 Subject: [PATCH 2/2] Remove unread Patrol summaryStats and its audit rule usePatrolIntelligenceState() still returned summaryStats, a closure that counted critical, warning and active Patrol findings plus fixed outcomes. Its only reader was PatrolIntelligenceSummary.tsx, deleted in 27ba85ddc6 (2026-06-23). A type-checker scan of the whole program (src and tests) finds no reference to the member outside the hook's return object, and nothing spreads, iterates or index-reads the returned state, so the closure and its return entry go. The canonical-patrol/no-local-summary-card-presentation rule in canonical-platform-audit.mjs matched only text shaped like that card's tint ternaries on summaryStats().criticalFindings, warningFindings and fixedCount, and told authors to use patrolSummaryPresentation, which no longer holds any summary-card presentation. With the accessor gone the rule guards a name nothing can call, so it goes, along with the three not.toContain guards in frontendResourceTypeBoundaries.test.ts that pinned the same strings. patrolSummaryPresentation.ts also leaves the audit allowlist: its remaining recency and verification presenters trip no rule, and without the entry the audit scans the module like any other (a planted "Loading messages..." string is now reported). The JavaScript output changes only by the removed code. Of 187 dist files, 94 are byte-identical and 92 differ only in chunk-hash references and SRI strings. In the AIIntelligence chunk, after normalising hashes and short identifiers, the only residual is the summaryStats closure and its entry in the returned state. The bundle-size check passes. The Patrol Inbox, Activity and Protection tabs render without console errors at desktop and phone widths. The contracts now record that the hook derives no client-side severity tally, that the page's open-issue severity counts come from the This week card's digest read, and that the audit carries no Patrol summary-card rule. The patrol-intelligence section-owner sentence named a summary surface that no longer exists; it now names the live tab panels. Contract-Neutral: Only residual guard demand is the platform support manifest frontend projection proof that the canonical-platform-audit.mjs path policy names (resource.test.ts, sourcePlatformOptions.test.ts, sourcePlatforms.test.ts). This commit deletes a Patrol summary-card rule and one allowlist entry and leaves the projection sync check untouched, so editing those tests would be fabricated proof. patrol-intelligence, api-contracts and frontend-primitives carry substantive notes, and frontendResourceTypeBoundaries.test.ts is the staged patrol page-and-state proof. --- .../v6/internal/subsystems/api-contracts.md | 1 + .../subsystems/frontend-primitives.md | 10 +++++ .../subsystems/patrol-intelligence.md | 26 +++++++++--- .../f808a2b14f5d24e7.json | 41 +++++++++++++++++++ .../scripts/canonical-platform-audit.mjs | 8 ---- .../patrol/usePatrolIntelligenceState.ts | 19 --------- .../frontendResourceTypeBoundaries.test.ts | 9 ---- 7 files changed, 72 insertions(+), 42 deletions(-) create mode 100644 frontend-modern/browser-verification/f808a2b14f5d24e7.json diff --git a/docs/release-control/v6/internal/subsystems/api-contracts.md b/docs/release-control/v6/internal/subsystems/api-contracts.md index f6943a66d2..642c0cce28 100644 --- a/docs/release-control/v6/internal/subsystems/api-contracts.md +++ b/docs/release-control/v6/internal/subsystems/api-contracts.md @@ -3325,6 +3325,7 @@ a new API state machine, queue contract, or verification-accounting field. action artifact so the Assistant runtime can refresh finding and action posture from IDs and safe summaries instead of relying on pasted chat text and the dedicated `frontend-modern/src/stores/aiIntelligenceSummaryModel.ts` owner, so recent-change counts and governed policy-posture fallbacks normalize once at the shared store boundary instead of as Patrol-hook-local payload repair + and the Patrol findings list the state hook reads from `aiIntelligenceStore.patrolFindings`, so `usePatrolIntelligenceState.ts` does not re-derive page-level severity tallies from it: the hook's unread `summaryStats` tally has been removed, the hook derives no client-side severity tally from that list today (the backend's per-severity `findings_count` reaches it only inside the AI summary payload it passes through), and the Activity tab's `This week` card takes its open-by-severity counts from `GET /api/ai/patrol/digest` instead of recounting that list and the shared `frontend-modern/src/components/Infrastructure/ResourceCorrelationSummary.tsx` card, so learned correlations and correlation context stay rendered through one governed frontend card instead of separate page-local list loops and the same shared correlation card's ordering and truncation rule, so callers pass raw correlations instead of encoding their own top-N sort behavior and the shared `frontend-modern/src/components/Infrastructure/ResourceChangeSummary.tsx` and `frontend-modern/src/components/Infrastructure/ResourceCorrelationSummary.tsx` cards' optional `buildResourceHref` input, so a surface that needs resource-filter links passes them into the shared card instead of rebuilding infrastructure URLs inline; the cards carry no link default since the cross-resource drilldown links were retired, and render resource labels as plain text diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 7707b99374..0f17f03c2b 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -4901,6 +4901,16 @@ or not fully verified. The same empty-state helper must consume Patrol trust-history evidence so a historical regression reads as history review context, not as a current issue and not as a healthy all-clear. +`frontend-modern/scripts/canonical-platform-audit.mjs` carries no Patrol +summary-card rule. Its `canonical-patrol/no-local-summary-card-presentation` +regex matched only text shaped like the deleted summary card's tint ternaries +on `summaryStats().criticalFindings`, `warningFindings` and `fixedCount`. The +Patrol state hook's `summaryStats` accessor has had no reader since that card +was deleted, so the accessor and the rule are both gone. +`frontend-modern/src/utils/patrolSummaryPresentation.ts` is off the audit +allowlist too: it now holds only the recency and verification presenters, +which no audit rule needs to exempt, so the audit scans it like any other +module. The Patrol page renders no summary shell today. If one returns, the same hierarchy applies inside it: once its primary assessment strip states Patrol's current risk and verification basis, diff --git a/docs/release-control/v6/internal/subsystems/patrol-intelligence.md b/docs/release-control/v6/internal/subsystems/patrol-intelligence.md index 92f205d675..2b4ede7b5d 100644 --- a/docs/release-control/v6/internal/subsystems/patrol-intelligence.md +++ b/docs/release-control/v6/internal/subsystems/patrol-intelligence.md @@ -1508,15 +1508,29 @@ provider/settings affordances. The feature surface now also keeps the same shell/runtime split internally: `frontend-modern/src/features/patrol/PatrolIntelligenceSurface.tsx` owns feature composition, the Patrol-owned section files under -`frontend-modern/src/features/patrol/` own the header, banner, summary, and -workspace render surfaces, and -`frontend-modern/src/features/patrol/usePatrolIntelligenceState.ts` owns Patrol -state, transport, polling, and effect lifecycle. The shell and section surfaces -must not re-accumulate Patrol API calls, timer orchestration, or store refresh -semantics, and `frontend-modern/src/stores/aiIntelligenceSummaryModel.ts` now +`frontend-modern/src/features/patrol/` own the header, banner, workspace, and +tab-panel render surfaces (the Inbox attention list, Protection objectives, and +the Activity tab's `This week` digest and recent work), and +`frontend-modern/src/features/patrol/usePatrolIntelligenceState.ts` owns the +page's shared Patrol state, transport, polling, and effect lifecycle. The shell +and the header, banner, and workspace surfaces must not re-accumulate those +Patrol API calls, timer orchestration, or store refresh semantics; the +attention list, objectives, recent work, and `This week` panels each own their +own typed read and refresh instead, and `frontend-modern/src/stores/aiIntelligenceSummaryModel.ts` now owns the canonical summary normalization so Patrol consumers inherit one governed recent-change and policy-posture snapshot instead of reintroducing hook-local fallback logic. +The hook no longer returns the summary card's severity and fix tallies. Its +`summaryStats` accessor (critical, warning, and total active findings plus +fixed outcomes, counted client-side over the Patrol findings list) lost its +only reader when the Patrol summary card was deleted and has been removed, +together with the `canonical-patrol/no-local-summary-card-presentation` rule in +`frontend-modern/scripts/canonical-platform-audit.mjs`, whose regex matched only +text shaped like the deleted card's tint ternaries on +`summaryStats().criticalFindings`, `warningFindings` and `fixedCount`. The open-issue severity counts the page shows today come from +the Activity tab's `This week` card, which reads them from +`GET /api/ai/patrol/digest` (see that card's section below); a count surface +that returns must not revive a hook-local tally of the findings list. That same Patrol hook boundary now consumes shared AI settings/model truth through `frontend-modern/src/stores/aiRuntimeState.ts` instead of mounting its own `/api/settings/ai` or `/api/ai/models` reads. Patrol-specific state still diff --git a/frontend-modern/browser-verification/f808a2b14f5d24e7.json b/frontend-modern/browser-verification/f808a2b14f5d24e7.json new file mode 100644 index 0000000000..f825bc20b6 --- /dev/null +++ b/frontend-modern/browser-verification/f808a2b14f5d24e7.json @@ -0,0 +1,41 @@ +{ + "version": 1, + "base_sha": "421d1e5423e5b83b7923ea8c3b133ad520cd5f60", + "verified_at": "2026-10-07T15:23:47Z", + "result": "passed", + "changed_paths": [ + "frontend-modern/src/features/patrol/usePatrolIntelligenceState.ts" + ], + "content_sha256": { + "frontend-modern/src/features/patrol/usePatrolIntelligenceState.ts": "0fef3795b2a6e2bcb1a979583deceeabd8536868adcb08eb383425dd97bb85ad" + }, + "routes": [ + "/patrol", + "/patrol (Activity tab)", + "/patrol (Protection tab)", + "/patrol?attention=" + ], + "viewports": [ + { + "width": 1440, + "height": 900 + }, + { + "width": 390, + "height": 844 + } + ], + "states": [ + "Embedded production build of this tree (vite build into internal/api/frontend-modern/dist, go build) on a private DEMO_MODE mock stack (port 7693, fresh data dir, telemetry off).", + "Inbox: tablist Inbox/Protection/Activity with Inbox selected; '50 decisions need you' attention list with 5 rows (for example 'Open clickhouse-2 · Docker container memory limit'); header recency line 'Last check: just now · checked 1377 resources'.", + "Activity: 'This week' digest card ('Patrol runs 11', 'New issues 6', '6 still open (2 critical, 4 warning)') above Recent work; Protection panel renders.", + "No console errors and no page errors at either width.", + "Dist proof against the parent tree (built at 333e51d2de, whose frontend-modern tree is identical to the parent 421d1e5423): 187 files each side; 94 byte-identical, 92 differ only in chunk-hash references and SRI strings; only assets/AIIntelligence-*.js differs in code, and after normalising hashes and 1-3 character identifiers its only residual is the removed summaryStats closure and its summaryStats entry in the returned state object. check-bundle-size.mjs passes." + ], + "interactions": [ + "Demo auto sign-in, navigate to /patrol, read the header and Inbox attention rows.", + "Click the first attention row: URL gains ?attention= and the detail panel opens; Escape, reload /patrol.", + "Click Activity, Protection, then Inbox tabs; Inbox re-selects.", + "Same script in a 1440x900 desktop context and a 390x844 phone context; screenshots of Inbox, opened row and Activity inspected at both widths." + ] +} diff --git a/frontend-modern/scripts/canonical-platform-audit.mjs b/frontend-modern/scripts/canonical-platform-audit.mjs index 1a4eb28ebd..8ec3b24526 100644 --- a/frontend-modern/scripts/canonical-platform-audit.mjs +++ b/frontend-modern/scripts/canonical-platform-audit.mjs @@ -130,7 +130,6 @@ const ALLOWLIST = new Set([ 'src/utils/remediationPresentation.ts', 'src/utils/patrolEmptyStatePresentation.ts', 'src/utils/patrolRunPresentation.ts', - 'src/utils/patrolSummaryPresentation.ts', 'src/utils/rbacPresentation.ts', 'src/utils/rbacPermissions.ts', 'src/utils/systemLogsPresentation.ts', @@ -581,13 +580,6 @@ const HELPER_RULES = [ message: 'Do not define local AI quickstart credits badge presentation in page code. Use @/utils/aiQuickstartPresentation instead.', }, - { - rule: 'canonical-patrol/no-local-summary-card-presentation', - regex: - /\bsummaryStats\(\)\.criticalFindings\s*>\s*0\b[\s\S]{0,320}bg-red-50[\s\S]{0,320}text-red-600|\bsummaryStats\(\)\.warningFindings\s*>\s*0\b[\s\S]{0,320}bg-amber-50[\s\S]{0,320}text-amber-600|\bsummaryStats\(\)\.fixedCount\s*>\s*0\b[\s\S]{0,320}bg-green-50[\s\S]{0,320}text-green-600/g, - message: - 'Do not define local patrol summary card presentation in page code. Use @/utils/patrolSummaryPresentation instead.', - }, { rule: 'canonical-patrol/no-local-empty-state-copy', regex: diff --git a/frontend-modern/src/features/patrol/usePatrolIntelligenceState.ts b/frontend-modern/src/features/patrol/usePatrolIntelligenceState.ts index d2eb5d4dd6..d28c5edc99 100644 --- a/frontend-modern/src/features/patrol/usePatrolIntelligenceState.ts +++ b/frontend-modern/src/features/patrol/usePatrolIntelligenceState.ts @@ -916,24 +916,6 @@ export function usePatrolIntelligenceState() { window.history.replaceState(window.history.state, '', nextPath); } - const summaryStats = () => { - const patrolFindings = allPatrolFindings(); - const activeFindings = patrolFindings.filter((finding) => finding.status === 'active'); - - return { - criticalFindings: activeFindings.filter((finding) => finding.severity === 'critical').length, - warningFindings: activeFindings.filter((finding) => finding.severity === 'warning').length, - totalActive: activeFindings.length, - fixedCount: patrolFindings.filter( - (finding) => - finding.investigationOutcome === 'fix_verified' || - finding.investigationOutcome === 'fix_executed' || - finding.investigationOutcome === 'resolved', - ).length, - hasAnyPatrolFindings: patrolFindings.length > 0, - }; - }; - const activePatrolFindings = () => allPatrolFindings().filter((finding) => finding.status === 'active'); const shouldShowPatrolSetupOnly = createMemo(() => { @@ -1134,7 +1116,6 @@ export function usePatrolIntelligenceState() { showBlockedBanner, showReadinessBanner, shouldShowPatrolSetupOnly, - summaryStats, triggerPatrolDisabledReason, }; } diff --git a/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts b/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts index f7d939d3cb..fb348d44de 100644 --- a/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts +++ b/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts @@ -3649,15 +3649,6 @@ describe('frontend resource type boundaries', () => { 'historicalRegressionCount={state.historicalRegressionCount()}', ); expect(patrolIntelligenceBannersSource).not.toContain('trackUpgradeClicked'); - expect(patrolIntelligenceSurfaceSource).not.toContain( - "summaryStats().criticalFindings > 0\n ? 'bg-red-50 dark:bg-red-900/25 border-red-200 dark:border-red-800'", - ); - expect(patrolIntelligenceSurfaceSource).not.toContain( - "summaryStats().warningFindings > 0\n ? 'bg-amber-50 dark:bg-amber-900/25 border-amber-200 dark:border-amber-800'", - ); - expect(patrolIntelligenceSurfaceSource).not.toContain( - "summaryStats().fixedCount > 0\n ? 'bg-green-50 dark:bg-green-900/25 border-green-200 dark:border-green-800'", - ); expect(patrolIntelligenceSurfaceSource).not.toContain( "(patrolStatus()?.quickstart_credits_remaining ?? 0) > 0\n ? 'bg-blue-50 dark:bg-blue-950/25 border-blue-200 dark:border-blue-800 text-blue-700 dark:text-blue-300'", );