From cb06077fabbd73f59d57d6db02e20f5181b37393 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Sat, 13 Dec 2025 15:08:47 +0000 Subject: [PATCH] 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. --- frontend-modern/src/components/Login.tsx | 9 +- .../Settings/BackupsSettingsPanel.tsx | 154 +++-- .../src/components/Settings/Settings.tsx | 77 ++- .../Settings/UpdatesSettingsPanel.tsx | 551 +++++++++++++----- .../components/SetupWizard/SetupWizard.tsx | 141 +++++ .../components/SetupWizard/StepIndicator.tsx | 37 ++ .../src/components/SetupWizard/index.ts | 3 + .../SetupWizard/steps/CompleteStep.tsx | 155 +++++ .../SetupWizard/steps/ConnectStep.tsx | 293 ++++++++++ .../SetupWizard/steps/FeaturesStep.tsx | 138 +++++ .../SetupWizard/steps/SecurityStep.tsx | 191 ++++++ .../SetupWizard/steps/WelcomeStep.tsx | 156 +++++ 12 files changed, 1707 insertions(+), 198 deletions(-) create mode 100644 frontend-modern/src/components/SetupWizard/SetupWizard.tsx create mode 100644 frontend-modern/src/components/SetupWizard/StepIndicator.tsx create mode 100644 frontend-modern/src/components/SetupWizard/index.ts create mode 100644 frontend-modern/src/components/SetupWizard/steps/CompleteStep.tsx create mode 100644 frontend-modern/src/components/SetupWizard/steps/ConnectStep.tsx create mode 100644 frontend-modern/src/components/SetupWizard/steps/FeaturesStep.tsx create mode 100644 frontend-modern/src/components/SetupWizard/steps/SecurityStep.tsx create mode 100644 frontend-modern/src/components/SetupWizard/steps/WelcomeStep.tsx diff --git a/frontend-modern/src/components/Login.tsx b/frontend-modern/src/components/Login.tsx index a593175..e59efe5 100644 --- a/frontend-modern/src/components/Login.tsx +++ b/frontend-modern/src/components/Login.tsx @@ -2,8 +2,8 @@ import { Component, createSignal, Show, onMount, lazy, Suspense } from 'solid-js import { logger } from '@/utils/logger'; import { STORAGE_KEYS } from '@/utils/localStorage'; -const FirstRunSetup = lazy(() => - import('./FirstRunSetup').then((m) => ({ default: m.FirstRunSetup })), +const SetupWizard = lazy(() => + import('./SetupWizard').then((m) => ({ default: m.SetupWizard })), ); interface LoginProps { @@ -339,9 +339,8 @@ export const Login: Component = (props) => { } > - window.location.reload()} /> diff --git a/frontend-modern/src/components/Settings/BackupsSettingsPanel.tsx b/frontend-modern/src/components/Settings/BackupsSettingsPanel.tsx index 4cf9837..11ef052 100644 --- a/frontend-modern/src/components/Settings/BackupsSettingsPanel.tsx +++ b/frontend-modern/src/components/Settings/BackupsSettingsPanel.tsx @@ -239,30 +239,54 @@ export const BackupsSettingsPanel: Component = (props
{/* Export Section */} -
-
-
+
+
+
+ {/* Archive/Download Box Icon */}
-
-

+
+

Export Configuration

-

- Download an encrypted backup of all nodes and settings +

+ Create an encrypted backup package

+ + {/* Feature list */} +
    +
  • + + + + All node connections & credentials +
  • +
  • + + + + Alert thresholds & overrides +
  • +
  • + + + + AES-256 encryption +
  • +
+

{/* Import Section */} -
-
-
+
+
+
+ {/* Upload/Restore Icon */}
-
-

+
+

Restore Configuration

-

- Upload a backup file to restore nodes and settings +

+ Import from an encrypted backup file

+ + {/* Feature list */} +
    +
  • + + + + Merge or replace existing config +
  • +
  • + + + + Validates backup before applying +
  • +
  • + + + + Requires original passphrase +
  • +
+

- {/* Important Notes */} -
-
- - +
+ {/* Shield Icon */} +
+ - -
-

Important Notes

-
    -
  • • Backups contain encrypted credentials and sensitive data
  • -
  • • Use a strong passphrase to protect your backup
  • -
  • • Store backup files securely and never share the passphrase
  • + > + + +
