Keep discovery manual refresh visible

This commit is contained in:
rcourtman 2026-05-15 23:55:22 +01:00
parent b0c4faa4a0
commit f81490ca4f
7 changed files with 62 additions and 26 deletions

View file

@ -170,7 +170,7 @@ runtime cost control, and shared AI transport surfaces.
divergence must be sourced from canonical `ReadState.DockerHosts()` views,
with model-shaped data limited to the watcher adapter input rather than
direct `StateSnapshot.DockerHosts` reads in the Patrol run loop.
4. Keep discovery scheduling authoritative through `internal/config/ai.go`: `discovery_enabled` and `discovery_interval_hours` must govern both lightweight infrastructure discovery and deep service-discovery background loops. `internal/api/ai_handlers.go` must preserve an explicitly supplied `discovery_interval_hours: 0` as the manual-only setting and may only apply the 24-hour default when discovery is enabled without an explicit interval payload. Discovery analysis remains a Pulse tool-led model workflow: Pulse supplies agent/API/metrics evidence and cache orchestration, while the selected model provides the intelligence. Background, settings-triggered, or drawer-triggered discovery progress must describe that discovery analysis directly and must not imply a live Pulse Assistant chat transcript unless the run is actually executing inside the chat surface. Assistant and Patrol access to discovery must stay behind the governed `pulse_discovery` tool, including an explicit forced refresh action for known resources, while settings-level manual runs must use the canonical `/api/discovery/run` new/changed/stale sweep rather than a frontend-only shortcut.
4. Keep discovery scheduling authoritative through `internal/config/ai.go`: `discovery_enabled` and `discovery_interval_hours` must govern both lightweight infrastructure discovery and deep service-discovery background loops. `internal/api/ai_handlers.go` must preserve an explicitly supplied `discovery_interval_hours: 0` as the manual-only setting and may only apply the 24-hour default when discovery is enabled without an explicit interval payload. Discovery analysis remains a Pulse tool-led model workflow: Pulse supplies agent/API/metrics evidence and cache orchestration, while the selected model provides the intelligence. Background, settings-triggered, or drawer-triggered discovery progress must describe that discovery analysis directly and must not imply a live Pulse Assistant chat transcript unless the run is actually executing inside the chat surface. Settings-triggered manual discovery is an explicit operator refresh and must not open, append to, or masquerade as a Pulse Assistant session. Assistant and Patrol access to discovery must stay behind the governed `pulse_discovery` tool, including an explicit forced refresh action for known resources, while settings-level manual runs must use the canonical `/api/discovery/run` new/changed/stale sweep rather than a frontend-only shortcut.
5. Preserve auditability for outbound model-bound context exports and keep the export record aligned with the prompt boundary that actually reaches the provider
External provider-bound unified-resource context must enforce the same
data-handling policy the export audit records: `local-only` resources are

View file

@ -905,8 +905,9 @@ AI runtime.
settings pair so selecting "Every 6 hours" or "Manual only" round-trips
through `/api/settings/ai` without depending on stale read-side diffing.
The same workload-discovery settings section must expose a manual
"Run discovery now" action wired through `/api/discovery/run`, while
resource-drawer discovery remains the forced single-resource refresh path.
"Run discovery now" action wired through `/api/discovery/run` even when
recurring workload discovery is off, while resource-drawer discovery remains
the forced single-resource refresh path.
Assistant-only controls inside the shared shell, such
as execution permissions and session maintenance, must stay explicitly
labeled as Pulse Assistant controls, while Patrol schedule and autonomy

View file

@ -122,27 +122,30 @@ export const AIRuntimeControlsSection: Component<AIRuntimeControlsSectionProps>
: 'Workload discovery will automatically re-scan resources at this interval'}
</p>
</div>
<div class="flex items-center justify-between gap-3 rounded border border-border bg-surface-alt px-3 py-2">
<p class="text-[10px] text-muted">
Runs the new, changed, and stale workload sweep used by the schedule.
</p>
<button
type="button"
class="inline-flex min-h-10 sm:min-h-9 items-center gap-1.5 rounded-md border border-border px-3 py-1.5 text-xs font-medium text-base-content hover:bg-surface-hover disabled:cursor-not-allowed disabled:opacity-50"
onClick={() => void state.handleRunDiscoveryRefresh()}
disabled={state.saving() || state.discoveryRunRunning()}
>
<Show
when={state.discoveryRunRunning()}
fallback={<RefreshCwIcon class="h-3.5 w-3.5" />}
>
<RefreshCwIcon class="h-3.5 w-3.5 animate-spin" />
</Show>
{state.discoveryRunRunning() ? 'Running...' : 'Run discovery now'}
</button>
</div>
</Show>
<div class="flex flex-col gap-2 rounded border border-border bg-surface-alt px-3 py-2 sm:flex-row sm:items-center sm:justify-between sm:gap-3">
<p class="text-[10px] text-muted sm:flex-1">
{state.form.discoveryEnabled
? 'Runs the new, changed, and stale workload sweep used by the schedule.'
: 'Runs a one-time workload discovery refresh without changing the schedule.'}
</p>
<button
type="button"
class="inline-flex min-h-10 items-center gap-1.5 self-start rounded-md border border-border px-3 py-1.5 text-xs font-medium text-base-content hover:bg-surface-hover disabled:cursor-not-allowed disabled:opacity-50 sm:min-h-9 sm:self-auto sm:flex-shrink-0"
onClick={() => void state.handleRunDiscoveryRefresh()}
disabled={state.saving() || state.discoveryRunRunning()}
>
<Show
when={state.discoveryRunRunning()}
fallback={<RefreshCwIcon class="h-3.5 w-3.5" />}
>
<RefreshCwIcon class="h-3.5 w-3.5 animate-spin" />
</Show>
{state.discoveryRunRunning() ? 'Running...' : 'Run discovery now'}
</button>
</div>
<p class="text-[10px] text-muted">{getAISettingsWorkloadDiscoverySummary().text}</p>
</div>
</Show>

