From c0de1a402134cfc8d25ccfdfd74276b29642a4ed Mon Sep 17 00:00:00 2001 From: rcourtman Date: Mon, 20 Apr 2026 09:27:02 +0100 Subject: [PATCH] feat(connections): explain API vs Unified Agent above the ledger MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New users landing on an empty monitored-systems ledger had no way to know when they should connect via a platform API vs install the Pulse Unified Agent, and no signal that the agent unlocks Assistant command execution and Patrol remediation on the host. - ConnectionsExplainer: dismissible (localStorage) callout above the table, two-mode grid with lucide glyphs (Cloud for API, Cpu for agent), short copy tying each mode to concrete capabilities. - Mount in InfrastructureWorkspace only in ledger mode (hidden during add/edit flows so it doesn't compete for attention). - Rename the agent type label from "Pulse agent" to "Pulse Unified Agent" in CONNECTION_TYPE_LABELS. Inline prose elsewhere (tooltips, install guides) keeps the conversational "Pulse agent" form — the formal branded name only lives where the ledger treats it as a type. --- .../Settings/ConnectionsExplainer.tsx | 94 +++++++++++++++++++ .../Settings/InfrastructureWorkspace.tsx | 20 ++-- .../__tests__/ConnectionsExplainer.test.tsx | 39 ++++++++ .../Settings/useConnectionsLedger.ts | 2 +- 4 files changed, 146 insertions(+), 9 deletions(-) create mode 100644 frontend-modern/src/components/Settings/ConnectionsExplainer.tsx create mode 100644 frontend-modern/src/components/Settings/__tests__/ConnectionsExplainer.test.tsx diff --git a/frontend-modern/src/components/Settings/ConnectionsExplainer.tsx b/frontend-modern/src/components/Settings/ConnectionsExplainer.tsx new file mode 100644 index 000000000..ace826319 --- /dev/null +++ b/frontend-modern/src/components/Settings/ConnectionsExplainer.tsx @@ -0,0 +1,94 @@ +import { Component, Show, createSignal } from 'solid-js'; +import { Cloud, Cpu, X } from 'lucide-solid'; + +const DISMISS_KEY = 'pulse.infrastructure.explainer.dismissed.v1'; + +const readDismissed = (): boolean => { + if (typeof window === 'undefined') return false; + try { + return window.localStorage.getItem(DISMISS_KEY) === '1'; + } catch { + return false; + } +}; + +const persistDismissed = () => { + if (typeof window === 'undefined') return; + try { + window.localStorage.setItem(DISMISS_KEY, '1'); + } catch { + // Ignore storage failures (private mode, quota) — the in-memory signal is enough for this session. + } +}; + +export const ConnectionsExplainer: Component = () => { + const [dismissed, setDismissed] = createSignal(readDismissed()); + + const handleDismiss = () => { + persistDismissed(); + setDismissed(true); + }; + + return ( + +
+ + +
+

+ Two ways to connect infrastructure +

+

+ Pick whichever fits the target. You can mix both on the same host. +

+
+ +
+
+ +
+
Platform API
+

+ Pulse polls the platform's own API (Proxmox VE / PBS / PMG, VMware, + TrueNAS). Fastest to set up; coverage matches what the platform exposes. +

+
+
+ +
+ +
+
Pulse Unified Agent
+

+ Installs on the host itself. Use when there's no API (bare-metal Linux, + Unraid), when you want data the API can't surface (CPU/disk temps, SMART, + power, Ceph/RAID), or to let Assistant and Patrol run commands and fixes + on the host. +

+
+
+
+
+
+ ); +}; diff --git a/frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx b/frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx index 55a5d0d7d..a7902cb2c 100644 --- a/frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx +++ b/frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx @@ -4,6 +4,7 @@ import { presentationPolicyIsReadOnly } from '@/stores/sessionPresentationPolicy import { copyToClipboard } from '@/utils/clipboard'; import { notificationStore } from '@/stores/notifications'; import { AgentProfilesPanel } from './AgentProfilesPanel'; +import { ConnectionsExplainer } from './ConnectionsExplainer'; import { ConnectionsTable, type ConnectionsTableHeaderAction } from './ConnectionsTable'; import { ConnectionEditor } from './ConnectionEditor/ConnectionEditor'; import { NodeCredentialSlot } from './ConnectionEditor/CredentialSlots/NodeCredentialSlot'; @@ -197,14 +198,17 @@ const InfrastructureWorkspaceContent: Component =
void handleCopy(text)} - /> + <> + + void handleCopy(text)} + /> + } > diff --git a/frontend-modern/src/components/Settings/__tests__/ConnectionsExplainer.test.tsx b/frontend-modern/src/components/Settings/__tests__/ConnectionsExplainer.test.tsx new file mode 100644 index 000000000..5ec4af30c --- /dev/null +++ b/frontend-modern/src/components/Settings/__tests__/ConnectionsExplainer.test.tsx @@ -0,0 +1,39 @@ +import { cleanup, fireEvent, render, screen } from '@solidjs/testing-library'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { ConnectionsExplainer } from '../ConnectionsExplainer'; + +const DISMISS_KEY = 'pulse.infrastructure.explainer.dismissed.v1'; + +describe('ConnectionsExplainer', () => { + beforeEach(() => { + window.localStorage.clear(); + }); + + afterEach(() => { + cleanup(); + window.localStorage.clear(); + }); + + it('names both ingestion modes with their branded labels', () => { + render(() => ); + + expect(screen.getByText('Platform API')).toBeInTheDocument(); + expect(screen.getByText('Pulse Unified Agent')).toBeInTheDocument(); + }); + + it('hides itself and persists dismissal to localStorage when closed', () => { + const { container } = render(() => ); + + fireEvent.click(screen.getByRole('button', { name: 'Dismiss' })); + + expect(container.querySelector('section')).toBeNull(); + expect(window.localStorage.getItem(DISMISS_KEY)).toBe('1'); + }); + + it('stays dismissed across remounts once localStorage records it', () => { + window.localStorage.setItem(DISMISS_KEY, '1'); + const { container } = render(() => ); + + expect(container.querySelector('section')).toBeNull(); + }); +}); diff --git a/frontend-modern/src/components/Settings/useConnectionsLedger.ts b/frontend-modern/src/components/Settings/useConnectionsLedger.ts index e6756b771..6d78aa78d 100644 --- a/frontend-modern/src/components/Settings/useConnectionsLedger.ts +++ b/frontend-modern/src/components/Settings/useConnectionsLedger.ts @@ -15,7 +15,7 @@ export const CONNECTION_TYPE_LABELS: Record = { pmg: 'Proxmox Mail Gateway', vmware: 'VMware vCenter', truenas: 'TrueNAS', - agent: 'Pulse agent', + agent: 'Pulse Unified Agent', docker: 'Docker', kubernetes: 'Kubernetes', };