+
+

Security Tips

+
    +
  • + + + + Backups contain encrypted credentials and sensitive data +
  • +
  • + + + + Use a strong passphrase (12+ characters, mix of letters, numbers, symbols) +
  • +
  • + + + + Store backup files securely and never share the passphrase +
diff --git a/frontend-modern/src/components/Settings/Settings.tsx b/frontend-modern/src/components/Settings/Settings.tsx index 7d5c905..859f84c 100644 --- a/frontend-modern/src/components/Settings/Settings.tsx +++ b/frontend-modern/src/components/Settings/Settings.tsx @@ -30,6 +30,7 @@ import { DiagnosticsPanel } from './DiagnosticsPanel'; import { GeneralSettingsPanel } from './GeneralSettingsPanel'; import { NetworkSettingsPanel } from './NetworkSettingsPanel'; import { UpdatesSettingsPanel } from './UpdatesSettingsPanel'; +import { UpdateConfirmationModal } from '@/components/UpdateConfirmationModal'; import { BackupsSettingsPanel } from './BackupsSettingsPanel'; import { SecurityAuthPanel } from './SecurityAuthPanel'; import { APIAccessPanel } from './APIAccessPanel'; @@ -764,6 +765,10 @@ const Settings: Component = (props) => { const [autoUpdateEnabled, setAutoUpdateEnabled] = createSignal(false); const [autoUpdateCheckInterval, setAutoUpdateCheckInterval] = createSignal(24); const [autoUpdateTime, setAutoUpdateTime] = createSignal('03:00'); + // Update installation state + const [updatePlan, setUpdatePlan] = createSignal(null); + const [isInstallingUpdate, setIsInstallingUpdate] = createSignal(false); + const [showUpdateConfirmation, setShowUpdateConfirmation] = createSignal(false); const [backupPollingEnabled, setBackupPollingEnabled] = createSignal(true); const [backupPollingInterval, setBackupPollingInterval] = createSignal(0); const [backupPollingCustomMinutes, setBackupPollingCustomMinutes] = createSignal(60); @@ -1832,7 +1837,23 @@ const Settings: Component = (props) => { const version = await UpdatesAPI.getVersion(); setVersionInfo(version); // Also set it in the store so it's available globally - updateStore.checkForUpdates(); // This will load version info too + await updateStore.checkForUpdates(); // This will load version info too + + // Fetch update info and plan from store + const storeInfo = updateStore.updateInfo(); + if (storeInfo) { + setUpdateInfo(storeInfo); + // Fetch update plan if update is available + if (storeInfo.available && storeInfo.latestVersion) { + try { + const plan = await UpdatesAPI.getUpdatePlan(storeInfo.latestVersion); + setUpdatePlan(plan); + } catch (planError) { + logger.warn('Failed to fetch update plan on load', planError); + } + } + } + // Only use version.channel as fallback if user hasn't configured a preference // The user's saved updateChannel preference should take priority // Check the signal value since systemSettings is scoped to the previous try block @@ -2028,6 +2049,19 @@ const Settings: Component = (props) => { const info = updateStore.updateInfo(); setUpdateInfo(info); + // Fetch update plan if update is available + if (info?.available && info.latestVersion) { + try { + const plan = await UpdatesAPI.getUpdatePlan(info.latestVersion); + setUpdatePlan(plan); + } catch (planError) { + logger.warn('Failed to fetch update plan', planError); + setUpdatePlan(null); + } + } else { + setUpdatePlan(null); + } + // If update was dismissed, clear it so user can see it again if (info?.available && updateStore.isDismissed()) { updateStore.clearDismissed(); @@ -2044,6 +2078,28 @@ const Settings: Component = (props) => { } }; + // Handle install update from settings panel + const handleInstallUpdate = () => { + setShowUpdateConfirmation(true); + }; + + const handleConfirmUpdate = async () => { + const info = updateInfo(); + if (!info?.downloadUrl) return; + + setIsInstallingUpdate(true); + try { + await UpdatesAPI.applyUpdate(info.downloadUrl); + // Close confirmation - GlobalUpdateProgressWatcher will auto-open the progress modal + setShowUpdateConfirmation(false); + } catch (error) { + logger.error('Failed to start update', error); + showError('Failed to start update. Please try again.'); + } finally { + setIsInstallingUpdate(false); + } + }; + const handleExport = async () => { if (!exportPassphrase()) { const hasAuth = securityStatus()?.hasAuthentication; @@ -3463,6 +3519,9 @@ const Settings: Component = (props) => { setAutoUpdateTime={setAutoUpdateTime} checkForUpdates={checkForUpdates} setHasUnsavedChanges={setHasUnsavedChanges} + updatePlan={updatePlan} + onInstallUpdate={handleInstallUpdate} + isInstalling={isInstallingUpdate} /> @@ -3826,6 +3885,22 @@ const Settings: Component = (props) => { }} /> + + {/* Update Confirmation Modal */} + setShowUpdateConfirmation(false)} + onConfirm={handleConfirmUpdate} + currentVersion={versionInfo()?.version || 'Unknown'} + latestVersion={updateInfo()?.latestVersion || ''} + plan={updatePlan() || { + canAutoUpdate: false, + requiresRoot: false, + rollbackSupport: false, + }} + isApplying={isInstallingUpdate()} + /> + {/* Export Dialog */} < Show when={showExportDialog()} >
diff --git a/frontend-modern/src/components/Settings/UpdatesSettingsPanel.tsx b/frontend-modern/src/components/Settings/UpdatesSettingsPanel.tsx index 1c6d24b..daf72ad 100644 --- a/frontend-modern/src/components/Settings/UpdatesSettingsPanel.tsx +++ b/frontend-modern/src/components/Settings/UpdatesSettingsPanel.tsx @@ -5,7 +5,8 @@ import RefreshCw from 'lucide-solid/icons/refresh-cw'; import CheckCircle from 'lucide-solid/icons/check-circle'; import ArrowRight from 'lucide-solid/icons/arrow-right'; import Package from 'lucide-solid/icons/package'; -import type { UpdateInfo, VersionInfo } from '@/api/updates'; +import Download from 'lucide-solid/icons/download'; +import type { UpdateInfo, VersionInfo, UpdatePlan } from '@/api/updates'; interface UpdatesSettingsPanelProps { versionInfo: Accessor; @@ -21,6 +22,10 @@ interface UpdatesSettingsPanelProps { setAutoUpdateTime: Setter; checkForUpdates: () => Promise; setHasUnsavedChanges: Setter; + // Update installation props + updatePlan: Accessor; + onInstallUpdate: () => void; + isInstalling: Accessor; } export const UpdatesSettingsPanel: Component = (props) => { @@ -209,186 +214,436 @@ export const UpdatesSettingsPanel: Component = (props {/* Update available */} -
-
- - - -

- How to install the update -

+
+ {/* Header */} +
+
+
+
+ + + +
+
+

+ Update Available +

+

+ Version {props.updateInfo()?.latestVersion} is ready to install +

+
+
+ + {/* Automated Install Button */} + + + +
-
+ {/* Installation Steps */} +
+ {/* Manual Steps Header */} + +
+ Follow these steps to update manually: +
+
+ + +
+ Click "Install Update" above for automatic installation, or update manually: +
+
+ + {/* ProxmoxVE LXC Installation */} -

- Type{' '} - update{' '} - in the LXC console -

-
- -
-

Run these commands:

- - docker pull rcourtman/pulse:latest -
- docker restart pulse -
-
-
- -
-

- Click the "Install Update" button below, or download and install manually: +

+
+ 1 + Open your Pulse LXC console +
+
+ 2 + Run the update command: +
+
+ + update + + +
+

+ The script will automatically download and install the latest version.

- - curl -LO - https://github.com/rcourtman/Pulse/releases/download/ - {props.updateInfo()?.latestVersion}/pulse-{props.updateInfo()?.latestVersion} - -linux-amd64.tar.gz -
- sudo systemctl stop pulse -
- sudo tar -xzf pulse-{props.updateInfo()?.latestVersion} - -linux-amd64.tar.gz -C /usr/local/bin pulse -
- sudo systemctl start pulse -
- -

- Pull latest changes and rebuild -

+ + {/* Docker Installation */} + +
+
+ 1 + Pull the latest image +
+
+ + docker pull rcourtman/pulse:latest + + +
+ +
+ 2 + Restart the container +
+
+ + docker restart pulse + + +
+

+ Or use Docker Compose: docker-compose pull && docker-compose up -d +

+
- -

- Pull the latest Pulse Docker image and recreate your container. -

+ + {/* Systemd/Manual Installation */} + +
+
+ 1 + Stop the service +
+
+ + sudo systemctl stop pulse + + +
+ +
+ 2 + Download and extract the new version +
+
+ + {`curl -LO https://github.com/rcourtman/Pulse/releases/download/${props.updateInfo()?.latestVersion}/pulse-${props.updateInfo()?.latestVersion}-linux-amd64.tar.gz +sudo tar -xzf pulse-${props.updateInfo()?.latestVersion}-linux-amd64.tar.gz -C /usr/local/bin pulse`} + + +
+ +
+ 3 + Start the service +
+
+ + sudo systemctl start pulse + + +
+
+
+ + {/* Development Installation */} + +
+
+ 1 + Pull the latest changes +
+
+ + git pull origin main + + +
+ +
+ 2 + Rebuild and restart +
+
+ + make build && make run + + +
+
- {/* Release notes */} + {/* Release notes footer */} -
- - Release Notes - -
-                        {props.updateInfo()?.releaseNotes}
-                      
-
+
+
+ + + + + View Release Notes + +
+                          {props.updateInfo()?.releaseNotes}
+                        
+
+
{/* Update settings */} -
+
+

+ + + + + Update Preferences +

+ {/* Update Channel */} -
-
- -

- Choose between stable and release candidate versions -

-
- +
+
+ + + +
+
+

Stable

+

+ Production-ready releases +

+
+
+ + +
{/* Auto Update Toggle */} -
-
- -

- Automatically check for updates (installation is manual) -

-
- -
- - {/* Auto update options (shown when enabled) */} - -
- {/* Check Interval */} -
+
+
+
+
+ + + +
+

+ Periodically check for new versions (installation is always manual) +

+
+
+ +
+ + {/* Auto update options (shown when enabled) */} + +
+ {/* Check Interval */} +
+ -

- How often to check for updates -

+
- -
- {/* Check Time */} -
-
-
-
-
+ +
diff --git a/frontend-modern/src/components/SetupWizard/SetupWizard.tsx b/frontend-modern/src/components/SetupWizard/SetupWizard.tsx new file mode 100644 index 0000000..4c4d41b --- /dev/null +++ b/frontend-modern/src/components/SetupWizard/SetupWizard.tsx @@ -0,0 +1,141 @@ +import { Component, createSignal, Show, onMount } from 'solid-js'; +import { WelcomeStep } from './steps/WelcomeStep'; +import { SecurityStep } from './steps/SecurityStep'; +import { ConnectStep } from './steps/ConnectStep'; +import { FeaturesStep } from './steps/FeaturesStep'; +import { CompleteStep } from './steps/CompleteStep'; +import { StepIndicator } from './StepIndicator'; + +export type WizardStep = 'welcome' | 'security' | 'connect' | 'features' | 'complete'; + +export interface WizardState { + // Security + username: string; + password: string; + apiToken: string; + // Node + nodeAdded: boolean; + nodeName: string; + // Features + aiEnabled: boolean; + autoUpdatesEnabled: boolean; +} + +interface SetupWizardProps { + onComplete: () => void; + bootstrapToken?: string; + isUnlocked?: boolean; +} + +export const SetupWizard: Component = (props) => { + const [currentStep, setCurrentStep] = createSignal('welcome'); + const [wizardState, setWizardState] = createSignal({ + username: 'admin', + password: '', + apiToken: '', + nodeAdded: false, + nodeName: '', + aiEnabled: false, + autoUpdatesEnabled: true, + }); + const [bootstrapToken, setBootstrapToken] = createSignal(props.bootstrapToken || ''); + const [isUnlocked, setIsUnlocked] = createSignal(props.isUnlocked || false); + + const steps: WizardStep[] = ['welcome', 'security', 'connect', 'features', 'complete']; + + const currentStepIndex = () => steps.indexOf(currentStep()); + + const nextStep = () => { + const idx = currentStepIndex(); + if (idx < steps.length - 1) { + setCurrentStep(steps[idx + 1]); + } + }; + + const prevStep = () => { + const idx = currentStepIndex(); + if (idx > 0) { + setCurrentStep(steps[idx - 1]); + } + }; + + const updateState = (updates: Partial) => { + setWizardState(prev => ({ ...prev, ...updates })); + }; + + const skipToComplete = () => { + setCurrentStep('complete'); + }; + + return ( +
+ {/* Background decoration */} +
+
+
+
+
+ + {/* Step indicator - only show after welcome */} + +
+ +
+
+ + {/* Main content */} +
+
+ + + + + + + + + + + + + + + + + + + +
+
+
+ ); +}; diff --git a/frontend-modern/src/components/SetupWizard/StepIndicator.tsx b/frontend-modern/src/components/SetupWizard/StepIndicator.tsx new file mode 100644 index 0000000..191ce34 --- /dev/null +++ b/frontend-modern/src/components/SetupWizard/StepIndicator.tsx @@ -0,0 +1,37 @@ +import { Component } from 'solid-js'; + +interface StepIndicatorProps { + steps: string[]; + currentStep: number; +} + +export const StepIndicator: Component = (props) => { + return ( +
+ {props.steps.map((step, index) => ( +
+
+ + {index < props.currentStep ? '✓' : index + 1} + + +
+ {index < props.steps.length - 1 && ( +
+ )} +
+ ))} +
+ ); +}; diff --git a/frontend-modern/src/components/SetupWizard/index.ts b/frontend-modern/src/components/SetupWizard/index.ts new file mode 100644 index 0000000..88370ca --- /dev/null +++ b/frontend-modern/src/components/SetupWizard/index.ts @@ -0,0 +1,3 @@ +export { SetupWizard } from './SetupWizard'; +export { StepIndicator } from './StepIndicator'; +export type { WizardState, WizardStep } from './SetupWizard'; diff --git a/frontend-modern/src/components/SetupWizard/steps/CompleteStep.tsx b/frontend-modern/src/components/SetupWizard/steps/CompleteStep.tsx new file mode 100644 index 0000000..c74b5c2 --- /dev/null +++ b/frontend-modern/src/components/SetupWizard/steps/CompleteStep.tsx @@ -0,0 +1,155 @@ +import { Component, createSignal } from 'solid-js'; +import { copyToClipboard } from '@/utils/clipboard'; +import { getPulseBaseUrl } from '@/utils/url'; +import type { WizardState } from '../SetupWizard'; + +interface CompleteStepProps { + state: WizardState; + onComplete: () => void; +} + +export const CompleteStep: Component = (props) => { + const [copied, setCopied] = createSignal<'password' | 'token' | null>(null); + + const handleCopy = async (type: 'password' | 'token') => { + const value = type === 'password' ? props.state.password : props.state.apiToken; + const success = await copyToClipboard(value); + if (success) { + setCopied(type); + setTimeout(() => setCopied(null), 2000); + } + }; + + const downloadCredentials = () => { + const baseUrl = getPulseBaseUrl(); + const content = `Pulse Credentials +================== +Generated: ${new Date().toISOString()} + +Web Login: +---------- +URL: ${baseUrl} +Username: ${props.state.username} +Password: ${props.state.password} + +API Token: +---------- +${props.state.apiToken} + +Example: curl -H "X-API-Token: ${props.state.apiToken}" ${baseUrl}/api/state + +⚠️ Keep these credentials secure! +`; + + const blob = new Blob([content], { type: 'text/plain' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `pulse-credentials-${Date.now()}.txt`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + }; + + return ( +
+ {/* Success animation */} +
+
+ + + +
+

+ You're All Set! 🎉 +

+

+ Pulse is ready to monitor your infrastructure +

+
+ + {/* Credentials box */} +
+
+

Your Credentials

+ +
+ +
+ {/* Username */} +
+
Username
+
{props.state.username}
+
+ + {/* Password */} +
+
Password
+
+ {props.state.password} + +
+
+ + {/* API Token */} +
+
API Token
+
+ {props.state.apiToken} + +
+
+
+ +
+

+ ⚠️ Save these now — they won't be shown again! +

+
+
+ + {/* Quick links */} + + + {/* Launch button */} + +
+ ); +}; diff --git a/frontend-modern/src/components/SetupWizard/steps/ConnectStep.tsx b/frontend-modern/src/components/SetupWizard/steps/ConnectStep.tsx new file mode 100644 index 0000000..a959aae --- /dev/null +++ b/frontend-modern/src/components/SetupWizard/steps/ConnectStep.tsx @@ -0,0 +1,293 @@ +import { Component, createSignal, For, Show } from 'solid-js'; +import { showError, showSuccess } from '@/utils/toast'; +import type { WizardState } from '../SetupWizard'; + +interface ConnectStepProps { + state: WizardState; + updateState: (updates: Partial) => void; + onNext: () => void; + onBack: () => void; + onSkip: () => void; +} + +type Platform = 'proxmox' | 'docker' | 'kubernetes'; + +interface DiscoveredNode { + ip: string; + port: number; + type: string; + hostname?: string; +} + +export const ConnectStep: Component = (props) => { + const [selectedPlatform, setSelectedPlatform] = createSignal(null); + const [isScanning, setIsScanning] = createSignal(false); + const [discoveredNodes, setDiscoveredNodes] = createSignal([]); + const [showManualForm, setShowManualForm] = createSignal(false); + const [isConnecting, setIsConnecting] = createSignal(false); + + // Manual form fields + const [host, setHost] = createSignal(''); + const [port, setPort] = createSignal('8006'); + const [tokenId, setTokenId] = createSignal(''); + const [tokenSecret, setTokenSecret] = createSignal(''); + + const platforms = [ + { id: 'proxmox' as Platform, name: 'Proxmox VE', icon: '🖥️', desc: 'Hypervisor & containers', ports: ['8006'] }, + { id: 'docker' as Platform, name: 'Docker', icon: '🐳', desc: 'Container hosts', ports: ['2375', '2376'] }, + { id: 'kubernetes' as Platform, name: 'Kubernetes', icon: '☸️', desc: 'Container orchestration', ports: ['6443'] }, + ]; + + const runDiscovery = async () => { + setIsScanning(true); + setDiscoveredNodes([]); + + try { + const response = await fetch('/api/discover', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ subnet: 'auto' }), + }); + + if (!response.ok) throw new Error('Discovery failed'); + + // Poll for results + for (let i = 0; i < 10; i++) { + await new Promise(r => setTimeout(r, 2000)); + const results = await fetch('/api/discover/results'); + if (results.ok) { + const data = await results.json(); + if (data.nodes && data.nodes.length > 0) { + setDiscoveredNodes(data.nodes.filter((n: DiscoveredNode) => + selectedPlatform() === 'proxmox' ? ['pve', 'pbs', 'pmg'].includes(n.type) : true + )); + break; + } + } + } + + if (discoveredNodes().length === 0) { + showSuccess('Scan complete - no nodes found. Try manual setup.'); + } + } catch (error) { + showError('Discovery failed. Try manual setup.'); + } finally { + setIsScanning(false); + } + }; + + const connectNode = async (node?: DiscoveredNode) => { + setIsConnecting(true); + + try { + const nodeData = node ? { + type: node.type === 'pbs' ? 'pbs' : node.type === 'pmg' ? 'pmg' : 'pve', + name: node.hostname || node.ip, + host: node.ip, + port: node.port, + } : { + type: 'pve', + name: host().replace(/:\d+$/, ''), + host: host(), + port: parseInt(port()), + tokenId: tokenId(), + tokenValue: tokenSecret(), + }; + + const response = await fetch('/api/nodes', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(nodeData), + }); + + if (!response.ok) throw new Error(await response.text()); + + props.updateState({ nodeAdded: true, nodeName: nodeData.name }); + showSuccess(`Connected to ${nodeData.name}!`); + props.onNext(); + } catch (error) { + showError(`Connection failed: ${error}`); + } finally { + setIsConnecting(false); + } + }; + + return ( +
+
+

Connect Your Infrastructure

+

Add monitoring for your systems

+
+ +
+ {/* Platform selection */} + +
+ + {(platform) => ( + + )} + +
+
+ + {/* Proxmox connection */} + +
+
+ + 🖥️ + Proxmox VE +
+ + {/* Options */} +
+ + +
+ + {/* Scanning indicator */} + +
+
+

Scanning network...

+
+ + + {/* Discovered nodes */} + 0}> +
+

Found {discoveredNodes().length} node(s):

+ + {(node) => ( + + )} + +
+
+ + {/* Manual form */} + +
+ setHost(e.currentTarget.value)} + class="w-full px-4 py-3 bg-white/10 border border-white/20 rounded-xl text-white placeholder-white/40" + placeholder="Host (e.g., 192.168.1.100)" + /> + setTokenId(e.currentTarget.value)} + class="w-full px-4 py-3 bg-white/10 border border-white/20 rounded-xl text-white placeholder-white/40" + placeholder="API Token ID (e.g., root@pam!pulse)" + /> + setTokenSecret(e.currentTarget.value)} + class="w-full px-4 py-3 bg-white/10 border border-white/20 rounded-xl text-white placeholder-white/40" + placeholder="API Token Secret" + /> + +
+
+
+
+ + {/* Docker placeholder */} + +
+
🐳
+

+ Docker monitoring requires the Pulse Agent.
+ Install it on your Docker host after setup. +

+ +
+
+ + {/* Kubernetes placeholder */} + +
+
☸️
+

+ Kubernetes monitoring requires the Pulse Agent.
+ Deploy via Helm after setup. +

+ +
+
+
+ + {/* Actions */} +
+ +
+ +
+
+ ); +}; diff --git a/frontend-modern/src/components/SetupWizard/steps/FeaturesStep.tsx b/frontend-modern/src/components/SetupWizard/steps/FeaturesStep.tsx new file mode 100644 index 0000000..938b5f6 --- /dev/null +++ b/frontend-modern/src/components/SetupWizard/steps/FeaturesStep.tsx @@ -0,0 +1,138 @@ +import { Component, createSignal } from 'solid-js'; +import { showSuccess } from '@/utils/toast'; +import { SettingsAPI } from '@/api/settings'; +import type { WizardState } from '../SetupWizard'; + +interface FeaturesStepProps { + state: WizardState; + updateState: (updates: Partial) => void; + onNext: () => void; + onBack: () => void; +} + +export const FeaturesStep: Component = (props) => { + const [aiEnabled, setAiEnabled] = createSignal(false); + const [autoUpdates, setAutoUpdates] = createSignal(true); + const [isSaving, setIsSaving] = createSignal(false); + + const features = [ + { + id: 'ai', + name: 'Pulse AI', + icon: '🤖', + desc: 'Intelligent monitoring assistant with auto-fix capabilities', + enabled: aiEnabled, + setEnabled: setAiEnabled, + badge: 'New in 5.0', + }, + { + id: 'updates', + name: 'Automatic Updates', + icon: '🔄', + desc: 'Keep Pulse up-to-date automatically', + enabled: autoUpdates, + setEnabled: setAutoUpdates, + badge: null, + }, + ]; + + const handleContinue = async () => { + setIsSaving(true); + + try { + // Save feature preferences + await SettingsAPI.updateSystemSettings({ + aiEnabled: aiEnabled(), + autoUpdateEnabled: autoUpdates(), + }); + + props.updateState({ + aiEnabled: aiEnabled(), + autoUpdatesEnabled: autoUpdates(), + }); + + showSuccess('Preferences saved!'); + props.onNext(); + } catch (error) { + // Continue anyway - settings can be changed later + props.onNext(); + } finally { + setIsSaving(false); + } + }; + + return ( +
+
+

Enable Features

+

Customize your Pulse experience

+
+ +
+ {features.map((feature) => ( + + ))} + + {/* AI info box */} +
+
+
+
+

Pulse AI Features

+

+ • Chat assistant for infrastructure questions
+ • Patrol mode for proactive monitoring
+ • Auto-fix for common issues
+ • Predictive failure detection +

+

+ Requires API key configuration in Settings → AI after setup +

+
+
+
+
+ + {/* Actions */} +
+ + +
+
+ ); +}; diff --git a/frontend-modern/src/components/SetupWizard/steps/SecurityStep.tsx b/frontend-modern/src/components/SetupWizard/steps/SecurityStep.tsx new file mode 100644 index 0000000..d2b5763 --- /dev/null +++ b/frontend-modern/src/components/SetupWizard/steps/SecurityStep.tsx @@ -0,0 +1,191 @@ +import { Component, createSignal, Show } from 'solid-js'; +import { showError, showSuccess } from '@/utils/toast'; +import { setApiToken as setApiClientToken } from '@/utils/apiClient'; +import type { WizardState } from '../SetupWizard'; + +interface SecurityStepProps { + state: WizardState; + updateState: (updates: Partial) => void; + bootstrapToken: string; + onNext: () => void; + onBack: () => void; +} + +export const SecurityStep: Component = (props) => { + const [username, setUsername] = createSignal(props.state.username || 'admin'); + const [useCustomPassword, setUseCustomPassword] = createSignal(false); + const [password, setPassword] = createSignal(''); + const [confirmPassword, setConfirmPassword] = createSignal(''); + const [isSettingUp, setIsSettingUp] = createSignal(false); + + const generatePassword = () => { + const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789!@#$%'; + let pass = ''; + for (let i = 0; i < 16; i++) { + pass += chars.charAt(Math.floor(Math.random() * chars.length)); + } + return pass; + }; + + const generateToken = (): string => { + const array = new Uint8Array(24); + crypto.getRandomValues(array); + return Array.from(array, (byte) => byte.toString(16).padStart(2, '0')).join(''); + }; + + const handleSetup = async () => { + if (useCustomPassword()) { + if (!password() || password().length < 12) { + showError('Password must be at least 12 characters'); + return; + } + if (password() !== confirmPassword()) { + showError('Passwords do not match'); + return; + } + } + + setIsSettingUp(true); + const finalPassword = useCustomPassword() ? password() : generatePassword(); + const token = generateToken(); + + try { + setApiClientToken(token); + + const response = await fetch('/api/security/quick-setup', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Setup-Token': props.bootstrapToken, + }, + credentials: 'include', + body: JSON.stringify({ + username: username(), + password: finalPassword, + apiToken: token, + force: false, + setupToken: props.bootstrapToken, + }), + }); + + if (!response.ok) { + throw new Error(await response.text()); + } + + props.updateState({ + username: username(), + password: finalPassword, + apiToken: token, + }); + + showSuccess('Security configured!'); + props.onNext(); + } catch (error) { + showError(`Setup failed: ${error}`); + } finally { + setIsSettingUp(false); + } + }; + + return ( +
+
+

Secure Your Dashboard

+

Create your admin account

+
+ +
+ {/* Username */} +
+ + setUsername(e.currentTarget.value)} + 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" + placeholder="admin" + /> +
+ + {/* Password choice */} +
+ +
+ + +
+ + +
+ setPassword(e.currentTarget.value)} + 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" + placeholder="Password (min 12 characters)" + /> + setConfirmPassword(e.currentTarget.value)} + 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" + placeholder="Confirm password" + /> +
+
+ + +
+

+ A secure 16-character password will be generated and shown after setup. +

+
+
+
+ + {/* Info */} +
+

+ This creates your admin account and an API token for automation. + Credentials will be displayed once - save them securely! +

+
+
+ + {/* Actions */} +
+ + +
+
+ ); +}; diff --git a/frontend-modern/src/components/SetupWizard/steps/WelcomeStep.tsx b/frontend-modern/src/components/SetupWizard/steps/WelcomeStep.tsx new file mode 100644 index 0000000..9f190ba --- /dev/null +++ b/frontend-modern/src/components/SetupWizard/steps/WelcomeStep.tsx @@ -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 = (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 cat ${path}`; + } + if (inContainer() && lxcCtid()) { + return `pct exec ${lxcCtid()} -- cat ${path}`; + } + if (inContainer()) { + return `pct exec -- cat ${path}`; + } + return `cat ${path}`; + }; + + return ( +
+ {/* Logo */} +
+
+ + + + + +
+

+ Welcome to Pulse +

+

+ Unified infrastructure monitoring +

+
+ + {/* Feature highlights */} +
+
+
🖥️
+
Proxmox
+
+
+
🐳
+
Docker
+
+
+
☸️
+
Kubernetes
+
+
+ + {/* Bootstrap token unlock */} + +
+

Unlock Setup

+

+ Retrieve the bootstrap token from your host: +

+
+ {getTokenCommand()} +
+ 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 + /> + +
+
+ + + + +
+ ); +};