diff --git a/docs/release-control/v6/internal/status.json b/docs/release-control/v6/internal/status.json index b929a503f..b77f786f0 100644 --- a/docs/release-control/v6/internal/status.json +++ b/docs/release-control/v6/internal/status.json @@ -2398,6 +2398,71 @@ "kind": "file" } ] + }, + { + "id": "RA26", + "summary": "Infrastructure Settings is a release-critical configuration surface: it must remain a first-class source-management workspace whose first viewport makes adding, discovering, installing agents, and verifying API plus Pulse Agent coverage clear, with stable source grouping and browser-verified layout before release-ready claims.", + "kind": "journey", + "blocking_level": "release-ready", + "proof_type": "hybrid", + "lane_ids": [ + "L8", + "L16" + ], + "subsystem_ids": [ + "agent-lifecycle", + "frontend-primitives" + ], + "release_gate_ids": [ + "settings-surface-layout-consistency" + ], + "proof_commands": [ + { + "id": "infrastructure-settings-workspace-tests", + "run": [ + "npm", + "--prefix", + "frontend-modern", + "test", + "--", + "src/components/Settings/__tests__/InfrastructureWorkspace.test.tsx", + "src/components/Settings/__tests__/settingsArchitecture.test.ts", + "src/utils/__tests__/discoveryPresentation.test.ts" + ] + } + ], + "evidence": [ + { + "repo": "pulse", + "path": "docs/release-control/v6/internal/status.json", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/components/Settings/__tests__/InfrastructureWorkspace.test.tsx", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/components/Settings/DiscoverySettingsForm.tsx", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/components/Settings/InfrastructureSourceManager.tsx", + "kind": "file" + }, + { + "repo": "pulse", + "path": "frontend-modern/src/components/Settings/SettingsPageShell.tsx", + "kind": "file" + } + ] } ], "evidence_reference_policy": { @@ -4416,7 +4481,21 @@ ] } ], - "work_claims": [], + "work_claims": [ + { + "id": "codex-lane-followup-mobile-post-rc-hardening", + "agent_id": "codex", + "summary": "Harden Pulse Mobile GA proof tooling and run fresh iPad approval proof", + "target_id": "v6-product-lane-expansion", + "claimed_at": "2026-04-24T11:40:01Z", + "heartbeat_at": "2026-04-24T11:40:01Z", + "expires_at": "2026-04-24T13:40:01Z", + "work_item": { + "kind": "lane-followup", + "id": "mobile-post-rc-hardening" + } + } + ], "open_decisions": [], "source_of_truth_file": "docs/release-control/v6/internal/SOURCE_OF_TRUTH.md", "resolved_decisions": [ diff --git a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md index 76c674e21..cd040cc7b 100644 --- a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md +++ b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md @@ -248,7 +248,7 @@ an add-only capacity posture. mismatched, and signature-invalid grants through agent metrics. 9. Add or change profile management, the extracted agent profiles runtime owner, the infrastructure source-manager landing, the pure unified-agent inventory/install model, the connections-ledger workspace shell, the unified ConnectionEditor and its per-type credential slots, route model, shared install section owner, the shared direct-node/discovery infrastructure settings owners plus their model, shared frontend install-command assembly, Proxmox setup/install API transport, TrueNAS platform-connection management, VMware platform-connection management, the shared monitored-system admission preview shell for those platform connections, setup-completion install handoff transport, deploy-fallback manual install transport, and fleet-control presentation through `frontend-modern/src/api/agentProfiles.ts`, `frontend-modern/src/api/nodes.ts`, `frontend-modern/src/components/Settings/AgentProfilesPanel.tsx`, `frontend-modern/src/components/Settings/useAgentProfilesPanelState.ts`, `frontend-modern/src/components/Settings/ConnectionsTable.tsx`, `frontend-modern/src/components/Settings/connectionsTableModel.ts`, `frontend-modern/src/components/Settings/useConnectionsLedger.ts`, `frontend-modern/src/components/Settings/useConnectionRowActions.ts`, `frontend-modern/src/components/Settings/ConnectionEditor/ConnectionEditor.tsx`, `frontend-modern/src/components/Settings/ConnectionEditor/AddressProbeStep.tsx`, `frontend-modern/src/components/Settings/ConnectionEditor/useConnectionEditor.ts`, `frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/NodeCredentialSlot.tsx`, `frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/TrueNASCredentialSlot.tsx`, `frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/VMwareCredentialSlot.tsx`, `frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx`, `frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx`, `frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx`, `frontend-modern/src/components/Settings/InfrastructureSourceManager.tsx`, `frontend-modern/src/components/Settings/infrastructureWorkspaceModel.ts`, `frontend-modern/src/components/Settings/MonitoredSystemAdmissionPreview.tsx`, `frontend-modern/src/components/Settings/platformConnectionsModel.ts`, `frontend-modern/src/components/Settings/useTrueNASSettingsPanelState.ts`, `frontend-modern/src/components/Settings/useVMwareSettingsPanelState.ts`, `frontend-modern/src/components/Settings/proxmoxSettingsModel.ts`, `frontend-modern/src/components/Settings/ConfiguredNodeTables.tsx`, `frontend-modern/src/components/Settings/SettingsSectionNav.tsx`, `frontend-modern/src/components/Settings/infrastructureSettingsModel.ts`, `frontend-modern/src/components/Settings/useInfrastructureConfiguredNodesState.ts`, `frontend-modern/src/components/Settings/useInfrastructureDiscoveryRuntimeState.ts`, `frontend-modern/src/components/Settings/useInfrastructureInstallState.tsx`, `frontend-modern/src/components/Settings/useInfrastructureOperationsState.tsx`, `frontend-modern/src/components/Settings/useInfrastructureSettingsState.ts`, `frontend-modern/src/components/Settings/nodeModalModel.ts`, `frontend-modern/src/components/Settings/useNodeModalState.ts`, `frontend-modern/src/components/SetupWizard/SetupCompletionPanel.tsx`, and `frontend-modern/src/utils/agentInstallCommand.ts`. Phase 9 retired the legacy reporting/inventory surface (InfrastructureOperationsController, InfrastructureInventorySection, InfrastructureActiveRowDetails, InfrastructureIgnoredRowDetails, InfrastructureStopMonitoringDialog, useInfrastructureReportingState) and the per-type shells (PlatformConnectionsWorkspace, ProxmoxSettingsPanel, ProxmoxDirectWorkspace, ProxmoxConfiguredNodesTable, ProxmoxDirectConnectionsCard, ProxmoxDiscoveryResultsCard, ProxmoxDeleteNodeDialog, ProxmoxNodeModalStack, NodeModal shell, TrueNASSettingsPanel, VMwareSettingsPanel, useProxmoxDirectWorkspaceState); lifecycle extensions must route through the unified aggregator ledger, source-manager cards, and ConnectionEditor credential slots rather than reintroducing those retired surfaces. Those lifecycle-owned settings hooks may consume websocket state only through `frontend-modern/src/contexts/appRuntime.ts`; they must not import `frontend-modern/src/App.tsx` or recreate root-shell providers. - Discovery configuration is part of that same lifecycle-owned workspace boundary. `InfrastructureSourceManager.tsx` must open one canonical discovery editor through `InfrastructureDiscoverySettingsDialog.tsx`, `DiscoverySettingsForm.tsx`, and `discoverySettingsModel.ts`, while the System/Network shell stays limited to network-boundary controls instead of reintroducing a second editable discovery surface. That same workspace boundary now owns the add-flow entry split too: the landing guidance exposes `Detect from address`, `Install Pulse Agent`, and `Choose source type` as first-run actions above the source ledger, while header actions stay limited to ongoing discovery controls. The same landing boundary may surface setup confidence from the unified rows and discovered candidates, including connected-system count, API coverage, agent coverage, discovery review state, and the next setup action, without creating a second inventory model or provider-specific summary fetch. + Discovery configuration is part of that same lifecycle-owned workspace boundary. `InfrastructureSourceManager.tsx` must open one canonical discovery editor through `InfrastructureDiscoverySettingsDialog.tsx`, `DiscoverySettingsForm.tsx`, and `discoverySettingsModel.ts`, while the System/Network shell stays limited to network-boundary controls instead of reintroducing a second editable discovery surface. That same workspace boundary now owns the infrastructure source-management toolbar too: the landing page exposes `Add infrastructure`, `Detect address`, `Install agent`, `Run discovery`, and `Discovery settings` as first-viewport actions inside the source manager rather than splitting first-run guidance from ongoing controls. The same landing boundary may surface setup confidence from the unified rows and discovered candidates, including connected-system count, API coverage, agent coverage, sources that still need an agent, and discovery review state, without creating a second inventory model or provider-specific summary fetch. Source groups must stay in the governed source catalog order instead of re-sorting by current row count, row-level lifecycle entry points must use `Manage` language, and locked agent-install states must show a compact command inventory without raw token placeholders or disabled copy commands until a token exists. Network discovery settings remain safety-critical: automatic scanning must surface the shared-network/subnet warning before operators save scan mode changes. Setup-completion handoff belongs to that same single add-flow boundary. The first-run completion screen must keep credentials as the first surfaced object, then present one compact next-step surface that sends operators to Add infrastructure or directly to the Agent handoff; source-choice explanation may live inside that surface, but lifecycle work must not reintroduce a separate setup-wizard tour, duplicate CTA section, or inline install-command owner before the canonical infrastructure workspace. The same lifecycle-owned workspace boundary now also owns attached-agent composition. When a unified Pulse Agent augments a first-class platform source such as Proxmox VE, the source manager and edit dialog must present one primary platform row with explicit `API` plus `Pulse Agent` composition rather than duplicating that same machine as a second peer row under a generic Pulse Agent platform bucket. Standalone hosts with no owning platform source remain grouped under a standalone-host owner bucket, with `Pulse Agent` shown as the collection method rather than as the pseudo-platform label. When that primary Proxmox source is cluster-backed, the same workspace boundary must render the row under the canonical cluster moniker carried by the backend grouping contract rather than under one sibling node's hostname. That same backend grouping contract must also carry the explicit member-node list for the cluster so the source manager can render child node composition such as `delly` and `minipc` beneath the owning cluster row with per-node coverage/status. Cluster-member node agents belong as augmentations on that owning Proxmox row and its child nodes, not as separate standalone-host peers. @@ -409,7 +409,10 @@ an add-only capacity posture. `NodeModalAuthenticationSection`, `NodeModalMonitoringSection`, and `NodeModalStatusFooter` primitives inline under the editor — dropping the Dialog wrapper and the surrounding discovery/configured-nodes - workspace. For TrueNAS and VMware, the credential slots are + workspace. The inline credential slot must keep the visible setup sequence + as `Endpoint`, `Authentication`, and `Coverage` before the PVE/PBS/PMG + setup forms so lifecycle actions keep a stable operator model inside the + unified editor. For TrueNAS and VMware, the credential slots are `frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/TrueNASCredentialSlot.tsx` and `frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/VMwareCredentialSlot.tsx`; diff --git a/docs/release-control/v6/internal/subsystems/api-contracts.md b/docs/release-control/v6/internal/subsystems/api-contracts.md index e61a3a21e..7a58f0b77 100644 --- a/docs/release-control/v6/internal/subsystems/api-contracts.md +++ b/docs/release-control/v6/internal/subsystems/api-contracts.md @@ -131,6 +131,11 @@ Own canonical runtime payload shapes between backend and frontend. That same client contract must expose the setup strategy before a token path is chosen: Agent Install is API + Agent, Direct Connection is API inventory, and Manual Token Setup is a manual API-token escape hatch. + The inline node credential slot must keep the visible submit sequence as + `Endpoint`, `Authentication`, and `Coverage` before the API-backed setup + controls. That sequence is presentation guidance for the existing setup + payload phases; it does not create a second node setup API model or allow + page-local payload ownership. 32. `internal/api/agent_install_command_shared.go` shared with `agent-lifecycle`: agent install command assembly is both an agent lifecycle control surface and a canonical API payload contract boundary. 33. `internal/api/ai_handler.go` shared with `ai-runtime`: Pulse Assistant handlers are both an AI runtime control surface and a canonical API payload contract boundary. 34. `internal/api/ai_handlers.go` shared with `ai-runtime`: AI settings and remediation handlers are both an AI runtime control surface and a canonical API payload contract boundary. diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 54132d404..ea857a81f 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -256,14 +256,16 @@ work extends shared components instead of creating new local variants. `/settings/infrastructure`: the landing route should read as one source-manager workspace with configured infrastructure instances first and no redundant monitored-systems ledger beneath it. The landing route may - include a compact guidance strip that explains platform APIs and host agents - as Pulse 6 infrastructure sources and exposes `Detect from address`, `Install - Pulse Agent`, and `Choose source type` as first-run actions. It may also show - a compact readiness strip derived from the same unified connection rows and - discovered candidates so operators can confirm connected-system count, - API coverage, agent coverage, discovery review state, and the next setup - action without opening a tour or second ledger. Existing sources stay visible - on the page, and add, detect, install, review, and edit flows open as + include a dedicated first-viewport toolbar that explains platform APIs and + Pulse Agent telemetry as infrastructure sources and exposes `Add + infrastructure`, `Detect address`, `Install agent`, `Run discovery`, and + `Discovery settings` inside the source manager. It may also show a compact + coverage strip derived from the same unified connection rows and discovered + candidates so operators can confirm connected-system count, API coverage, + agent coverage, sources that still need an agent, and discovery review state + without opening a tour or second ledger. Existing sources stay visible in + stable source-catalog order, and add, detect, install, review, and manage + flows open as secondary interactions from that same destination instead of taking over the whole page. Those secondary views must stay under the same single `Infrastructure` diff --git a/frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/NodeCredentialSlot.tsx b/frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/NodeCredentialSlot.tsx index 1fa768fc7..9129c91a4 100644 --- a/frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/NodeCredentialSlot.tsx +++ b/frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/NodeCredentialSlot.tsx @@ -28,9 +28,8 @@ export interface NodeCredentialSlotProps { } export const NodeCredentialSlot: Component = (props) => { - const prefill: Partial | undefined = props.prefillNode ?? ( - props.initialAddress ? { host: props.initialAddress } : undefined - ); + const prefill: Partial | undefined = + props.prefillNode ?? (props.initialAddress ? { host: props.initialAddress } : undefined); const handleSave = async (nodeData: Partial) => { await props.settings.saveNode(nodeData); @@ -64,6 +63,40 @@ export const NodeCredentialSlot: Component = (props) => return (
+
+
+
+ + 1 + +
+
Endpoint
+
Name and API address
+
+
+
+ + 2 + +
+
Authentication
+
Assisted setup or manual token
+
+
+
+ + 3 + +
+
Coverage
+
Monitoring scope and save
+
+
+
+
diff --git a/frontend-modern/src/components/Settings/DiscoverySettingsForm.tsx b/frontend-modern/src/components/Settings/DiscoverySettingsForm.tsx index 200a45c2e..e619dafd3 100644 --- a/frontend-modern/src/components/Settings/DiscoverySettingsForm.tsx +++ b/frontend-modern/src/components/Settings/DiscoverySettingsForm.tsx @@ -25,10 +25,10 @@ export const DiscoverySettingsForm: Component = (pro return (
-
+
= (pro d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> -
+

{priorityNotice.title}

    {(item) =>
  • • {item}
  • }
    @@ -108,11 +108,15 @@ export const DiscoverySettingsForm: Component = (pro await props.handleDiscoveryModeChange('auto'); } }} - disabled={props.envOverrides().discoverySubnet || props.savingDiscoverySettings()} + disabled={ + props.envOverrides().discoverySubnet || props.savingDiscoverySettings() + } class="mt-1 h-5 w-5 border-slate-300 text-blue-600 focus:ring-blue-500 sm:h-4 sm:w-4" />
    -

    {autoModePresentation.label}

    +

    + {autoModePresentation.label} +

    {autoModePresentation.description}

    @@ -134,7 +138,9 @@ export const DiscoverySettingsForm: Component = (pro void props.handleDiscoveryModeChange('custom'); } }} - disabled={props.envOverrides().discoverySubnet || props.savingDiscoverySettings()} + disabled={ + props.envOverrides().discoverySubnet || props.savingDiscoverySettings() + } class="mt-1 h-5 w-5 border-slate-300 text-blue-600 focus:ring-blue-500 sm:h-4 sm:w-4" />
    @@ -181,9 +187,7 @@ export const DiscoverySettingsForm: Component = (pro return; } - const updatedValue = props.normalizeSubnetList( - selections.join(', '), - ); + const updatedValue = props.normalizeSubnetList(selections.join(', ')); props.setDiscoveryMode('custom'); props.setDiscoverySubnetDraft(updatedValue); props.setLastCustomSubnet(updatedValue); diff --git a/frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx b/frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx index cd9645224..1f1f016a1 100644 --- a/frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx +++ b/frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx @@ -37,7 +37,9 @@ export const InfrastructureInstallerSection: Component = () => {
    -

    Security configured. Save these first-run credentials now.

    +

    + Security configured. Save these first-run credentials now. +

    This is the Pulse Agent handoff from first-run setup inside Add infrastructure. { -

    -                                {snippet.command}
    -                              
    -
    - -

    {snippet.note}

    -
    -
    + + {snippet.label} + )}
    @@ -366,8 +346,8 @@ export const InfrastructureInstallerSection: Component = () => { />

    - Override the address agents use to connect to this server (e.g., use IP address{' '} - http://192.0.2.50:7655 if DNS fails). + Override the address agents use to connect to this server (e.g., use IP + address http://192.0.2.50:7655 if DNS fails). Currently using auto-detected: {state.agentUrl()} @@ -399,16 +379,16 @@ export const InfrastructureInstallerSection: Component = () => {

Preserves custom trust for copied install, upgrade, and uninstall commands. - Shell commands pass --cacert to both the download and the installer. - Windows commands set PULSE_CACERT and use a transport-aware - PowerShell bootstrap for the initial script fetch. + Shell commands pass --cacert to both the download and the + installer. Windows commands set PULSE_CACERT and use a + transport-aware PowerShell bootstrap for the initial script fetch.

- TLS verification disabled — skip cert checks for - self-signed setups. Not recommended for production. + TLS verification disabled — skip cert checks + for self-signed setups. Not recommended for production.
@@ -440,15 +420,16 @@ export const InfrastructureInstallerSection: Component = () => {
- Pulse commands enabled — The agent will accept - diagnostic and fix commands from Pulse Patrol features. + Pulse commands enabled — The agent will + accept diagnostic and fix commands from Pulse Patrol features.
- Config signing (optional) — Require signed remote - config payloads with PULSE_AGENT_CONFIG_SIGNATURE_REQUIRED=true. - Provide keys via PULSE_AGENT_CONFIG_SIGNING_KEY (Pulse) and{' '} + Config signing (optional) — Require signed + remote config payloads with{' '} + PULSE_AGENT_CONFIG_SIGNATURE_REQUIRED=true. Provide keys via{' '} + PULSE_AGENT_CONFIG_SIGNING_KEY (Pulse) and{' '} PULSE_AGENT_CONFIG_PUBLIC_KEYS (agents).
@@ -463,7 +444,9 @@ export const InfrastructureInstallerSection: Component = () => { id="install-profile-select" value={state.installProfile()} onChange={(event) => - state.handleInstallProfileChange(event.currentTarget.value as InstallProfile) + state.handleInstallProfileChange( + event.currentTarget.value as InstallProfile, + ) } class="w-full rounded-md border bg-surface px-3 py-2 text-sm shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:focus:border-blue-400 dark:focus:ring-blue-800" > @@ -598,7 +581,9 @@ export const InfrastructureInstallerSection: Component = () => { />
-

{state.lookupError()}

+

+ {state.lookupError()} +

{(result) => { @@ -618,7 +603,9 @@ export const InfrastructureInstallerSection: Component = () => {
- {isConnected() ? 'First host connected' : agent().displayName || agent().hostname} + {isConnected() + ? 'First host connected' + : agent().displayName || agent().hostname}

{

-

Auto-detection not working?

+

+ Auto-detection not working? +

If Docker, Kubernetes, or Proxmox isn't detected automatically, add these flags to the install command: @@ -749,7 +738,14 @@ export const InfrastructureInstallerSection: Component = () => { class="absolute right-2 top-2 inline-flex min-h-10 min-w-10 items-center justify-center rounded-md bg-surface-hover p-2 text-slate-400 transition-colors hover:bg-slate-700 hover:text-slate-200 sm:min-h-9 sm:min-w-9" title="Copy command" > - + @@ -775,7 +771,9 @@ export const InfrastructureInstallerSection: Component = () => { )

- Windows (PowerShell as Administrator) + + Windows (PowerShell as Administrator) +
- +
+
+ + + - - - + + + + + + + + + + + + + + + +
); @@ -401,9 +416,7 @@ export const InfrastructureSourceManager: Component - + @@ -414,31 +427,32 @@ export const InfrastructureSourceManager: Component (
-

Infrastructure readiness

+

Infrastructure coverage

- Systems are now built from infrastructure sources. Use this summary to confirm Pulse has - API inventory, agent telemetry, and discovery review covered for the estate. + This is the source map Pulse uses to understand the estate. Confirm the API inventory, + agent telemetry, scan scope, and any pending discovery candidates before relying on + dashboards or automation.

-
-
+
Connected systems @@ -463,6 +477,14 @@ export const InfrastructureSourceManager: Component
+
+
+ Needs agent +
+
+ {formatCount(apiOnlySystemCount(), 'system')} +
+
Discovery
{discoveryReadinessLabel()}
@@ -473,58 +495,6 @@ export const InfrastructureSourceManager: Component ); - const onboardingBand = () => ( -
-
-
-

- Start by connecting what Pulse should monitor -

-

- Pulse 6 treats platform APIs and host agents as infrastructure sources. Connect a - supported platform API for inventory and health, install Pulse Agent for node-local - telemetry, or use both when you want full coverage. -

-
- - -
- - - - - - - - - -
-
-
-
- ); - const emptyStateContent = () => (
Start monitoring infrastructure
@@ -537,179 +507,365 @@ export const InfrastructureSourceManager: Component - {onboardingBand()} + {headerActions()} {setupConfidenceBand()} - - - - - System - - - Source - - - Endpoint - - - Coverage - - - Status - - - - Actions +
+
+ + + + System - - - + + Source + + + Endpoint + + + Coverage + + + Status + + + + Actions + + + + - - - {(product) => { - const configuredRows = () => groupedConfiguredRows().get(product.type) ?? []; - const discoveredRows = () => groupedDiscoveredRows().get(product.type) ?? []; - const groupRowClass = () => 'border-b border-border-subtle bg-base hover:bg-base'; - const groupLabelClass = () => 'text-[15px] font-semibold text-base-content'; + + + {(product) => { + const configuredRows = () => groupedConfiguredRows().get(product.type) ?? []; + const discoveredRows = () => groupedDiscoveredRows().get(product.type) ?? []; + const groupRowClass = () => + 'border-b border-border-subtle bg-base hover:bg-base'; + const groupLabelClass = () => 'text-[15px] font-semibold text-base-content'; - return ( - <> - + + +
+ {product.label} +
+
+ + } + > -
+
{product.label}
- - } - > - - -
- {product.label} -
-
- - - - - -
- - - 0}> - - {(row) => { - return ( - <> - + + + + + + + + 0}> + + {(row) => { + return ( + <> + + +
+ {row.name} +
+ +
+ {row.subtitle} +
+
+ +
+ {row.identitySubtitle} +
+
+
+ + + {(() => { + const presentation = infrastructureSourcePresentation( + row.source, + ); + const title = + agentMethodTitleFor(row) ?? presentation.title; + return ( + + {presentation.label} + + ); + })()} + + + + -} + > +
+ {row.host} +
+
+
+ + + 0} + fallback={-} + > +
+ {row.coverageLabels.join(', ')} +
+
+
+ + +
+ + {row.statusLabel} + + 0}> + + {row.agentUpdateCount === 1 + ? 'Agent update' + : `${row.agentUpdateCount} agent updates`} + + + + {row.lastActivityText} + +
+
+ + + + Read only + } + > + + + + +
+ + + + + + + + + + 0}> + + {(member, memberIndex) => { + const memberPresentation = infrastructureSourcePresentation( + member.source, + ); + const memberSourceTitle = + memberMethodTitleFor(row, memberIndex()) ?? + memberPresentation.title; + return ( + + +
+ +
+
+ {member.name} +
+
+ {member.subtitle} +
+
+
+
+ + + + {memberPresentation.label} + + + + + -} + > +
+ {member.host} +
+
+
+ + + 0} + fallback={-} + > +
+ {member.coverageLabels.join(', ')} +
+
+
+ + +
+ + {member.statusLabel} + + + {member.lastActivityText} + +
+
+ + + + + + +
+ ); + }} +
+
+ + ); + }} +
+
+ + 0}> + + {(server) => { + return ( +
- {row.name} + {discoveredServerName(server)}
- -
- {row.subtitle} -
-
- -
- {row.identitySubtitle} -
-
- {(() => { - const presentation = infrastructureSourcePresentation( - row.source, - ); - const title = agentMethodTitleFor(row) ?? presentation.title; - return ( - - {presentation.label} - - ); - })()} - - - - -} + -
- {row.host} -
-
+ Candidate +
- 0} - fallback={-} +
-
- {row.coverageLabels.join(', ')} -
- + {discoveredServerEndpoint(server)} +
+
+ + +
+ {discoveredCoverageText(server)} +
- - {row.statusLabel} + + Discovered - 0}> - - {row.agentUpdateCount === 1 - ? 'Agent update' - : `${row.agentUpdateCount} agent updates`} - - - {row.lastActivityText} + {lastDiscoveryResultText() ?? 'Waiting for scan'}
@@ -717,238 +873,42 @@ export const InfrastructureSourceManager: Component Read only} >
+ ); + }} + + + + ); + }} + - - - - - - - - - 0}> - - {(member, memberIndex) => { - const memberPresentation = infrastructureSourcePresentation( - member.source, - ); - const memberSourceTitle = - memberMethodTitleFor(row, memberIndex()) ?? - memberPresentation.title; - return ( - - -
- -
-
- {member.name} -
-
- {member.subtitle} -
-
-
-
- - - - {memberPresentation.label} - - - - - -} - > -
- {member.host} -
-
-
- - - 0} - fallback={-} - > -
- {member.coverageLabels.join(', ')} -
-
-
- - -
- - {member.statusLabel} - - - {member.lastActivityText} - -
-
- - - - - - -
- ); - }} -
-
- - ); - }} - - - - 0}> - - {(server) => { - return ( - - -
- {discoveredServerName(server)} -
-
- - - - Candidate - - - - -
- {discoveredServerEndpoint(server)} -
-
- - -
- {discoveredCoverageText(server)} -
-
- - -
- - Discovered - - - {lastDiscoveryResultText() ?? 'Waiting for scan'} - -
-
- - - - Read only} - > - - - - -
- ); - }} -
-
- - ); - }} - - - - - - {emptyStateContent()} - - - - - - - - - - - - -
+ {emptyStateContent()} + + +
+ + +
@@ -1112,7 +1072,7 @@ export const InfrastructureSourceManager: Component props.onOpenConnection?.(row)} class={`${inlineButtonClass} flex-shrink-0`} > - Edit + Manage @@ -1191,19 +1151,6 @@ export const InfrastructureSourceManager: Component - - -
- -
-
diff --git a/frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx b/frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx index cfbf773e9..2620b41c9 100644 --- a/frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx +++ b/frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx @@ -849,13 +849,13 @@ const InfrastructureWorkspaceContent: Component = const editDialogTitle = createMemo(() => { const row = editingRow(); - if (!row) return 'Edit source'; - return `Edit ${row.name}`; + if (!row) return 'Manage source'; + return `Manage ${row.name}`; }); const editDialogDescription = createMemo(() => { const connection = editingConnection(); - if (!connection) return 'Update this source without leaving the infrastructure manager.'; + if (!connection) return 'Update source state, credentials, and lifecycle actions here.'; const label = describeManagedSourceType(connection.type); const methods = attachedAgentConnections().length > 0 ? ' · API + Pulse Agent' : ''; return `${label}${connection.address ? ` · ${connection.address}` : ''}${methods}`; diff --git a/frontend-modern/src/components/Settings/SettingsPageShell.tsx b/frontend-modern/src/components/Settings/SettingsPageShell.tsx index 895ea52cd..6e063dd81 100644 --- a/frontend-modern/src/components/Settings/SettingsPageShell.tsx +++ b/frontend-modern/src/components/Settings/SettingsPageShell.tsx @@ -1,4 +1,4 @@ -import { Accessor, Component, For, JSX, Setter, Show } from 'solid-js'; +import { Accessor, Component, For, JSX, Setter, Show, createSignal } from 'solid-js'; import ChevronRight from 'lucide-solid/icons/chevron-right'; import { Card } from '@/components/shared/Card'; import { PageHeader } from '@/components/shared/PageHeader'; @@ -8,11 +8,7 @@ import { getSettingsUnsavedChangesBanner, SETTINGS_SHELL_COPY, } from '@/utils/settingsShellPresentation'; -import type { - SettingsHeaderMeta, - SettingsNavGroup, - SettingsTab, -} from './settingsNavigationModel'; +import type { SettingsHeaderMeta, SettingsNavGroup, SettingsTab } from './settingsNavigationModel'; import { isInfrastructureSettingsTab } from './settingsNavigationModel'; interface SettingsPageShellProps { @@ -36,7 +32,29 @@ interface SettingsPageShellProps { } export const SettingsPageShell: Component = (props) => { + const [focusedNavigationExpanded, setFocusedNavigationExpanded] = createSignal(false); const unsavedChangesBanner = () => getSettingsUnsavedChangesBanner(); + const infrastructureWorkspaceActive = () => isInfrastructureSettingsTab(props.activeTab()); + const effectiveSidebarCollapsed = () => + infrastructureWorkspaceActive() && !props.isMobileMenuOpen() + ? !focusedNavigationExpanded() + : props.sidebarCollapsed(); + const collapseSidebar = () => { + if (infrastructureWorkspaceActive() && !props.isMobileMenuOpen()) { + setFocusedNavigationExpanded(false); + return; + } + + props.setSidebarCollapsed(true); + }; + const expandSidebar = () => { + if (infrastructureWorkspaceActive() && !props.isMobileMenuOpen()) { + setFocusedNavigationExpanded(true); + return; + } + + props.setSidebarCollapsed(false); + }; const isSidebarItemActive = (itemId: SettingsTab) => itemId === 'infrastructure-systems' ? isInfrastructureSettingsTab(props.activeTab()) @@ -96,21 +114,21 @@ export const SettingsPageShell: Component = (props) => {
- +

{SETTINGS_SHELL_COPY.navigationTitle}

- +
- +
= (props) => {
@@ -189,13 +207,13 @@ export const SettingsPageShell: Component = (props) => { type="button" aria-current={isActive() ? 'page' : undefined} disabled={item.disabled} - class={`group flex w-full items-center ${props.sidebarCollapsed() ? 'justify-center' : 'justify-between'} lg:rounded-md ${props.sidebarCollapsed() ? 'px-2 py-2.5' : 'px-4 py-3.5 lg:px-3 lg:py-2'} text-[15px] lg:text-sm font-medium transition-colors ${item.disabled ? 'opacity-60 cursor-not-allowed text-muted' : isActive() ? 'lg:bg-blue-50 text-blue-600 dark:lg:bg-blue-900 dark:text-blue-300 lg:dark:text-blue-200 bg-surface' : ' lg:hover:bg-surface-hover hover:text-base-content active:bg-surface-hover lg:active:bg-transparent'}`} + class={`group flex w-full items-center ${effectiveSidebarCollapsed() ? 'justify-center' : 'justify-between'} lg:rounded-md ${effectiveSidebarCollapsed() ? 'px-2 py-2.5' : 'px-4 py-3.5 lg:px-3 lg:py-2'} text-[15px] lg:text-sm font-medium transition-colors ${item.disabled ? 'opacity-60 cursor-not-allowed text-muted' : isActive() ? 'lg:bg-blue-50 text-blue-600 dark:lg:bg-blue-900 dark:text-blue-300 lg:dark:text-blue-200 bg-surface' : ' lg:hover:bg-surface-hover hover:text-base-content active:bg-surface-hover lg:active:bg-transparent'}`} onClick={() => { if (item.disabled) return; props.setActiveTab(item.id); props.setIsMobileMenuOpen(false); }} - title={props.sidebarCollapsed() ? item.label : undefined} + title={effectiveSidebarCollapsed() ? item.label : undefined} >
= (props) => { {...(item.iconProps || {})} />
- + @@ -261,7 +279,9 @@ export const SettingsPageShell: Component = (props) => {
-
{props.children}
+
+ {props.children} +
diff --git a/frontend-modern/src/components/Settings/__tests__/InfrastructureOperationsModel.test.tsx b/frontend-modern/src/components/Settings/__tests__/InfrastructureOperationsModel.test.tsx index e7c014fbb..8215935c6 100644 --- a/frontend-modern/src/components/Settings/__tests__/InfrastructureOperationsModel.test.tsx +++ b/frontend-modern/src/components/Settings/__tests__/InfrastructureOperationsModel.test.tsx @@ -179,13 +179,23 @@ describe('infrastructure operations model', () => { ); expect(infrastructureInstallerSectionSource).toContain('Generate install token'); expect(infrastructureInstallerSectionSource).toContain('Generate token'); - expect(infrastructureInstallerSectionSource).not.toContain('Add infrastructure'); + expect(infrastructureInstallerSectionSource).toContain( + 'This is the Pulse Agent handoff from first-run setup inside Add infrastructure.', + ); expect(infrastructureInstallerSectionSource).toContain('Pulse Agent is a low-overhead background service.'); expect(infrastructureInstallerSectionSource).toContain( 'For Proxmox clusters, keep the cluster API', ); expect(infrastructureInstallerSectionSource).toContain('host-level'); expect(infrastructureInstallerSectionSource).toContain('augmentation.'); + expect(infrastructureInstallerSectionSource).toContain('Installation commands'); + expect(infrastructureInstallerSectionSource).toContain( + 'Generate an install token first. Pulse will then build copy-ready commands', + ); + expect(infrastructureInstallerSectionSource).not.toContain(''); + expect(infrastructureInstallerSectionSource).not.toContain( + 'Copy disabled until an install token is generated', + ); }); it('does not reintroduce the retired reporting state hook on the operations state', async () => { diff --git a/frontend-modern/src/components/Settings/__tests__/InfrastructureWorkspace.test.tsx b/frontend-modern/src/components/Settings/__tests__/InfrastructureWorkspace.test.tsx index 879ef3cc0..2f50f2f4d 100644 --- a/frontend-modern/src/components/Settings/__tests__/InfrastructureWorkspace.test.tsx +++ b/frontend-modern/src/components/Settings/__tests__/InfrastructureWorkspace.test.tsx @@ -287,24 +287,24 @@ describe('InfrastructureWorkspace', () => { renderWorkspace(); await waitFor(() => expect(screen.getByText('Infrastructure systems')).toBeInTheDocument()); - expect(screen.getByText('Start by connecting what Pulse should monitor')).toBeInTheDocument(); expect( - screen.getByText(/Pulse 6 treats platform APIs and host agents as infrastructure sources/i), + screen.getByText(/Add, discover, and verify the platform APIs plus Pulse Agent telemetry/i), ).toBeInTheDocument(); expect(screen.getByRole('button', { name: /Run discovery/i })).toBeInTheDocument(); expect(screen.getByRole('button', { name: /Discovery settings/i })).toBeInTheDocument(); - expect(screen.getByRole('button', { name: /Detect from address/i })).toBeInTheDocument(); - expect(screen.getByRole('button', { name: /Install Pulse Agent/i })).toBeInTheDocument(); - expect(screen.getByRole('button', { name: /Choose source type/i })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /^Add infrastructure$/i })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /Detect address/i })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /^Install agent$/i })).toBeInTheDocument(); const readiness = screen.getByRole('region', { name: /Infrastructure setup confidence/i, }); - expect(within(readiness).getByText('Infrastructure readiness')).toBeInTheDocument(); + expect(within(readiness).getByText('Infrastructure coverage')).toBeInTheDocument(); expect(within(readiness).getByText('Connected systems')).toBeInTheDocument(); expect(within(readiness).getByText('API coverage')).toBeInTheDocument(); expect(within(readiness).getByText('Agent coverage')).toBeInTheDocument(); + expect(within(readiness).getByText('Needs agent')).toBeInTheDocument(); expect(within(readiness).getByText('Discovery')).toBeInTheDocument(); - expect(within(readiness).getAllByText('1 system')).toHaveLength(2); + expect(within(readiness).getAllByText('1 system')).toHaveLength(3); expect(within(readiness).getByText('0 systems')).toBeInTheDocument(); expect(within(readiness).getByText('Discovery off')).toBeInTheDocument(); expect(within(readiness).getByRole('button', { name: /Install agents/i })).toBeInTheDocument(); @@ -316,8 +316,7 @@ describe('InfrastructureWorkspace', () => { expect(screen.getByRole('button', { name: /Add Proxmox VE/i })).toBeInTheDocument(); expect(screen.queryByRole('button', { name: /Add TrueNAS SCALE/i })).toBeNull(); expect(screen.queryByRole('button', { name: /Add host/i })).toBeNull(); - expect(screen.getByRole('button', { name: /^Add infrastructure$/i })).toBeInTheDocument(); - expect(screen.getByRole('button', { name: /Edit/i })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /Manage/i })).toBeInTheDocument(); expect(screen.queryByRole('heading', { name: 'Monitored systems' })).not.toBeInTheDocument(); }); @@ -326,12 +325,12 @@ describe('InfrastructureWorkspace', () => { await waitFor(() => expect(screen.getByText('Infrastructure systems')).toBeInTheDocument()); - fireEvent.click(screen.getByRole('button', { name: /Detect from address/i })); + fireEvent.click(screen.getByRole('button', { name: /Detect address/i })); expect(navigateSpy).toHaveBeenLastCalledWith('/settings/infrastructure?add=detect', { scroll: false, }); - fireEvent.click(screen.getByRole('button', { name: /Install Pulse Agent/i })); + fireEvent.click(screen.getByRole('button', { name: /^Install agent$/i })); expect(navigateSpy).toHaveBeenLastCalledWith('/settings/infrastructure?add=agent', { scroll: false, }); @@ -347,12 +346,116 @@ describe('InfrastructureWorkspace', () => { scroll: false, }); - fireEvent.click(screen.getByRole('button', { name: /Choose source type/i })); + fireEvent.click(screen.getByRole('button', { name: /^Add infrastructure$/i })); expect(navigateSpy).toHaveBeenLastCalledWith('/settings/infrastructure?add=pick', { scroll: false, }); }); + it('keeps source groups in the catalog order instead of count order', async () => { + const pveConnection = connectionFixture({ + id: 'pve:zeus', + type: 'pve', + name: 'zeus', + address: 'https://10.0.0.1:8006', + }); + const towerAgent = connectionFixture({ + id: 'agent:tower', + type: 'agent', + name: 'Tower', + address: 'Tower', + source: 'agent', + capabilities: { supportsPause: false, supportsScope: false, supportsTest: false }, + }); + const miniAgent = connectionFixture({ + id: 'agent:mini', + type: 'agent', + name: 'Mini', + address: 'Mini', + source: 'agent', + capabilities: { supportsPause: false, supportsScope: false, supportsTest: false }, + }); + + connectionState.connections = [pveConnection, towerAgent, miniAgent]; + connectionState.rows = [ + { + id: pveConnection.id, + ownerType: 'pve', + name: 'zeus', + subtitle: 'via platform API', + source: 'api', + host: pveConnection.address, + coverageLabels: ['VMs'], + statusLabel: 'Active', + statusClassName: 'bg-green-100 text-green-800', + agentUpdateCount: 0, + lastActivityText: '1m ago', + enabled: true, + canEdit: true, + canPause: true, + canRemove: true, + isAgent: false, + isCluster: false, + attachedConnections: [], + members: [], + connection: pveConnection, + }, + { + id: towerAgent.id, + ownerType: 'agent', + name: 'Tower', + subtitle: 'via Pulse Agent', + source: 'agent', + host: 'Tower', + coverageLabels: ['Host telemetry'], + statusLabel: 'Active', + statusClassName: 'bg-green-100 text-green-800', + agentUpdateCount: 0, + lastActivityText: '1m ago', + enabled: true, + canEdit: false, + canPause: false, + canRemove: true, + isAgent: true, + isCluster: false, + attachedConnections: [], + members: [], + connection: towerAgent, + }, + { + id: miniAgent.id, + ownerType: 'agent', + name: 'Mini', + subtitle: 'via Pulse Agent', + source: 'agent', + host: 'Mini', + coverageLabels: ['Host telemetry'], + statusLabel: 'Active', + statusClassName: 'bg-green-100 text-green-800', + agentUpdateCount: 0, + lastActivityText: '1m ago', + enabled: true, + canEdit: false, + canPause: false, + canRemove: true, + isAgent: true, + isCluster: false, + attachedConnections: [], + members: [], + connection: miniAgent, + }, + ]; + + renderWorkspace(); + + await waitFor(() => expect(screen.getByText('Proxmox VE')).toBeInTheDocument()); + const pveGroup = screen.getByText('Proxmox VE'); + const hostGroup = screen.getByText('Standalone hosts'); + expect(pveGroup.compareDocumentPosition(hostGroup) & Node.DOCUMENT_POSITION_FOLLOWING).toBe( + Node.DOCUMENT_POSITION_FOLLOWING, + ); + }); + it('switches the source manager layout from measured container width during live resize', async () => { installResizeObserverMock(); Object.defineProperty(window, 'innerWidth', { @@ -510,7 +613,7 @@ describe('InfrastructureWorkspace', () => { expect(onboardingMetricsTrackers[0]?.recordOpened).toHaveBeenCalledTimes(1); }); - it('opens the platform picker from the Add infrastructure footer button', () => { + it('opens the platform picker from the Add infrastructure action', () => { renderWorkspace(); fireEvent.click(screen.getByRole('button', { name: /^Add infrastructure$/i })); @@ -607,18 +710,18 @@ describe('InfrastructureWorkspace', () => { expect(onboardingMetricsTrackers[0]?.recordOpened).toHaveBeenCalledTimes(1); }); - it('opens the edit dialog directly from an existing source card', async () => { + it('opens the manage dialog directly from an existing source card', async () => { renderWorkspace({ pveNodes: () => [{ name: 'zeus', host: 'https://10.0.0.1:8006' } as any], }); await waitFor(() => - expect(screen.getByRole('button', { name: /^Edit$/i })).toBeInTheDocument(), + expect(screen.getByRole('button', { name: /^Manage$/i })).toBeInTheDocument(), ); - fireEvent.click(screen.getByRole('button', { name: /^Edit$/i })); + fireEvent.click(screen.getByRole('button', { name: /^Manage$/i })); await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument()); - expect(screen.getByText('Edit zeus')).toBeInTheDocument(); + expect(screen.getByText('Manage zeus')).toBeInTheDocument(); expect(screen.getByTestId('proxmox-section')).toBeInTheDocument(); }); @@ -679,7 +782,7 @@ describe('InfrastructureWorkspace', () => { expect(screen.getByText('Unraid 7.1.0')).toBeInTheDocument(); expect(screen.getByText('192.168.0.10')).toBeInTheDocument(); - fireEvent.click(screen.getByRole('button', { name: /^Edit$/i })); + fireEvent.click(screen.getByRole('button', { name: /^Manage$/i })); await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument()); expect(screen.getByText('Pulse Agent version')).toBeInTheDocument(); @@ -745,9 +848,9 @@ describe('InfrastructureWorkspace', () => { }); await waitFor(() => - expect(screen.getByRole('button', { name: /^Edit$/i })).toBeInTheDocument(), + expect(screen.getByRole('button', { name: /^Manage$/i })).toBeInTheDocument(), ); - fireEvent.click(screen.getByRole('button', { name: /^Edit$/i })); + fireEvent.click(screen.getByRole('button', { name: /^Manage$/i })); expect(screen.getByText('Agent update')).toBeInTheDocument(); await waitFor(() => expect(screen.getByRole('dialog')).toBeInTheDocument()); @@ -853,9 +956,9 @@ describe('InfrastructureWorkspace', () => { routeState.search = '?add=agent'; renderWorkspace(); - expect(screen.queryByRole('button', { name: /Detect from address/i })).toBeNull(); + expect(screen.queryByRole('button', { name: /Detect address/i })).toBeNull(); expect(screen.queryByRole('button', { name: /Add Proxmox VE/i })).toBeNull(); - expect(screen.queryByRole('button', { name: /^Edit$/i })).toBeNull(); + expect(screen.queryByRole('button', { name: /^Manage$/i })).toBeNull(); expect(screen.queryByRole('dialog')).toBeNull(); expect(screen.queryByTestId('install-section')).toBeNull(); expect(screen.getByText('Infrastructure systems')).toBeInTheDocument(); diff --git a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts index 1f346ff3f..b170d0d2f 100644 --- a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts @@ -45,7 +45,9 @@ describe('settings architecture guardrails', () => { it('keeps infrastructure onboarding route-backed under the shared settings shell', () => { expect(settingsHeaderMetaSource).toContain("'infrastructure-systems': {"); - expect(settingsHeaderMetaSource).toContain('Manage infrastructure sources.'); + expect(settingsHeaderMetaSource).toContain( + 'Add, discover, and verify the infrastructure Pulse monitors.', + ); expect(settingsHeaderMetaSource).toContain("'organization-access': {"); expect(settingsHeaderMetaSource).toContain( 'Manage organization invitations, member roles, and ownership transfers.', @@ -163,7 +165,7 @@ describe('settings architecture guardrails', () => { expect(infrastructureSourceManagerSource).toContain('Run discovery'); expect(infrastructureSourceManagerSource).toContain('Discovery settings'); expect(infrastructureSourceManagerSource).toContain( - 'Configured systems and discovered candidates grouped by platform or host type. Install Pulse Agent on each machine where you want full node-local telemetry.', + "Add, discover, and verify the platform APIs plus Pulse Agent telemetry that make up Pulse's infrastructure model.", ); expect(infrastructureSourceManagerSource).toContain('onReviewDiscoveredSource'); expect(infrastructureSourceManagerSource).toContain('Discovered'); @@ -171,18 +173,16 @@ describe('settings architecture guardrails', () => { expect(infrastructureSourceManagerSource).toContain('TableHeader'); expect(infrastructureSourceManagerSource).toContain('aria-label={product.actionLabel}'); expect(infrastructureSourceManagerSource).toContain('Review'); - expect(infrastructureSourceManagerSource).toContain('Edit'); - expect(infrastructureSourceManagerSource).toContain( - 'Start by connecting what Pulse should monitor', - ); - expect(infrastructureSourceManagerSource).toContain('Detect from address'); - expect(infrastructureSourceManagerSource).toContain('Install Pulse Agent'); + expect(infrastructureSourceManagerSource).toContain('Manage'); + expect(infrastructureSourceManagerSource).toContain('Detect address'); + expect(infrastructureSourceManagerSource).toContain('Install agent'); expect(infrastructureSourceManagerSource).toContain('Choose source type'); expect(infrastructureSourceManagerSource).toContain('getInfrastructureEmptyStateSummary'); - expect(infrastructureSourceManagerSource).toContain('Infrastructure readiness'); + expect(infrastructureSourceManagerSource).toContain('Infrastructure coverage'); expect(infrastructureSourceManagerSource).toContain('Connected systems'); expect(infrastructureSourceManagerSource).toContain('API coverage'); expect(infrastructureSourceManagerSource).toContain('Agent coverage'); + expect(infrastructureSourceManagerSource).toContain('Needs agent'); expect(infrastructureSourceManagerSource).toContain('setupConfidenceAction'); expect(infrastructureSourceManagerSource).not.toContain('Connection types'); expect(infrastructureSourcePickerSource).toContain('Detect from address'); @@ -244,7 +244,7 @@ describe('settings architecture guardrails', () => { 'getInfrastructureSupportSummaryBadges', ); expect(infrastructureOnboardingPresentationSource).toContain( - 'VMware vCenter is also available now.', + 'Supported source types include VMware vCenter', ); expect(nodeCredentialSlotSource).toContain('useNodeModalState(modalProps)'); diff --git a/frontend-modern/src/components/Settings/settingsHeaderMeta.ts b/frontend-modern/src/components/Settings/settingsHeaderMeta.ts index 98c543cfe..14a6cc1f4 100644 --- a/frontend-modern/src/components/Settings/settingsHeaderMeta.ts +++ b/frontend-modern/src/components/Settings/settingsHeaderMeta.ts @@ -9,7 +9,7 @@ import { export const SETTINGS_HEADER_META: SettingsHeaderMetaMap = { 'infrastructure-systems': { title: 'Infrastructure', - description: 'Manage infrastructure sources.', + description: 'Add, discover, and verify the infrastructure Pulse monitors.', }, 'system-general': { title: 'General', @@ -45,11 +45,13 @@ export const SETTINGS_HEADER_META: SettingsHeaderMetaMap = { }, 'support-reporting': { title: 'Data & Reports', - description: 'Export inventory data and generate performance reports from the canonical settings shell.', + description: + 'Export inventory data and generate performance reports from the canonical settings shell.', }, 'support-logs': { title: 'System Logs', - description: 'Inspect the live Pulse log stream and download the captured buffer for support work.', + description: + 'Inspect the live Pulse log stream and download the captured buffer for support work.', }, 'organization-overview': { title: 'Organization Overview', diff --git a/frontend-modern/src/utils/__tests__/discoveryPresentation.test.ts b/frontend-modern/src/utils/__tests__/discoveryPresentation.test.ts index 90806d856..12eae34ce 100644 --- a/frontend-modern/src/utils/__tests__/discoveryPresentation.test.ts +++ b/frontend-modern/src/utils/__tests__/discoveryPresentation.test.ts @@ -64,11 +64,11 @@ describe('discoveryPresentation', () => { it('returns canonical network discovery settings copy', () => { expect(getNetworkDiscoveryPriorityNotice()).toEqual({ - title: 'Configuration precedence', + title: 'Network scan safety', items: [ 'Environment variables still override these settings.', 'Changes made here are saved to system.json immediately.', - 'These settings remain in effect until an environment override replaces them.', + 'Automatic mode can scan every detected interface, including bridge or shared networks; use custom subnets when scope matters.', ], }); diff --git a/frontend-modern/src/utils/discoveryPresentation.ts b/frontend-modern/src/utils/discoveryPresentation.ts index 02fbef979..fc311b3f5 100644 --- a/frontend-modern/src/utils/discoveryPresentation.ts +++ b/frontend-modern/src/utils/discoveryPresentation.ts @@ -64,11 +64,11 @@ export function getDiscoveryNotesEmptyState() { export function getNetworkDiscoveryPriorityNotice() { return { - title: 'Configuration precedence', + title: 'Network scan safety', items: [ 'Environment variables still override these settings.', 'Changes made here are saved to system.json immediately.', - 'These settings remain in effect until an environment override replaces them.', + 'Automatic mode can scan every detected interface, including bridge or shared networks; use custom subnets when scope matters.', ], } as const; } @@ -110,9 +110,7 @@ export function getNetworkDiscoverySubnetPresentation(mode: 'auto' | 'custom') { helpTooltip: 'Use CIDR notation, for example 192.168.1.0/24 or 10.0.0.0/24. Smaller ranges finish more quickly.', placeholder: - mode === 'auto' - ? 'automatic (scan every detected network)' - : '192.168.1.0/24, 10.0.0.0/24', + mode === 'auto' ? 'automatic (scan every detected network)' : '192.168.1.0/24, 10.0.0.0/24', guidance: mode === 'auto' ? 'Automatic mode scans all host network interfaces, which can include shared or corporate networks. Switch to custom subnets for a faster, more targeted scan.'