Align settings cards and add manual host install

This commit is contained in:
rcourtman 2025-10-24 22:43:18 +00:00
parent a6bf2c852b
commit c933eb699c
2 changed files with 177 additions and 77 deletions

View file

@ -365,94 +365,94 @@ WantedBy=multi-user.target`;
};
return (
<div class="space-y-8">
<Card class="space-y-5">
<div>
<div class="space-y-6">
<Card padding="lg" class="space-y-5">
<div class="space-y-1">
<h3 class="text-base font-semibold text-gray-900 dark:text-gray-100">Add a Docker host</h3>
<p class="text-sm text-gray-600 dark:text-gray-400">
Run this command as root on your Docker host to start monitoring.
</p>
</div>
<Show when={requiresToken()}>
<div class="space-y-3">
<div class="space-y-1">
<p class="text-sm font-semibold text-gray-900 dark:text-gray-100">Generate API token</p>
<p class="text-sm text-gray-600 dark:text-gray-400">
Create a fresh token scoped to <code>{DOCKER_REPORT_SCOPE}</code>
<div class="space-y-5">
<Show when={requiresToken()}>
<div class="space-y-3">
<div class="space-y-1">
<p class="text-sm font-semibold text-gray-900 dark:text-gray-100">Generate API token</p>
<p class="text-sm text-gray-600 dark:text-gray-400">
Create a fresh token scoped to <code>{DOCKER_REPORT_SCOPE}</code>
</p>
</div>
<div class="flex gap-2">
<input
type="text"
value={tokenName()}
onInput={(e) => setTokenName(e.currentTarget.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && !isGeneratingToken()) {
handleGenerateToken();
}
}}
placeholder="Token name (optional)"
class="flex-1 rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-200 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100 dark:focus:border-blue-400 dark:focus:ring-blue-900/60"
/>
<button
type="button"
onClick={handleGenerateToken}
disabled={isGeneratingToken()}
class="inline-flex items-center justify-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-60"
>
{isGeneratingToken() ? 'Generating…' : currentToken() ? 'Generate another' : 'Generate token'}
</button>
</div>
<Show when={latestRecord()}>
<div class="flex items-center gap-2 rounded-lg border border-blue-200 bg-blue-50 px-4 py-2 text-xs text-blue-800 dark:border-blue-800 dark:bg-blue-900/20 dark:text-blue-200">
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
</svg>
<span>
Token <strong>{latestRecord()?.name}</strong> created and inserted into the command below.
</span>
</div>
</Show>
</div>
</Show>
<Show when={showInstallCommand()}>
<div class="space-y-3">
<div class="flex items-center justify-between">
<label class="text-sm font-semibold text-gray-900 dark:text-gray-100">Install command</label>
<button
type="button"
onClick={async () => {
const command = getInstallCommandTemplate().replace(TOKEN_PLACEHOLDER, currentToken() || TOKEN_PLACEHOLDER);
const success = await copyToClipboard(command);
if (typeof window !== 'undefined' && window.showToast) {
window.showToast(success ? 'success' : 'error', success ? 'Copied!' : 'Failed to copy');
}
}}
class="px-3 py-1.5 text-xs font-medium rounded-lg transition-colors bg-blue-600 text-white hover:bg-blue-700"
>
Copy command
</button>
</div>
<pre class="overflow-x-auto rounded-md bg-gray-900/90 p-3 text-xs text-gray-100">
<code>{getInstallCommandTemplate().replace(TOKEN_PLACEHOLDER, currentToken() || TOKEN_PLACEHOLDER)}</code>
</pre>
<p class="text-xs text-gray-500 dark:text-gray-400">
Run as root on your Docker host. The installer downloads the agent, creates a systemd service, and starts reporting automatically.
</p>
</div>
</Show>
<div class="flex gap-2">
<input
type="text"
value={tokenName()}
onInput={(e) => setTokenName(e.currentTarget.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' && !isGeneratingToken()) {
handleGenerateToken();
}
}}
placeholder="Token name (optional)"
class="flex-1 rounded-lg border border-gray-300 bg-white px-3 py-2 text-sm text-gray-900 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-200 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-100 dark:focus:border-blue-400 dark:focus:ring-blue-900/60"
/>
<button
type="button"
onClick={handleGenerateToken}
disabled={isGeneratingToken()}
class="inline-flex items-center justify-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-60"
>
{isGeneratingToken() ? 'Generating…' : currentToken() ? 'Generate another' : 'Generate token'}
</button>
</div>
<Show when={latestRecord()}>
<div class="flex items-center gap-2 rounded-lg border border-green-200 bg-green-50 px-4 py-2 text-xs text-green-800 dark:border-green-800 dark:bg-green-900/20 dark:text-green-200">
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M5 13l4 4L19 7" />
</svg>
<span>
Token <strong>{latestRecord()?.name}</strong> created and inserted into the command below.
</span>
</div>
</Show>
</div>
</Show>
<Show when={showInstallCommand()}>
<div class="space-y-2">
<div class="flex items-center justify-between">
<label class="text-sm font-semibold text-gray-900 dark:text-gray-100">Install command</label>
<button
type="button"
onClick={async () => {
const command = getInstallCommandTemplate().replace(TOKEN_PLACEHOLDER, currentToken() || TOKEN_PLACEHOLDER);
const success = await copyToClipboard(command);
if (typeof window !== 'undefined' && window.showToast) {
window.showToast(success ? 'success' : 'error', success ? 'Copied!' : 'Failed to copy');
}
}}
class="px-3 py-1.5 text-xs font-medium rounded-lg transition-colors bg-blue-600 text-white hover:bg-blue-700"
>
Copy command
</button>
</div>
<div class="relative rounded-lg border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-900 p-3 overflow-x-auto">
<code class="text-sm text-gray-900 dark:text-gray-100 font-mono break-all">
{getInstallCommandTemplate().replace(TOKEN_PLACEHOLDER, currentToken() || TOKEN_PLACEHOLDER)}
</code>
</div>
<Show when={requiresToken() && !currentToken()}>
<p class="text-xs text-gray-500 dark:text-gray-400">
Run as root on your Docker host. The installer downloads the agent, creates a systemd service, and starts reporting automatically.
Generate a token to see the install command.
</p>
</div>
</Show>
<Show when={requiresToken() && !currentToken()}>
<p class="text-xs text-gray-500 dark:text-gray-400">
Generate a token to see the install command.
</p>
</Show>
</Show>
</div>
<details class="rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 text-sm text-gray-700 dark:border-gray-700 dark:bg-gray-800/50 dark:text-gray-300">
<summary class="cursor-pointer text-sm font-medium text-gray-900 dark:text-gray-100">

View file

@ -234,6 +234,28 @@ export const HostAgents: Component = () => {
return currentToken() || 'disabled';
};
const getSystemdServiceUnit = () => `[Unit]
Description=Pulse Host Agent
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=/usr/local/bin/pulse-host-agent --url ${pulseUrl()} --token ${resolvedToken()} --interval 30s
Restart=always
RestartSec=5s
User=root
[Install]
WantedBy=multi-user.target`;
const getManualUninstallCommand = () =>
`sudo systemctl stop pulse-host-agent && \\
sudo systemctl disable pulse-host-agent && \\
sudo rm -f /etc/systemd/system/pulse-host-agent.service && \\
sudo rm -f /usr/local/bin/pulse-host-agent && \\
sudo systemctl daemon-reload`;
return (
<div class="space-y-6">
<Card padding="lg" class="space-y-5">
@ -365,6 +387,84 @@ export const HostAgents: Component = () => {
)}
</For>
</div>
<details class="rounded-lg border border-gray-200 bg-gray-50 px-4 py-3 text-sm text-gray-700 dark:border-gray-700 dark:bg-gray-800/50 dark:text-gray-300">
<summary class="cursor-pointer text-sm font-medium text-gray-900 dark:text-gray-100">
Advanced options (manual install & uninstall)
</summary>
<div class="mt-3 space-y-4">
<div class="space-y-3 rounded-lg border border-gray-200 bg-white p-3 text-xs dark:border-gray-700 dark:bg-gray-900">
<p class="font-medium text-gray-900 dark:text-gray-100">Manual Linux install</p>
<p class="text-xs text-gray-500 dark:text-gray-400">
Build the agent from source and manage the service yourself instead of using the helper script.
</p>
<p class="font-medium text-gray-900 dark:text-gray-100">1. Build the binary</p>
<div class="rounded bg-gray-900 p-3 font-mono text-xs text-gray-100 dark:bg-gray-950">
<code>
cd /opt/pulse
<br />
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o pulse-host-agent ./cmd/pulse-host-agent
</code>
</div>
<p class="font-medium text-gray-900 dark:text-gray-100">2. Copy to host</p>
<div class="rounded bg-gray-900 p-3 font-mono text-xs text-gray-100 dark:bg-gray-950">
<code>
scp pulse-host-agent user@host:/usr/local/bin/
<br />
ssh user@host sudo chmod +x /usr/local/bin/pulse-host-agent
</code>
</div>
<p class="font-medium text-gray-900 dark:text-gray-100">3. Systemd service template</p>
<div class="relative">
<button
type="button"
onClick={async () => {
const success = await copyToClipboard(getSystemdServiceUnit());
if (typeof window !== 'undefined' && window.showToast) {
window.showToast(success ? 'success' : 'error', success ? 'Copied to clipboard' : 'Failed to copy to clipboard');
}
}}
class="absolute right-2 top-2 rounded-lg bg-gray-700 px-3 py-1.5 text-xs font-medium text-gray-200 transition-colors hover:bg-gray-600"
>
Copy
</button>
<div class="rounded bg-gray-900 p-3 font-mono text-xs text-gray-100 dark:bg-gray-950 overflow-x-auto">
<pre>{getSystemdServiceUnit()}</pre>
</div>
</div>
<p class="font-medium text-gray-900 dark:text-gray-100">4. Enable & start</p>
<div class="rounded bg-gray-900 p-3 font-mono text-xs text-gray-100 dark:bg-gray-950">
<code>
sudo systemctl daemon-reload
<br />
sudo systemctl enable --now pulse-host-agent
</code>
</div>
</div>
<div>
<p class="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400">Manual uninstall</p>
<div class="mt-2 flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-3">
<code class="flex-1 break-all rounded bg-gray-900 px-3 py-2 font-mono text-xs text-gray-100 dark:bg-gray-950">
{getManualUninstallCommand()}
</code>
<button
type="button"
onClick={async () => {
const success = await copyToClipboard(getManualUninstallCommand());
if (typeof window !== 'undefined' && window.showToast) {
window.showToast(success ? 'success' : 'error', success ? 'Copied to clipboard' : 'Failed to copy to clipboard');
}
}}
class="self-start rounded-lg bg-red-50 px-3 py-1.5 text-xs font-medium text-red-700 transition-colors hover:bg-red-100 dark:bg-red-900/30 dark:text-red-300 dark:hover:bg-red-900/50"
>
Copy
</button>
</div>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
Stops the agent, removes the systemd unit, and deletes the binary.
</p>
</div>
</div>
</details>
</div>
</Show>