View file

@ -506,6 +506,35 @@ describe('AISettings workload discovery persistence', () => {
});
});
it('keeps the manual discovery refresh visible when recurring discovery is off', async () => {
getSettingsMock.mockResolvedValue({
...baseSettings(),
discovery_enabled: false,
discovery_interval_hours: 0,
});
renderComponent();
await waitFor(() => {
expect(screen.getByRole('button', { name: /Workload Discovery Off/i })).toBeInTheDocument();
});
fireEvent.click(screen.getByRole('button', { name: /Workload Discovery Off/i }));
expect(screen.queryByLabelText('Scan Interval')).not.toBeInTheDocument();
expect(
screen.getByText('Runs a one-time workload discovery refresh without changing the schedule.'),
).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: /Run discovery now/i }));
await waitFor(() => {
expect(runDiscoveryRefreshMock).toHaveBeenCalledTimes(1);
expect(notificationSuccessMock).toHaveBeenCalledWith(
'Discovery refresh finished: 1 workload refreshed.',
);
});
});
it('reports when a manual discovery refresh has no pending work', async () => {
getSettingsMock.mockResolvedValue({
...baseSettings(),

View file

@ -268,6 +268,9 @@ describe('settings architecture guardrails', () => {
expect(aiSettingsStateSource).toContain('discoveryRunRunning');
expect(aiRuntimeControlsSectionSource).toContain('state.handleRunDiscoveryRefresh()');
expect(aiRuntimeControlsSectionSource).toContain('Run discovery now');
expect(aiRuntimeControlsSectionSource).toContain(
'Runs a one-time workload discovery refresh without changing the schedule.',
);
expect(aiRuntimeControlsSectionSource).not.toContain("fetch('/api/discovery/run");
});

View file

@ -38,10 +38,10 @@ describe('aiSettingsPresentation', () => {
expect(getAISettingsWorkloadDiscoveryHelpContent()).toEqual({
title: 'What is workload discovery?',
description:
'Workload discovery scans your VMs, containers, and container runtimes to identify running services, versions, and access details. Pulse Assistant uses that context for concrete troubleshooting guidance, and Patrol uses it to verify the right workloads continuously.',
'Workload discovery scans your VMs, containers, and container runtimes to identify running services, versions, and access details. Pulse stores that context so Assistant can use it in chat and Patrol can use it during verification.',
});
expect(getAISettingsWorkloadDiscoverySummary()).toEqual({
text: 'Workload discovery gives Pulse Assistant and Patrol concrete service context, so chat responses and verification findings can reference real services and commands instead of generic advice.',
text: 'Workload discovery stores concrete service context for Assistant chat and Patrol verification, so responses and findings can reference real services and commands instead of generic advice.',
});
expect(getAISettingsSetupDialogPresentation()).toEqual({
ariaLabel: 'Set up Assistant and Patrol',

View file

@ -44,13 +44,13 @@ export function getAISettingsWorkloadDiscoveryHelpContent() {
return {
title: 'What is workload discovery?',
description:
'Workload discovery scans your VMs, containers, and container runtimes to identify running services, versions, and access details. Pulse Assistant uses that context for concrete troubleshooting guidance, and Patrol uses it to verify the right workloads continuously.',
'Workload discovery scans your VMs, containers, and container runtimes to identify running services, versions, and access details. Pulse stores that context so Assistant can use it in chat and Patrol can use it during verification.',
} as const;
}
export function getAISettingsWorkloadDiscoverySummary() {
return {
text: 'Workload discovery gives Pulse Assistant and Patrol concrete service context, so chat responses and verification findings can reference real services and commands instead of generic advice.',
text: 'Workload discovery stores concrete service context for Assistant chat and Patrol verification, so responses and findings can reference real services and commands instead of generic advice.',
} as const;
}