Clarify first-run unlock guidance

This commit is contained in:
rcourtman 2026-03-27 10:13:44 +00:00
parent cefcddd14b
commit a17987691c
5 changed files with 277 additions and 60 deletions

View file

@ -1,4 +1,4 @@
import { Component, createSignal, Show } from 'solid-js';
import { Component, createMemo, createSignal, onMount, Show } from 'solid-js';
import { showError, showSuccess } from '@/utils/toast';
import { apiFetch, apiFetchJSON } from '@/utils/apiClient';
import { logger } from '@/utils/logger';
@ -19,6 +19,7 @@ export const WelcomeStep: Component<WelcomeStepProps> = (props) => {
const [isDocker, setIsDocker] = createSignal(false);
const [inContainer, setInContainer] = createSignal(false);
const [lxcCtid, setLxcCtid] = createSignal('');
const [dockerContainerName, setDockerContainerName] = createSignal('');
const [copied, setCopied] = createSignal(false);
const copyCommand = async () => {
@ -40,20 +41,23 @@ export const WelcomeStep: Component<WelcomeStepProps> = (props) => {
isDocker?: boolean;
inContainer?: boolean;
lxcCtid?: string;
dockerContainerName?: string;
}>('/api/security/status');
if (data?.bootstrapTokenPath) {
setTokenPath(data.bootstrapTokenPath);
setIsDocker(data.isDocker || false);
setInContainer(data.inContainer || false);
setLxcCtid(data.lxcCtid || '');
setDockerContainerName(data.dockerContainerName || '');
}
} catch (error) {
logger.error('Failed to fetch bootstrap info:', error);
}
};
// Call on component load
fetchBootstrapInfo();
onMount(() => {
void fetchBootstrapInfo();
});
const handleUnlock = async () => {
if (!props.bootstrapToken.trim()) {
@ -85,7 +89,7 @@ export const WelcomeStep: Component<WelcomeStepProps> = (props) => {
const getTokenCommand = () => {
const path = tokenPath() || '/etc/pulse/.bootstrap_token';
if (isDocker()) {
return `docker exec <container> cat ${path}`;
return `docker exec ${dockerContainerName() || '<pulse-container>'} cat ${path}`;
}
if (inContainer() && lxcCtid()) {
return `pct exec ${lxcCtid()} -- cat ${path}`;
@ -96,6 +100,42 @@ export const WelcomeStep: Component<WelcomeStepProps> = (props) => {
return `cat ${path}`;
};
const deploymentLabel = createMemo(() => {
if (isDocker()) {
return 'Docker deployment';
}
if (inContainer() && lxcCtid()) {
return 'LXC container';
}
if (inContainer()) {
return 'Containerized deployment';
}
return 'Direct host install';
});
const deploymentHint = createMemo(() => {
const path = tokenPath() || '/etc/pulse/.bootstrap_token';
if (isDocker()) {
return dockerContainerName()
? `Pulse appears to be running in Docker as container "${dockerContainerName()}". Run the command on the Docker host so you can read ${path} from that container.`
: `Pulse appears to be running in Docker. Run the command on the Docker host and replace <pulse-container> with the running Pulse container name.`;
}
if (inContainer() && lxcCtid()) {
return `Pulse appears to be running in LXC container ${lxcCtid()}. Run the command on the Proxmox host so you can execute into that container and read ${path}.`;
}
if (inContainer()) {
return `Pulse appears to be running in a containerized environment. Run the command from the host that manages the container so you can read ${path}.`;
}
return `Run the command directly in a shell on the Pulse server to read ${path}.`;
});
const unlockHelp = createMemo(() => {
if (isDocker()) {
return 'This one-time bootstrap token only unlocks first-run setup. After verification, you will create the admin account and Pulse will generate the long-lived API token separately.';
}
return 'This one-time bootstrap token only unlocks first-run setup on this Pulse server. It is not your admin password and it is not the API token you will use after setup.';
});
return (
<div class="text-center relative">
{/* Logo */}
@ -117,6 +157,36 @@ export const WelcomeStep: Component<WelcomeStepProps> = (props) => {
</p>
</div>
<div class="mb-8 grid gap-3 text-left sm:grid-cols-3">
<div class="rounded-md border border-border bg-surface px-4 py-3">
<div class="text-[11px] font-semibold uppercase tracking-wide text-blue-700 dark:text-blue-300">
Step 1
</div>
<div class="mt-1 text-sm font-semibold text-base-content">Unlock this Pulse server</div>
<p class="mt-1 text-xs text-muted">
Read the one-time bootstrap token from the system where Pulse is installed.
</p>
</div>
<div class="rounded-md border border-border bg-surface px-4 py-3">
<div class="text-[11px] font-semibold uppercase tracking-wide text-blue-700 dark:text-blue-300">
Step 2
</div>
<div class="mt-1 text-sm font-semibold text-base-content">Create the admin account</div>
<p class="mt-1 text-xs text-muted">
Set the first login and let Pulse generate the credentials you need to save.
</p>
</div>
<div class="rounded-md border border-border bg-surface px-4 py-3">
<div class="text-[11px] font-semibold uppercase tracking-wide text-blue-700 dark:text-blue-300">
Step 3
</div>
<div class="mt-1 text-sm font-semibold text-base-content">Install the first host</div>
<p class="mt-1 text-xs text-muted">
Open Infrastructure Install and connect the first system you want Pulse to monitor.
</p>
</div>
</div>
{/* Bootstrap token unlock */}
<Show when={!props.isUnlocked}>
<div class="p-8 max-w-lg mx-auto bg-surface border border-border rounded-md text-left animate-slide-up delay-300 relative group">
@ -125,10 +195,22 @@ export const WelcomeStep: Component<WelcomeStepProps> = (props) => {
Unlock Setup
</h3>
<p class="text-sm text-muted mb-6">
Run the following command on the machine where Pulse is installed to retrieve the
secure bootstrap token:
Run the following command on the Pulse server to retrieve the one-time bootstrap
token that unlocks this wizard:
</p>
<div class="mb-4 rounded-md border border-blue-200 bg-blue-50 px-4 py-3 dark:border-blue-800 dark:bg-blue-950/40">
<div class="text-[11px] font-semibold uppercase tracking-wide text-blue-700 dark:text-blue-300">
Where to run it
</div>
<div class="mt-1 flex items-center gap-2">
<span class="inline-flex items-center rounded-full bg-blue-100 px-2 py-0.5 text-[11px] font-semibold text-blue-700 dark:bg-blue-900 dark:text-blue-200">
{deploymentLabel()}
</span>
</div>
<p class="mt-2 text-sm text-blue-900 dark:text-blue-100">{deploymentHint()}</p>
</div>
<div class="mb-8">
<div class="bg-base rounded-md p-4 font-mono text-sm text-emerald-400 border border-border-subtle flex items-center justify-between">
<div class="flex items-center space-x-3 overflow-x-auto scrollbar-hide">
@ -147,6 +229,13 @@ export const WelcomeStep: Component<WelcomeStepProps> = (props) => {
</div>
</div>
<div class="mb-6 rounded-md border border-emerald-200 bg-emerald-50 px-4 py-3 dark:border-emerald-800 dark:bg-emerald-950/40">
<div class="text-[11px] font-semibold uppercase tracking-wide text-emerald-700 dark:text-emerald-300">
What this token does
</div>
<p class="mt-2 text-sm text-emerald-900 dark:text-emerald-100">{unlockHelp()}</p>
</div>
<div class="space-y-4">
<input
type="text"
@ -163,12 +252,17 @@ export const WelcomeStep: Component<WelcomeStepProps> = (props) => {
}, 400);
}
}}
onKeyPress={(e) => e.key === 'Enter' && handleUnlock()}
onKeyDown={(e) => e.key === 'Enter' && void handleUnlock()}
class="w-full px-5 py-3.5 bg-surface border border-border rounded-md text-base-content placeholder-slate-400 focus:outline-none focus:ring-0 focus:border-blue-500 transition-colors font-mono"
placeholder="Paste your bootstrap token"
autofocus
/>
<p class="text-xs text-muted">
After Pulse verifies this token, the next step is creating the admin account for
this server.
</p>
<button
onClick={handleUnlock}
disabled={isValidating() || !props.bootstrapToken.trim()}
@ -195,10 +289,10 @@ export const WelcomeStep: Component<WelcomeStepProps> = (props) => {
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
Validating...
Verifying bootstrap token...
</>
) : (
'Continue to Setup →'
'Verify bootstrap token →'
)}
</button>
</div>
@ -212,7 +306,7 @@ export const WelcomeStep: Component<WelcomeStepProps> = (props) => {
onClick={props.onNext}
class="py-4 px-10 bg-blue-600 hover:bg-blue-700 text-white text-lg font-medium rounded-md transition-colors duration-200"
>
Get Started
Continue to Security
</button>
</div>
</Show>