feat: Add multi-step Setup Wizard for Pulse 5.0 onboarding

- New SetupWizard component with 5-step flow:
  1. Welcome: Bootstrap token unlock, platform showcase
  2. Security: Admin account + API token creation
  3. Connect: Multi-platform infrastructure (Proxmox, Docker, K8s)
  4. Features: AI and auto-updates toggles
  5. Complete: Credentials display with copy/download

- Replaced FirstRunSetup with SetupWizard in Login.tsx
- Added Install Update button to UpdatesSettingsPanel
- Enhanced UpdatesSettingsPanel with update plan integration
- Added UpdateConfirmationModal to Settings for inline updates

Positions Pulse as a unified infrastructure monitoring platform,
not just a Proxmox-specific tool.
This commit is contained in:
rcourtman 2025-12-13 15:08:47 +00:00
parent 5c4069cdbf
commit c3fdb6d6f8
12 changed files with 1707 additions and 198 deletions

View file

@ -0,0 +1,156 @@
import { Component, createSignal, Show } from 'solid-js';
import { showError, showSuccess } from '@/utils/toast';
interface WelcomeStepProps {
onNext: () => void;
bootstrapToken: string;
setBootstrapToken: (token: string) => void;
isUnlocked: boolean;
setIsUnlocked: (unlocked: boolean) => void;
}
export const WelcomeStep: Component<WelcomeStepProps> = (props) => {
const [isValidating, setIsValidating] = createSignal(false);
const [tokenPath, setTokenPath] = createSignal('');
const [isDocker, setIsDocker] = createSignal(false);
const [inContainer, setInContainer] = createSignal(false);
const [lxcCtid, setLxcCtid] = createSignal('');
// Fetch bootstrap info on mount
const fetchBootstrapInfo = async () => {
try {
const response = await fetch('/api/security/status');
if (response.ok) {
const data = await response.json();
if (data.bootstrapTokenPath) {
setTokenPath(data.bootstrapTokenPath);
setIsDocker(data.isDocker || false);
setInContainer(data.inContainer || false);
setLxcCtid(data.lxcCtid || '');
}
}
} catch (error) {
console.error('Failed to fetch bootstrap info:', error);
}
};
// Call on component load
fetchBootstrapInfo();
const handleUnlock = async () => {
if (!props.bootstrapToken.trim()) {
showError('Please enter the bootstrap token');
return;
}
setIsValidating(true);
try {
const response = await fetch('/api/security/validate-bootstrap-token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: props.bootstrapToken.trim() }),
});
if (!response.ok) {
throw new Error('Invalid bootstrap token');
}
props.setIsUnlocked(true);
showSuccess('Token verified!');
props.onNext();
} catch (error) {
showError('Invalid bootstrap token. Please check and try again.');
} finally {
setIsValidating(false);
}
};
const getTokenCommand = () => {
const path = tokenPath() || '/etc/pulse/.bootstrap_token';
if (isDocker()) {
return `docker exec <container> cat ${path}`;
}
if (inContainer() && lxcCtid()) {
return `pct exec ${lxcCtid()} -- cat ${path}`;
}
if (inContainer()) {
return `pct exec <ctid> -- cat ${path}`;
}
return `cat ${path}`;
};
return (
<div class="text-center">
{/* Logo */}
<div class="mb-8">
<div class="inline-flex items-center justify-center w-24 h-24 rounded-full bg-gradient-to-br from-blue-500 to-indigo-600 shadow-2xl shadow-blue-500/30 mb-6">
<svg width="56" height="56" viewBox="0 0 256 256" class="text-white">
<circle class="fill-current opacity-20" cx="128" cy="128" r="122" />
<circle class="fill-none stroke-current" stroke-width="14" cx="128" cy="128" r="84" opacity="0.9" />
<circle class="fill-current" cx="128" cy="128" r="26" />
</svg>
</div>
<h1 class="text-4xl font-bold text-white mb-3">
Welcome to Pulse
</h1>
<p class="text-xl text-blue-200/80">
Unified infrastructure monitoring
</p>
</div>
{/* Feature highlights */}
<div class="grid grid-cols-3 gap-4 mb-8">
<div class="bg-white/5 backdrop-blur rounded-xl p-4 border border-white/10">
<div class="text-2xl mb-2">🖥</div>
<div class="text-sm text-white/80">Proxmox</div>
</div>
<div class="bg-white/5 backdrop-blur rounded-xl p-4 border border-white/10">
<div class="text-2xl mb-2">🐳</div>
<div class="text-sm text-white/80">Docker</div>
</div>
<div class="bg-white/5 backdrop-blur rounded-xl p-4 border border-white/10">
<div class="text-2xl mb-2"></div>
<div class="text-sm text-white/80">Kubernetes</div>
</div>
</div>
{/* Bootstrap token unlock */}
<Show when={!props.isUnlocked}>
<div class="bg-white/10 backdrop-blur-xl rounded-2xl p-6 border border-white/20 text-left">
<h3 class="text-lg font-semibold text-white mb-2">Unlock Setup</h3>
<p class="text-sm text-white/70 mb-4">
Retrieve the bootstrap token from your host:
</p>
<div class="bg-black/30 rounded-lg p-3 font-mono text-sm text-green-400 mb-4">
{getTokenCommand()}
</div>
<input
type="text"
value={props.bootstrapToken}
onInput={(e) => props.setBootstrapToken(e.currentTarget.value)}
onKeyPress={(e) => e.key === 'Enter' && handleUnlock()}
class="w-full px-4 py-3 bg-white/10 border border-white/20 rounded-xl text-white placeholder-white/40 focus:outline-none focus:ring-2 focus:ring-blue-500 font-mono"
placeholder="Paste your bootstrap token"
autofocus
/>
<button
onClick={handleUnlock}
disabled={isValidating() || !props.bootstrapToken.trim()}
class="w-full mt-4 py-3 px-6 bg-gradient-to-r from-blue-500 to-indigo-600 hover:from-blue-600 hover:to-indigo-700 disabled:opacity-50 disabled:cursor-not-allowed text-white font-medium rounded-xl transition-all shadow-lg shadow-blue-500/25"
>
{isValidating() ? 'Validating...' : 'Continue →'}
</button>
</div>
</Show>
<Show when={props.isUnlocked}>
<button
onClick={props.onNext}
class="py-4 px-8 bg-gradient-to-r from-blue-500 to-indigo-600 hover:from-blue-600 hover:to-indigo-700 text-white text-lg font-medium rounded-xl transition-all shadow-lg shadow-blue-500/25"
>
Get Started
</button>
</Show>
</div>
);
};