mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-24 12:13:28 +00:00
feat(connections): polish explainer with agent capability + trust chips
The first pass explained both modes but gave users no reason to choose the agent when the API also covers their platform. Polish the layout and add concrete positives grounded in what the agent actually ships: - Two-column split with a divider and a subtle blue-tinted accent on the Agent panel. Agent icon badge uses the same accent so it pairs visually without shouting. - "Recommended" pill next to Pulse Unified Agent — it unlocks more features, so the default bias is toward installing it. - Capability chips on the agent panel: Hardware telemetry, Assistant commands, Patrol remediation. These match what agentexec, hostagent, and ai/patrol_findings actually do at runtime. - Trust strip of verifiable facts: Single Go binary · ~13 MB download · No runtime dependencies · Open source. Specific numbers beat vague adjectives like "lightweight" — the ~13 MB figure is the actual compressed size of pulse-agent-v6.0.0-rc.1-linux-amd64.tar.gz. - Tests pin the capability labels and trust facts so the benefit copy can't silently drift.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { Component, Show, createSignal } from 'solid-js';
|
||||
import { Component, For, Show, createSignal } from 'solid-js';
|
||||
import { Cloud, Cpu, X } from 'lucide-solid';
|
||||
|
||||
const DISMISS_KEY = 'pulse.infrastructure.explainer.dismissed.v1';
|
||||
@@ -21,6 +21,19 @@ const persistDismissed = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const AGENT_CAPABILITIES = [
|
||||
'Hardware telemetry',
|
||||
'Assistant commands',
|
||||
'Patrol remediation',
|
||||
];
|
||||
|
||||
const AGENT_FACTS = [
|
||||
'Single Go binary',
|
||||
'~13 MB download',
|
||||
'No runtime dependencies',
|
||||
'Open source',
|
||||
];
|
||||
|
||||
export const ConnectionsExplainer: Component = () => {
|
||||
const [dismissed, setDismissed] = createSignal(readDismissed());
|
||||
|
||||
@@ -33,61 +46,104 @@ export const ConnectionsExplainer: Component = () => {
|
||||
<Show when={!dismissed()}>
|
||||
<section
|
||||
aria-label="How to connect infrastructure"
|
||||
class="relative rounded-md border border-border bg-surface-alt px-4 py-4"
|
||||
class="relative overflow-hidden rounded-lg border border-border bg-surface"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDismiss}
|
||||
aria-label="Dismiss"
|
||||
class="absolute right-2 top-2 inline-flex items-center justify-center rounded-md p-1 text-muted transition-colors hover:bg-surface-hover hover:text-base-content"
|
||||
>
|
||||
<X class="h-4 w-4" aria-hidden="true" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDismiss}
|
||||
aria-label="Dismiss"
|
||||
class="absolute right-2 top-2 z-10 inline-flex items-center justify-center rounded-md p-1 text-muted transition-colors hover:bg-surface-hover hover:text-base-content"
|
||||
>
|
||||
<X class="h-4 w-4" aria-hidden="true" />
|
||||
</button>
|
||||
|
||||
<div class="mb-3 pr-8">
|
||||
<h3 class="text-sm font-semibold text-base-content">
|
||||
Two ways to connect infrastructure
|
||||
</h3>
|
||||
<p class="text-xs text-muted">
|
||||
Pick whichever fits the target. You can mix both on the same host.
|
||||
</p>
|
||||
</div>
|
||||
<div class="border-b border-border px-5 py-3">
|
||||
<h3 class="text-sm font-semibold text-base-content">
|
||||
Two ways to connect infrastructure
|
||||
</h3>
|
||||
<p class="mt-0.5 text-xs text-muted">
|
||||
Pick whichever fits the target. You can mix both on the same host.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div class="flex gap-3">
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="mt-0.5 flex h-8 w-8 flex-none items-center justify-center rounded-md border border-border bg-surface text-muted"
|
||||
>
|
||||
<Cloud class="h-4 w-4" />
|
||||
</div>
|
||||
<div class="min-w-0 space-y-1">
|
||||
<div class="text-sm font-medium text-base-content">Platform API</div>
|
||||
<p class="text-xs text-muted">
|
||||
<div class="grid grid-cols-1 divide-border md:grid-cols-2 md:divide-x">
|
||||
<div class="p-5">
|
||||
<div class="flex items-start gap-3">
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="flex h-9 w-9 flex-none items-center justify-center rounded-lg border border-border bg-surface-alt text-base-content"
|
||||
>
|
||||
<Cloud class="h-4 w-4" />
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<div class="text-sm font-semibold text-base-content">Platform API</div>
|
||||
<div class="text-xs text-muted">Polled from the platform</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="mt-3 text-xs leading-relaxed text-muted">
|
||||
Pulse polls the platform's own API (Proxmox VE / PBS / PMG, VMware,
|
||||
TrueNAS). Fastest to set up; coverage matches what the platform exposes.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3">
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="mt-0.5 flex h-8 w-8 flex-none items-center justify-center rounded-md border border-border bg-surface text-muted"
|
||||
>
|
||||
<Cpu class="h-4 w-4" />
|
||||
</div>
|
||||
<div class="min-w-0 space-y-1">
|
||||
<div class="text-sm font-medium text-base-content">Pulse Unified Agent</div>
|
||||
<p class="text-xs text-muted">
|
||||
<div class="relative bg-blue-50/40 p-5 dark:bg-blue-950/20">
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="absolute inset-y-0 left-0 w-0.5 bg-blue-500 md:hidden"
|
||||
/>
|
||||
<div class="flex items-start gap-3">
|
||||
<div
|
||||
aria-hidden="true"
|
||||
class="flex h-9 w-9 flex-none items-center justify-center rounded-lg border border-blue-200 bg-blue-100 text-blue-700 dark:border-blue-900 dark:bg-blue-900/40 dark:text-blue-300"
|
||||
>
|
||||
<Cpu class="h-4 w-4" />
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="text-sm font-semibold text-base-content">
|
||||
Pulse Unified Agent
|
||||
</span>
|
||||
<span class="inline-flex items-center rounded-full border border-blue-200 bg-blue-100 px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide text-blue-700 dark:border-blue-900 dark:bg-blue-900/40 dark:text-blue-300">
|
||||
Recommended
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-xs text-muted">Runs on the host</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="mt-3 text-xs leading-relaxed text-muted">
|
||||
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.
|
||||
</p>
|
||||
|
||||
<div class="mt-3 flex flex-wrap gap-1.5">
|
||||
<For each={AGENT_CAPABILITIES}>
|
||||
{(label) => (
|
||||
<span class="inline-flex items-center rounded-full border border-border bg-surface px-2 py-0.5 text-[11px] font-medium text-base-content">
|
||||
{label}
|
||||
</span>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
|
||||
<ul class="mt-3 flex flex-wrap gap-x-3 gap-y-1 text-[11px] text-muted">
|
||||
<For each={AGENT_FACTS}>
|
||||
{(fact, index) => (
|
||||
<li class="flex items-center gap-1.5">
|
||||
<Show when={index() > 0}>
|
||||
<span aria-hidden="true" class="text-border">
|
||||
·
|
||||
</span>
|
||||
</Show>
|
||||
{fact}
|
||||
</li>
|
||||
)}
|
||||
</For>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</Show>
|
||||
);
|
||||
|
||||
@@ -21,6 +21,17 @@ describe('ConnectionsExplainer', () => {
|
||||
expect(screen.getByText('Pulse Unified Agent')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('surfaces concrete agent capabilities and trust facts users care about', () => {
|
||||
render(() => <ConnectionsExplainer />);
|
||||
|
||||
for (const capability of ['Hardware telemetry', 'Assistant commands', 'Patrol remediation']) {
|
||||
expect(screen.getByText(capability)).toBeInTheDocument();
|
||||
}
|
||||
for (const fact of ['Single Go binary', '~13 MB download', 'No runtime dependencies', 'Open source']) {
|
||||
expect(screen.getByText(fact)).toBeInTheDocument();
|
||||
}
|
||||
});
|
||||
|
||||
it('hides itself and persists dismissal to localStorage when closed', () => {
|
||||
const { container } = render(() => <ConnectionsExplainer />);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user