mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Point patrol attention lifecycle at the durable threshold and finding controls
Two users independently concluded the permanent 'Remember as expected' dismissals were removed (discussions #1623, #1699) because the Needs attention workbench only offers acknowledge and temporary suppression, while the finding-level Manage menu sits behind a collapsed disclosure below it. The attention detail's Lifecycle section now states that its controls cover only the current occurrence and links the two durable paths: the alert thresholds page, and the Patrol findings panel via a control that expands the disclosure and scrolls to it. The disclosure summary also carries an open-work count badge so it no longer reads as an empty archive.
This commit is contained in:
@@ -1,25 +1,27 @@
|
||||
{
|
||||
"version": 1,
|
||||
"base_sha": "3adeb77d60620cdcc4c71431685725384bdc562a",
|
||||
"verified_at": "2026-08-12T08:19:01Z",
|
||||
"base_sha": "929c03b4906590fa12b7d7ce1d63e81ab384854b",
|
||||
"verified_at": "2026-08-13T20:53:32Z",
|
||||
"result": "passed",
|
||||
"changed_paths": [
|
||||
"frontend-modern/src/api/settings.ts",
|
||||
"frontend-modern/src/components/Settings/useInfrastructureSettingsState.ts",
|
||||
"frontend-modern/src/components/Settings/useSystemSettingsState.ts"
|
||||
"frontend-modern/src/features/patrol/PatrolAttentionWorkbench.tsx",
|
||||
"frontend-modern/src/features/patrol/PatrolIntelligenceSurface.tsx",
|
||||
"frontend-modern/src/features/patrol/__tests__/PatrolAttentionWorkbench.test.tsx",
|
||||
"frontend-modern/src/routing/resourceLinks.ts"
|
||||
],
|
||||
"content_sha256": {
|
||||
"frontend-modern/src/api/settings.ts": "cb95289d5df7cd9332cc78573a5c6052f8968cc0fbb2d66c15c8ddc49f44ab62",
|
||||
"frontend-modern/src/components/Settings/useInfrastructureSettingsState.ts": "528b32af6b1c777ed87f5dd2d5b15b531f830c520b0f624acfc5a2d75ad3e314",
|
||||
"frontend-modern/src/components/Settings/useSystemSettingsState.ts": "acb66acc4a0254a6816564713f65a1ed5cf723eb526e4960076b4c84335e2c09"
|
||||
"frontend-modern/src/features/patrol/PatrolAttentionWorkbench.tsx": "34d7d90cfb0a0a6405346115393688a8db89be602e4112512a61fce7f17237f2",
|
||||
"frontend-modern/src/features/patrol/PatrolIntelligenceSurface.tsx": "396357b7930259e4be0947e97264d5311956c118295c10d0daaa2f59be89e44c",
|
||||
"frontend-modern/src/features/patrol/__tests__/PatrolAttentionWorkbench.test.tsx": "d809cd5ee80973042e8c703e746a392ee0235d3868a0177ff8e874c87d7b811e",
|
||||
"frontend-modern/src/routing/resourceLinks.ts": "9b44383c2061a6ec4f2e49a124be766a2d7294862dd32f3343bde1199e690030"
|
||||
},
|
||||
"routes": [
|
||||
"/settings/system-general"
|
||||
"/patrol"
|
||||
],
|
||||
"viewports": [
|
||||
{
|
||||
"width": 1280,
|
||||
"height": 720
|
||||
"width": 1311,
|
||||
"height": 1234
|
||||
},
|
||||
{
|
||||
"width": 375,
|
||||
@@ -27,21 +29,16 @@
|
||||
}
|
||||
],
|
||||
"states": [
|
||||
"Admin session Monitoring Cadence card at the fresh-install Realtime (10s) default",
|
||||
"Admin card after selecting the Low (60s) preset and saving (GET /api/system/settings confirmed pvePollingInterval 60)",
|
||||
"Non-admin session (persisted session cookie whose user no longer matches the configured admin) with GET /api/system/settings returning 403 while GET /api/runtime/display returned pvePollingInterval 60: card showed Current cadence: 60 seconds with Low (60s) pressed instead of the old Realtime fallback",
|
||||
"Admin card after switching to Custom 45s and saving (GET /api/system/settings confirmed 45)",
|
||||
"Non-admin card after reload showing Current cadence: 45 seconds with Custom pressed and 45 in the custom input",
|
||||
"Same non-admin Custom 45s state re-exercised at the 375px mobile viewport",
|
||||
"Admin session reload showing Custom 45s restored through the refactored preset-resolution path"
|
||||
"Needs attention list with 10 active mock items",
|
||||
"Attention detail open with Lifecycle controls and the new durable-options guidance",
|
||||
"Findings disclosure collapsed with '5 open' badge on the summary",
|
||||
"Findings disclosure expanded showing the Open work findings list",
|
||||
"Expanded finding Manage menu listing Remember as expected and dismiss actions"
|
||||
],
|
||||
"interactions": [
|
||||
"Built the sidecar backend and ran it as a scratch instance; logged in as admin on the pulse-1601-admin Vite frontend",
|
||||
"Selected Low (60s) on the Monitoring Cadence card and saved; verified the effective interval via the settings and runtime-display APIs",
|
||||
"Restarted the backend with a different configured admin username so the persisted browser session became a genuine non-admin session (settings 403, security-status capabilities all false)",
|
||||
"Reloaded /settings/system-general in that non-admin session and confirmed the cadence card rendered the server's real 60s interval from the runtime-display projection",
|
||||
"Logged in as the new admin in a separate cookie jar, set a non-preset Custom 45s cadence, and saved",
|
||||
"Reloaded the non-admin session and confirmed Custom 45s displayed, then repeated the check at the 375px mobile viewport",
|
||||
"Reloaded the admin session to confirm the shared preset-resolution refactor still renders the saved Custom 45s state"
|
||||
"Clicked an attention item to open the detail panel (desktop and 375px mobile)",
|
||||
"Verified 'adjust alert thresholds' renders as a link with href /alerts/thresholds",
|
||||
"Clicked 'review Patrol findings' on both viewports: disclosure opened and scrolled into view",
|
||||
"Expanded a finding row and opened its Manage menu to confirm Remember as expected is reachable"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useLocation } from '@solidjs/router';
|
||||
import { A, useLocation } from '@solidjs/router';
|
||||
import {
|
||||
createEffect,
|
||||
createMemo,
|
||||
@@ -46,6 +46,7 @@ import {
|
||||
import { aiChatStore } from '@/stores/aiChat';
|
||||
import { patrolAttentionStore } from '@/stores/patrolAttention';
|
||||
import {
|
||||
ALERT_THRESHOLDS_PATH,
|
||||
buildPatrolAttentionPath,
|
||||
buildStandalonePath,
|
||||
buildWorkloadsRouteSearch,
|
||||
@@ -67,7 +68,7 @@ const FILTERS: Array<{ id: AttentionFilter; label: string }> = [
|
||||
{ id: 'resolved', label: 'Recent resolved' },
|
||||
];
|
||||
|
||||
export function PatrolAttentionWorkbench() {
|
||||
export function PatrolAttentionWorkbench(props: { onOpenFindings?: () => void } = {}) {
|
||||
const location = useLocation();
|
||||
const [selectedItemId, setSelectedItemId] = createSignal('');
|
||||
const [actionDetail, setActionDetail] = createSignal<ActionDetailResponse | null>(null);
|
||||
@@ -306,6 +307,7 @@ export function PatrolAttentionWorkbench() {
|
||||
changeLifecycle(() => suppressPatrolAttention(itemId, reason, expiresAt))
|
||||
}
|
||||
onUnsuppress={(itemId) => changeLifecycle(() => unsuppressPatrolAttention(itemId))}
|
||||
onOpenFindings={props.onOpenFindings}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
@@ -466,6 +468,7 @@ function AttentionDetail(props: {
|
||||
onUnacknowledge: (itemId: string) => Promise<void>;
|
||||
onSuppress: (itemId: string, reason: string, expiresAt: string) => Promise<void>;
|
||||
onUnsuppress: (itemId: string) => Promise<void>;
|
||||
onOpenFindings?: () => void;
|
||||
}) {
|
||||
const detail = () => props.detail;
|
||||
const item = () => detail()?.item;
|
||||
@@ -597,6 +600,7 @@ function AttentionDetail(props: {
|
||||
onUnacknowledge={props.onUnacknowledge}
|
||||
onSuppress={props.onSuppress}
|
||||
onUnsuppress={props.onUnsuppress}
|
||||
onOpenFindings={props.onOpenFindings}
|
||||
/>
|
||||
|
||||
<DetailSection title="Affected resource">
|
||||
@@ -810,6 +814,7 @@ function AttentionLifecycleControls(props: {
|
||||
onUnacknowledge: (itemId: string) => Promise<void>;
|
||||
onSuppress: (itemId: string, reason: string, expiresAt: string) => Promise<void>;
|
||||
onUnsuppress: (itemId: string) => Promise<void>;
|
||||
onOpenFindings?: () => void;
|
||||
}) {
|
||||
const [showSuppression, setShowSuppression] = createSignal(false);
|
||||
const [reason, setReason] = createSignal('');
|
||||
@@ -975,6 +980,30 @@ function AttentionLifecycleControls(props: {
|
||||
{props.error}
|
||||
</p>
|
||||
</Show>
|
||||
<div class="mt-3 border-t border-border-subtle pt-3 text-xs leading-5 text-muted">
|
||||
<p>These controls cover only this occurrence, and suppression always expires.</p>
|
||||
<p class="mt-1">
|
||||
For a permanent change,{' '}
|
||||
<A
|
||||
href={ALERT_THRESHOLDS_PATH}
|
||||
class="font-medium text-blue-700 hover:underline dark:text-blue-300"
|
||||
>
|
||||
adjust alert thresholds
|
||||
</A>{' '}
|
||||
to change or turn off this alert for the affected resource
|
||||
<Show when={props.onOpenFindings} fallback=".">
|
||||
, or{' '}
|
||||
<button
|
||||
type="button"
|
||||
class="font-medium text-blue-700 hover:underline dark:text-blue-300"
|
||||
onClick={() => props.onOpenFindings?.()}
|
||||
>
|
||||
review Patrol findings
|
||||
</button>{' '}
|
||||
to mark a finding as expected so Patrol stops raising it.
|
||||
</Show>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</DetailSection>
|
||||
);
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import { createSignal, Show } from 'solid-js';
|
||||
import { MetadataBadge } from '@/components/shared/MetadataBadge';
|
||||
import { aiIntelligenceStore } from '@/stores/aiIntelligence';
|
||||
import { usePatrolIntelligenceState } from './usePatrolIntelligenceState';
|
||||
import { PatrolIntelligenceHeader } from './PatrolIntelligenceHeader';
|
||||
import { PatrolIntelligenceBanners } from './PatrolIntelligenceBanners';
|
||||
@@ -6,16 +9,45 @@ import { PatrolAttentionWorkbench } from './PatrolAttentionWorkbench';
|
||||
|
||||
export function PatrolIntelligenceSurface() {
|
||||
const state = usePatrolIntelligenceState();
|
||||
const [findingsOpen, setFindingsOpen] = createSignal(false);
|
||||
let findingsSummary: HTMLElement | undefined;
|
||||
const openWorkCount = () => aiIntelligenceStore.patrolOpenWorkCount;
|
||||
const openFindings = () => {
|
||||
setFindingsOpen(true);
|
||||
queueMicrotask(() => {
|
||||
findingsSummary?.scrollIntoView?.({ block: 'start' });
|
||||
findingsSummary?.focus?.({ preventScroll: true });
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="space-y-6">
|
||||
<PatrolIntelligenceHeader state={state} />
|
||||
<PatrolIntelligenceBanners state={state} />
|
||||
<PatrolAttentionWorkbench />
|
||||
<PatrolAttentionWorkbench onOpenFindings={openFindings} />
|
||||
|
||||
<details class="rounded-lg border border-border bg-surface">
|
||||
<summary class="cursor-pointer px-4 py-3 text-sm font-medium text-base-content focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-blue-500 sm:px-5">
|
||||
Patrol checks, investigations, and run history
|
||||
<details
|
||||
class="rounded-lg border border-border bg-surface"
|
||||
open={findingsOpen()}
|
||||
onToggle={(event) => setFindingsOpen(event.currentTarget.open)}
|
||||
>
|
||||
<summary
|
||||
ref={findingsSummary}
|
||||
class="cursor-pointer px-4 py-3 text-sm font-medium text-base-content focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-blue-500 sm:px-5"
|
||||
>
|
||||
<span class="inline-flex flex-wrap items-center gap-2">
|
||||
Patrol checks, investigations, and run history
|
||||
<Show when={openWorkCount() > 0}>
|
||||
<MetadataBadge
|
||||
tone="info"
|
||||
size="xs"
|
||||
shape="rounded"
|
||||
aria-label={`${openWorkCount()} open patrol ${openWorkCount() === 1 ? 'finding' : 'findings'}`}
|
||||
>
|
||||
{openWorkCount()} open
|
||||
</MetadataBadge>
|
||||
</Show>
|
||||
</span>
|
||||
</summary>
|
||||
<div
|
||||
class={`space-y-4 border-t border-border p-4 sm:p-5 ${!state.patrolEnabledLocal() ? 'opacity-50 pointer-events-none' : ''}`}
|
||||
|
||||
@@ -194,7 +194,7 @@ describe('PatrolAttentionWorkbench', () => {
|
||||
const renderWorkbench = () =>
|
||||
render(() => (
|
||||
<Router>
|
||||
<Route path="/patrol" component={PatrolAttentionWorkbench} />
|
||||
<Route path="/patrol" component={() => <PatrolAttentionWorkbench />} />
|
||||
</Router>
|
||||
));
|
||||
|
||||
@@ -471,6 +471,56 @@ describe('PatrolAttentionWorkbench', () => {
|
||||
expect(screen.getByRole('button', { name: 'Return to active attention' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('points lifecycle users at the durable threshold and finding controls', async () => {
|
||||
const active = item();
|
||||
apiMocks.getList.mockResolvedValue(
|
||||
listResponse([active], summary({ activeCount: 1, openCount: 1, calm: false })),
|
||||
);
|
||||
apiMocks.getDetail.mockResolvedValue(detail(active));
|
||||
const onOpenFindings = vi.fn();
|
||||
render(() => (
|
||||
<Router>
|
||||
<Route path="/patrol" component={() => <PatrolAttentionWorkbench onOpenFindings={onOpenFindings} />} />
|
||||
</Router>
|
||||
));
|
||||
|
||||
fireEvent.click(
|
||||
await screen.findByRole('button', {
|
||||
name: 'Open Disk pressure on Database VM',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(
|
||||
await screen.findByText(/These controls cover only this occurrence/i),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', { name: 'adjust alert thresholds' })).toHaveAttribute(
|
||||
'href',
|
||||
'/alerts/thresholds',
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: 'review Patrol findings' }));
|
||||
expect(onOpenFindings).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('omits the findings pointer when no findings surface is wired', async () => {
|
||||
const active = item();
|
||||
apiMocks.getList.mockResolvedValue(
|
||||
listResponse([active], summary({ activeCount: 1, openCount: 1, calm: false })),
|
||||
);
|
||||
apiMocks.getDetail.mockResolvedValue(detail(active));
|
||||
renderWorkbench();
|
||||
|
||||
fireEvent.click(
|
||||
await screen.findByRole('button', {
|
||||
name: 'Open Disk pressure on Database VM',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(
|
||||
await screen.findByText(/These controls cover only this occurrence/i),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: 'review Patrol findings' })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('opens the canonical governed action review from an eligible attention item', async () => {
|
||||
const actionOffer = {
|
||||
targetResourceId: 'docker:host-1/container-1',
|
||||
|
||||
@@ -41,6 +41,7 @@ export const TRUENAS_PATH = '/truenas';
|
||||
export const TRUENAS_DEFAULT_TAB = 'overview';
|
||||
export const VMWARE_PATH = '/vmware';
|
||||
export const VMWARE_DEFAULT_TAB = 'overview';
|
||||
export const ALERT_THRESHOLDS_PATH = '/alerts/thresholds';
|
||||
export const PMG_THRESHOLDS_PATH = '/alerts/thresholds/mail-gateway';
|
||||
export const PATROL_PATH = '/patrol';
|
||||
export const PATROL_ATTENTION_QUERY_PARAM = 'attention';
|
||||
|
||||
Reference in New Issue
Block a user