diff --git a/frontend-modern/src/components/Backups/BackupsFilter.tsx b/frontend-modern/src/components/Backups/BackupsFilter.tsx index 0146cb6..69f1c43 100644 --- a/frontend-modern/src/components/Backups/BackupsFilter.tsx +++ b/frontend-modern/src/components/Backups/BackupsFilter.tsx @@ -23,6 +23,9 @@ interface BackupsFilterProps { onReset?: () => void; statusFilter?: () => 'all' | 'verified' | 'unverified'; setStatusFilter?: (value: 'all' | 'verified' | 'unverified') => void; + // Time format toggle + useRelativeTime?: () => boolean; + setUseRelativeTime?: (value: boolean) => void; } export const BackupsFilter: Component = (props) => { @@ -504,6 +507,25 @@ export const BackupsFilter: Component = (props) => { + {/* Time Format Toggle */} + + + + + {/* Reset Button */} + + + + + {/* Import Section */} +
+
+
+ + + +
+
+

+ Restore Configuration +

+

+ Upload a backup file to restore nodes and settings +

+ +
+
+
+ + + {/* Important Notes */} +
+
+ + + +
+

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
  • +
+
+
+
+ + + + ); +}; diff --git a/frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx b/frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx new file mode 100644 index 0000000..26fbefc --- /dev/null +++ b/frontend-modern/src/components/Settings/GeneralSettingsPanel.tsx @@ -0,0 +1,208 @@ +import { Component, Show, For, Accessor, Setter } from 'solid-js'; +import { Card } from '@/components/shared/Card'; +import { SectionHeader } from '@/components/shared/SectionHeader'; +import { Toggle } from '@/components/shared/Toggle'; +import Sliders from 'lucide-solid/icons/sliders-horizontal'; +import Activity from 'lucide-solid/icons/activity'; + +const PVE_POLLING_MIN_SECONDS = 10; +const PVE_POLLING_MAX_SECONDS = 3600; +const PVE_POLLING_PRESETS = [ + { label: 'Realtime (10s)', value: 10 }, + { label: 'Balanced (30s)', value: 30 }, + { label: 'Low (60s)', value: 60 }, + { label: 'Very low (5m)', value: 300 }, +]; + +interface GeneralSettingsPanelProps { + darkMode: Accessor; + toggleDarkMode: () => void; + pvePollingInterval: Accessor; + setPVEPollingInterval: Setter; + pvePollingSelection: Accessor; + setPVEPollingSelection: Setter; + pvePollingCustomSeconds: Accessor; + setPVEPollingCustomSeconds: Setter; + pvePollingEnvLocked: () => boolean; + setHasUnsavedChanges: Setter; +} + +export const GeneralSettingsPanel: Component = (props) => { + return ( +
+ {/* Appearance Card */} + +
+
+
+ +
+ +
+
+
+
+
+

Dark mode

+

+ Toggle to match your environment. Pulse remembers this preference on each browser. +

+
+ { + const desired = (event.currentTarget as HTMLInputElement).checked; + if (desired !== props.darkMode()) { + props.toggleDarkMode(); + } + }} + /> +
+
+
+ + {/* Monitoring Cadence Card */} + +
+
+
+ +
+ +
+
+
+
+

+ Shorter intervals provide near-real-time updates at the cost of higher API and CPU + usage on each node. Set a longer interval to reduce load on busy clusters. +

+

+ Current cadence: {props.pvePollingInterval()} seconds ( + {props.pvePollingInterval() >= 60 + ? `${(props.pvePollingInterval() / 60).toFixed( + props.pvePollingInterval() % 60 === 0 ? 0 : 1 + )} minute${props.pvePollingInterval() / 60 === 1 ? '' : 's'}` + : 'under a minute'} + ). +

+
+ +
+ {/* Preset buttons */} +
+ + {(option) => ( + + )} + + +
+ + {/* Custom interval input */} + +
+ + { + if (props.pvePollingEnvLocked()) return; + const parsed = Math.floor(Number(e.currentTarget.value)); + if (Number.isNaN(parsed)) { + return; + } + const clamped = Math.min( + PVE_POLLING_MAX_SECONDS, + Math.max(PVE_POLLING_MIN_SECONDS, parsed) + ); + props.setPVEPollingCustomSeconds(clamped); + props.setPVEPollingInterval(clamped); + props.setHasUnsavedChanges(true); + }} + /> +

+ Applies to all PVE clusters and standalone nodes. +

+
+
+ + {/* Env override warning */} + +
+ + + + + + Managed via environment variable PVE_POLLING_INTERVAL. +
+
+
+
+
+
+ ); +}; diff --git a/frontend-modern/src/components/Settings/NetworkSettingsPanel.tsx b/frontend-modern/src/components/Settings/NetworkSettingsPanel.tsx new file mode 100644 index 0000000..0e1a937 --- /dev/null +++ b/frontend-modern/src/components/Settings/NetworkSettingsPanel.tsx @@ -0,0 +1,621 @@ +import { Component, Show, For, Accessor, Setter } from 'solid-js'; +import { Card } from '@/components/shared/Card'; +import { SectionHeader } from '@/components/shared/SectionHeader'; +import { Toggle } from '@/components/shared/Toggle'; +import type { ToggleChangeEvent } from '@/components/shared/Toggle'; +import Network from 'lucide-solid/icons/network'; + +const COMMON_DISCOVERY_SUBNETS = [ + '192.168.1.0/24', + '192.168.0.0/24', + '10.0.0.0/24', + '172.16.0.0/24', + '192.168.10.0/24', +]; + +interface NetworkSettingsPanelProps { + // Discovery settings + discoveryEnabled: Accessor; + discoveryMode: Accessor<'auto' | 'custom'>; + discoverySubnetDraft: Accessor; + discoverySubnetError: Accessor; + savingDiscoverySettings: Accessor; + envOverrides: Accessor>; + + // Network settings + allowedOrigins: Accessor; + setAllowedOrigins: Setter; + allowEmbedding: Accessor; + setAllowEmbedding: Setter; + allowedEmbedOrigins: Accessor; + setAllowedEmbedOrigins: Setter; + webhookAllowedPrivateCIDRs: Accessor; + setWebhookAllowedPrivateCIDRs: Setter; + + // Handlers + handleDiscoveryEnabledChange: (enabled: boolean) => Promise; + handleDiscoveryModeChange: (mode: 'auto' | 'custom') => Promise; + setDiscoveryMode: Setter<'auto' | 'custom'>; + setDiscoverySubnetDraft: Setter; + setDiscoverySubnetError: Setter; + setLastCustomSubnet: Setter; + commitDiscoverySubnet: (value: string) => Promise; + setHasUnsavedChanges: Setter; + + // Utility functions + parseSubnetList: (value: string) => string[]; + normalizeSubnetList: (value: string) => string; + isValidCIDR: (value: string) => boolean; + currentDraftSubnetValue: () => string; + + // Ref for input + discoverySubnetInputRef?: (el: HTMLInputElement) => void; +} + +export const NetworkSettingsPanel: Component = (props) => { + return ( +
+ {/* Info Card */} + +
+ + + +
+

Configuration Priority

+
    +
  • + • Some env vars override settings (API_TOKENS, legacy API_TOKEN, PORTS, AUTH) +
  • +
  • • Changes made here are saved to system.json immediately
  • +
  • • Settings persist unless overridden by env vars
  • +
+
+
+
+ + {/* Main Network Card */} + + {/* Header */} +
+
+
+ +
+ +
+
+ +
+ {/* Network Discovery Section */} +
+ + + {/* Discovery Toggle */} +
+
+

Automatic scanning

+

+ Enable discovery to surface Proxmox VE, PBS, and PMG endpoints automatically. +

+
+ { + if (props.envOverrides().discoveryEnabled || props.savingDiscoverySettings()) { + e.preventDefault(); + return; + } + const success = await props.handleDiscoveryEnabledChange(e.currentTarget.checked); + if (!success) { + e.currentTarget.checked = props.discoveryEnabled(); + } + }} + disabled={props.envOverrides().discoveryEnabled || props.savingDiscoverySettings()} + containerClass="gap-2" + label={ + + {props.discoveryEnabled() ? 'On' : 'Off'} + + } + /> +
+ + {/* Discovery Options (shown when enabled) */} + +
+
+ + Scan scope + +
+ {/* Auto mode */} + + + {/* Custom mode */} + + + {/* Common subnet presets */} + +
+ + Common networks: + + + {(preset) => { + const baseValue = props.currentDraftSubnetValue(); + const currentSelections = props.parseSubnetList(baseValue); + const isActive = currentSelections.includes(preset); + return ( + + ); + }} + +
+
+
+
+ + {/* Subnet Input */} +
+
+ + + + + + + + +
+ { + if (props.envOverrides().discoverySubnet) { + return; + } + const rawValue = e.currentTarget.value; + props.setDiscoverySubnetDraft(rawValue); + if (props.discoveryMode() !== 'custom') { + props.setDiscoveryMode('custom'); + } + props.setLastCustomSubnet(rawValue); + const trimmed = rawValue.trim(); + if (!trimmed) { + props.setDiscoverySubnetError(undefined); + return; + } + if (!props.isValidCIDR(trimmed)) { + props.setDiscoverySubnetError( + 'Use CIDR format such as 192.168.1.0/24 (comma-separated for multiple)' + ); + } else { + props.setDiscoverySubnetError(undefined); + } + }} + onBlur={async (e) => { + if ( + props.envOverrides().discoverySubnet || + props.discoveryMode() !== 'custom' + ) { + return; + } + const rawValue = e.currentTarget.value; + props.setDiscoverySubnetDraft(rawValue); + const trimmed = rawValue.trim(); + if (!trimmed) { + props.setDiscoverySubnetError( + 'Enter at least one subnet in CIDR format (e.g., 192.168.1.0/24)' + ); + return; + } + if (!props.isValidCIDR(trimmed)) { + props.setDiscoverySubnetError( + 'Use CIDR format such as 192.168.1.0/24 (comma-separated for multiple)' + ); + return; + } + props.setDiscoverySubnetError(undefined); + await props.commitDiscoverySubnet(rawValue); + }} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); + (e.currentTarget as HTMLInputElement).blur(); + } + }} + /> + +

+ {props.discoverySubnetError()} +

+
+ +

+ Auto scans every reachable network phase. Large networks may time out — switch + to custom subnets to narrow the search. +

+
+ +

+ Example: 192.168.1.0/24, 10.0.0.0/24 (comma-separated). Smaller ranges finish + faster and avoid timeouts. +

+
+
+
+
+ + {/* Env override warning */} + +
+ Discovery settings are locked by environment variables. Update the service + configuration and restart Pulse to change them here. +
+
+
+ + {/* CORS Settings Section */} +
+

+ + + + + Network Settings +

+
+ +

+ For reverse proxy setups (* = allow all, empty = same-origin only) +

+
+ { + if (!props.envOverrides().allowedOrigins) { + props.setAllowedOrigins(e.currentTarget.value); + props.setHasUnsavedChanges(true); + } + }} + disabled={props.envOverrides().allowedOrigins} + placeholder="* or https://example.com" + class={`w-full px-3 py-1.5 text-sm border rounded-lg ${ + props.envOverrides().allowedOrigins + ? 'border-amber-300 dark:border-amber-600 bg-amber-50 dark:bg-amber-900/20 cursor-not-allowed opacity-75' + : 'border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800' + }`} + /> + {props.envOverrides().allowedOrigins && ( +
+
+ + + + Overridden by ALLOWED_ORIGINS environment variable +
+
+ Remove the env var and restart to enable UI configuration +
+
+ )} +
+
+
+ + {/* Embedding Section */} +
+

+ + + + + Embedding +

+

+ Allow Pulse to be embedded in iframes (e.g., Homepage dashboard) +

+
+
+ { + props.setAllowEmbedding(e.currentTarget.checked); + props.setHasUnsavedChanges(true); + }} + class="rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500" + /> + +
+ + +
+ +

+ Comma-separated list of origins that can embed Pulse (leave empty for same-origin + only) +

+ { + props.setAllowedEmbedOrigins(e.currentTarget.value); + props.setHasUnsavedChanges(true); + }} + placeholder="https://my.domain, https://dashboard.example.com" + class="w-full px-3 py-1.5 text-sm border rounded-lg border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800" + /> +

+ Example: If Pulse is at pulse.my.domain and your dashboard is at{' '} + my.domain, add https://my.domain here. +

+
+
+
+
+ + {/* Webhook Security Section */} +
+

+ + + + Webhook Security +

+
+
+ +

+ By default, webhooks to private IP addresses are blocked for security. Enter + trusted CIDR ranges to allow webhooks to internal services (leave empty to block + all private IPs). +

+ { + props.setWebhookAllowedPrivateCIDRs(e.currentTarget.value); + props.setHasUnsavedChanges(true); + }} + placeholder="192.168.1.0/24, 10.0.0.0/8" + class="w-full px-3 py-1.5 text-sm border rounded-lg border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800" + /> +

+ Example: 192.168.1.0/24,10.0.0.0/8 allows webhooks to these private + networks. Localhost and cloud metadata services remain blocked. +

+
+
+
+ + {/* Port Configuration Notice */} + +

+ Port Configuration: Use{' '} + + systemctl edit pulse + +

+

+ [Service] +
+ Environment="FRONTEND_PORT=8080" +
+ + Then restart: sudo systemctl restart pulse + +

+
+
+
+
+ ); +}; diff --git a/frontend-modern/src/components/Settings/ProxmoxAgentNodesPanel.tsx b/frontend-modern/src/components/Settings/ProxmoxAgentNodesPanel.tsx new file mode 100644 index 0000000..76e1cd1 --- /dev/null +++ b/frontend-modern/src/components/Settings/ProxmoxAgentNodesPanel.tsx @@ -0,0 +1,590 @@ +import { Component, Show, For, Accessor, Setter, JSX } from 'solid-js'; +import { Card } from '@/components/shared/Card'; +import { Toggle } from '@/components/shared/Toggle'; +import type { ToggleChangeEvent } from '@/components/shared/Toggle'; +import { + PveNodesTable, + PbsNodesTable, + PmgNodesTable, + type TemperatureTransportInfo, +} from './ConfiguredNodeTables'; +import { notificationStore } from '@/stores/notifications'; +import Server from 'lucide-solid/icons/server'; +import HardDrive from 'lucide-solid/icons/hard-drive'; +import Mail from 'lucide-solid/icons/mail'; +import Loader from 'lucide-solid/icons/loader-2'; +import type { NodeConfig } from '@/api/nodes'; + +type AgentType = 'pve' | 'pbs' | 'pmg'; + +interface DiscoveredServer { + ip: string; + port: number; + type: 'pve' | 'pbs' | 'pmg'; + hostname?: string; +} + +interface DiscoveryScanStatus { + scanning: boolean; + lastScanStartedAt?: string; + lastResultAt?: string; + errors?: string[]; +} + +type NodeConfigWithStatus = NodeConfig & { + hasPassword?: boolean; + hasToken?: boolean; + connectionStatus?: 'connected' | 'error' | 'pending' | 'unknown'; + connectionError?: string; + status?: 'connected' | 'error' | 'pending' | 'unknown'; +}; + +interface ProxmoxAgentNodesPanelProps { + agentType: AgentType; + + // Node data + nodes: Accessor; + discoveredNodes: Accessor; + + // State data for tables + stateNodes?: any[]; + stateHosts?: any[]; + statePbs?: any[]; + statePmg?: any[]; + + // Temperature monitoring + temperatureMonitoringEnabled: Accessor; + temperatureTransports?: Accessor; + + // Discovery settings + discoveryEnabled: Accessor; + discoveryMode: Accessor<'auto' | 'custom'>; + discoveryScanStatus: Accessor; + envOverrides: Accessor>; + savingDiscoverySettings: Accessor; + + // Loading state + initialLoadComplete: Accessor; + + // Actions + handleDiscoveryEnabledChange: (enabled: boolean) => Promise; + triggerDiscoveryScan: (opts: { quiet: boolean }) => Promise; + loadDiscoveredNodes: () => Promise; + testNodeConnection: (node: NodeConfig) => Promise; + requestDeleteNode: (node: NodeConfig) => void; + refreshClusterNodes?: (clusterId: string, sampleNodeId: string) => Promise; + + // Modal controls + setEditingNode: Setter; + setCurrentNodeType: Setter; + setModalResetKey: Setter; + setShowNodeModal: Setter; + + // For PMG edit workaround + allNodes?: Accessor; + + // Utility + formatRelativeTime: (date: string | undefined) => string; +} + +const AGENT_CONFIG: Record JSX.Element; + discoveryTooltip: string; +}> = { + pve: { + title: 'Proxmox VE nodes', + addButtonText: 'Add PVE Node', + emptyTitle: 'No PVE nodes configured', + emptyDescription: 'Add a Proxmox VE node to start monitoring your infrastructure', + scanningText: 'Scanning your network for Proxmox VE servers…', + icon: () => , + discoveryTooltip: 'Enable automatic discovery of Proxmox servers on your network', + }, + pbs: { + title: 'Proxmox Backup Server nodes', + addButtonText: 'Add PBS Node', + emptyTitle: 'No PBS nodes configured', + emptyDescription: 'Add a Proxmox Backup Server to monitor your backup infrastructure', + scanningText: 'Scanning your network for Proxmox Backup Servers…', + icon: () => , + discoveryTooltip: 'Enable automatic discovery of PBS servers on your network', + }, + pmg: { + title: 'Proxmox Mail Gateway nodes', + addButtonText: 'Add PMG Node', + emptyTitle: 'No PMG nodes configured', + emptyDescription: 'Add a Proxmox Mail Gateway to monitor mail queue and quarantine metrics', + scanningText: 'Scanning network...', + icon: () => , + discoveryTooltip: 'Enable automatic discovery of PMG servers on your network', + }, +}; + +export const ProxmoxAgentNodesPanel: Component = (props) => { + const config = () => AGENT_CONFIG[props.agentType]; + const filteredDiscoveredNodes = () => props.discoveredNodes().filter((n) => n.type === props.agentType); + + const handleAddNode = () => { + props.setEditingNode(null); + props.setCurrentNodeType(props.agentType); + props.setModalResetKey((prev) => prev + 1); + props.setShowNodeModal(true); + }; + + const handleRefreshDiscovery = async () => { + notificationStore.info('Refreshing discovery...', 2000); + try { + await props.triggerDiscoveryScan({ quiet: true }); + } finally { + await props.loadDiscoveredNodes(); + } + }; + + const handleDiscoveredNodeClick = (server: DiscoveredServer) => { + if (props.agentType === 'pmg') { + // PMG uses a different approach - sets up modal then fills input + props.setEditingNode(null); + props.setCurrentNodeType('pmg'); + props.setModalResetKey((prev) => prev + 1); + props.setShowNodeModal(true); + setTimeout(() => { + const hostInput = document.querySelector( + 'input[placeholder*="192.168"]', + ) as HTMLInputElement; + if (hostInput) { + hostInput.value = server.ip; + hostInput.dispatchEvent(new Event('input', { bubbles: true })); + } + }, 50); + } else { + // PVE and PBS pre-fill the node object + const baseNode: NodeConfigWithStatus = { + id: '', + type: props.agentType, + name: server.hostname || `${props.agentType}-${server.ip}`, + host: `https://${server.ip}:${server.port}`, + user: '', + tokenName: '', + tokenValue: '', + verifySSL: false, + status: 'pending', + }; + + if (props.agentType === 'pve') { + Object.assign(baseNode, { + monitorVMs: true, + monitorContainers: true, + monitorStorage: true, + monitorBackups: true, + monitorPhysicalDisks: false, + }); + } else if (props.agentType === 'pbs') { + Object.assign(baseNode, { + monitorDatastores: true, + monitorSyncJobs: true, + monitorVerifyJobs: true, + monitorPruneJobs: true, + monitorGarbageJobs: true, + }); + } + + props.setEditingNode(baseNode); + props.setCurrentNodeType(props.agentType); + props.setShowNodeModal(true); + } + }; + + const renderNodesTable = () => { + if (props.agentType === 'pve') { + return ( + { + props.setEditingNode(node as NodeConfigWithStatus); + props.setCurrentNodeType('pve'); + props.setShowNodeModal(true); + }} + onDelete={props.requestDeleteNode} + onRefreshCluster={props.refreshClusterNodes} + /> + ); + } else if (props.agentType === 'pbs') { + return ( + { + props.setEditingNode(node as NodeConfigWithStatus); + props.setCurrentNodeType('pbs'); + props.setShowNodeModal(true); + }} + onDelete={props.requestDeleteNode} + /> + ); + } else { + return ( + { + props.setEditingNode(props.allNodes?.().find((n) => n.id === node.id) as NodeConfigWithStatus ?? null); + props.setCurrentNodeType('pmg'); + props.setModalResetKey((prev) => prev + 1); + props.setShowNodeModal(true); + }} + onDelete={props.requestDeleteNode} + /> + ); + } + }; + + return ( +
+
+ {/* Loading state */} + +
+ Loading configuration... +
+
+ + {/* Main content */} + + +
+ {/* Header with actions */} +
+

+ {config().title} +

+
+ {/* Discovery toggle */} +
+ + Discovery + + { + if ( + props.envOverrides().discoveryEnabled || + props.savingDiscoverySettings() + ) { + e.preventDefault(); + return; + } + const success = await props.handleDiscoveryEnabledChange( + e.currentTarget.checked, + ); + if (!success) { + e.currentTarget.checked = props.discoveryEnabled(); + } + }} + disabled={ + props.envOverrides().discoveryEnabled || props.savingDiscoverySettings() + } + containerClass="gap-2" + label={ + + {props.discoveryEnabled() ? 'On' : 'Off'} + + } + /> +
+ + {/* Refresh button */} + + + + + {/* Add button */} + +
+
+ + {/* Nodes table */} + 0}> + {renderNodesTable()} + + + {/* Empty state */} + +
+
+ {config().icon()} +
+

+ {config().emptyTitle} +

+

+ {config().emptyDescription} +

+
+
+
+
+
+ + {/* Discovery section */} + +
+ {/* Scan status */} +
+ + + + {config().scanningText} + + + + + Last scan{' '} + {props.formatRelativeTime( + props.discoveryScanStatus().lastResultAt ?? + props.discoveryScanStatus().lastScanStartedAt, + )} + + +
+ + {/* Discovery errors */} + +
+ Discovery issues: +
    + + {(err) =>
  • {err}
  • } +
    +
+ + /timed out|timeout/i.test(err), + ) + } + > +

+ Large networks can time out in auto mode. Switch to a custom subnet + for faster, targeted scans. +

+
+
+
+ + {/* Scanning placeholder */} + + +
+ + + + +

Scanning for PMG servers...

+
+
+ +
+ + + Waiting for responses… this can take up to a minute depending on your + network size. + +
+
+
+ + {/* Discovered nodes list */} + + {(server) => ( + + {/* PMG style */} +
handleDiscoveredNodeClick(server)} + > +
+
+ + + + +
+

+ {server.hostname || `PMG at ${server.ip}`} +

+

+ {server.ip}:{server.port} +

+
+ + Discovered + + + Click to configure + +
+
+
+ + + +
+
+
+ )} +
+ + {(server) => ( + + {/* PVE/PBS style */} +
handleDiscoveredNodeClick(server)} + > +
+
+
+
+
+

+ {server.hostname || `${props.agentType === 'pve' ? 'Proxmox VE' : 'Backup Server'} at ${server.ip}`} +

+

+ {server.ip}:{server.port} +

+
+ + Discovered + + + Click to configure + +
+
+
+
+ + + +
+
+
+ )} +
+
+
+
+
+ ); +}; diff --git a/frontend-modern/src/components/Settings/SecurityAuthPanel.tsx b/frontend-modern/src/components/Settings/SecurityAuthPanel.tsx new file mode 100644 index 0000000..65c4177 --- /dev/null +++ b/frontend-modern/src/components/Settings/SecurityAuthPanel.tsx @@ -0,0 +1,320 @@ +import { Component, Show, Accessor, Setter } from 'solid-js'; +import { Card } from '@/components/shared/Card'; +import { SectionHeader } from '@/components/shared/SectionHeader'; +import { Toggle } from '@/components/shared/Toggle'; +import type { ToggleChangeEvent } from '@/components/shared/Toggle'; +import { QuickSecuritySetup } from './QuickSecuritySetup'; +import Lock from 'lucide-solid/icons/lock'; +import type { VersionInfo } from '@/api/updates'; + +interface SecurityStatusInfo { + hasAuthentication: boolean; + apiTokenConfigured: boolean; + authUsername?: string; + configuredButPendingRestart?: boolean; + hasProxyAuth?: boolean; + proxyAuthUsername?: string; + proxyAuthIsAdmin?: boolean; + proxyAuthLogoutURL?: string; + deprecatedDisableAuth?: boolean; +} + +interface SecurityAuthPanelProps { + securityStatus: Accessor; + securityStatusLoading: Accessor; + versionInfo: Accessor; + authDisabledByEnv: Accessor; + showQuickSecuritySetup: Accessor; + setShowQuickSecuritySetup: Setter; + showQuickSecurityWizard: Accessor; + setShowQuickSecurityWizard: Setter; + showPasswordModal: Accessor; + setShowPasswordModal: Setter; + hideLocalLogin: Accessor; + hideLocalLoginLocked: () => boolean; + savingHideLocalLogin: Accessor; + handleHideLocalLoginChange: (enabled: boolean) => Promise; + loadSecurityStatus: () => Promise; +} + +export const SecurityAuthPanel: Component = (props) => { + return ( +
+ {/* Show message when auth is disabled */} + + + {/* Header */} +
+
+
+ + + +
+ + + Controlled by DISABLE_AUTH + + } + > + + +
+
+ + {/* Content */} +
+

+ + Authentication is currently disabled. Set up password authentication to protect + your Pulse instance. + + } + > + Authentication settings are locked by the legacy{' '} + + DISABLE_AUTH + {' '} + environment variable. Remove it from your deployment and restart Pulse before + enabling security from this page. + +

+ + + { + props.setShowQuickSecuritySetup(false); + props.loadSecurityStatus(); + }} + /> + +
+
+
+ + {/* Authentication */} + + + {/* Header */} +
+
+
+ +
+ +
+
+ + {/* Content */} +
+
+ + + Remove DISABLE_AUTH to rotate credentials + + } + > + + +
+
+ User:{' '} + {props.securityStatus()?.authUsername || 'Not configured'} +
+
+ +
+ + props.handleHideLocalLoginChange(e.currentTarget.checked) + } + disabled={props.hideLocalLoginLocked() || props.savingHideLocalLogin()} + locked={props.hideLocalLoginLocked()} + lockedMessage="This setting is managed by the PULSE_AUTH_HIDE_LOCAL_LOGIN environment variable" + /> +
+ + +
+ { + props.setShowQuickSecurityWizard(false); + props.loadSecurityStatus(); + }} + /> +
+
+
+
+
+ + {/* Show pending restart message if configured but not loaded */} + +
+
+
+ + + +
+
+

+ Security Configured - Restart Required +

+

+ Security settings have been configured but the service needs to be restarted to + activate them. +

+

+ After restarting, you'll need to log in with your saved credentials. +

+ +
+

+ How to restart Pulse: +

+ + +
+

+ Type{' '} + update{' '} + in your ProxmoxVE console +

+

+ Or restart manually with: systemctl restart pulse +

+
+
+ + +
+

+ Restart your Docker container: +

+ + docker restart pulse + +
+
+ + +
+

Restart the service:

+ + sudo systemctl restart pulse + +
+
+ + +
+

+ Restart the development server: +

+ + sudo systemctl restart pulse-hot-dev + +
+
+ + +
+

+ Restart Pulse using your deployment method +

+
+
+
+ +
+

+ Tip: Make sure you've saved your credentials before restarting! +

+
+
+
+
+
+
+ ); +}; diff --git a/frontend-modern/src/components/Settings/SecurityOverviewPanel.tsx b/frontend-modern/src/components/Settings/SecurityOverviewPanel.tsx new file mode 100644 index 0000000..61fed42 --- /dev/null +++ b/frontend-modern/src/components/Settings/SecurityOverviewPanel.tsx @@ -0,0 +1,89 @@ +import { Component, Show, Accessor } from 'solid-js'; +import { Card } from '@/components/shared/Card'; +import { SecurityPostureSummary } from './SecurityPostureSummary'; + +interface SecurityStatusInfo { + hasAuthentication: boolean; + oidcEnabled?: boolean; + hasProxyAuth?: boolean; + apiTokenConfigured: boolean; + exportProtected: boolean; + unprotectedExportAllowed?: boolean; + hasHTTPS?: boolean; + hasAuditLogging: boolean; + requiresAuth: boolean; + publicAccess?: boolean; + isPrivateNetwork?: boolean; + clientIP?: string; + proxyAuthUsername?: string; + proxyAuthIsAdmin?: boolean; + proxyAuthLogoutURL?: string; +} + +interface SecurityOverviewPanelProps { + securityStatus: Accessor; + securityStatusLoading: Accessor; +} + +export const SecurityOverviewPanel: Component = (props) => { + return ( +
+ + + + + + +
+
+ + + + + Proxy authentication detected + +
+

+ Requests are validated by an upstream proxy. The current proxied user is + {props.securityStatus()?.proxyAuthUsername + ? ` ${props.securityStatus()?.proxyAuthUsername}` + : ' available once a request is received'} + . + {props.securityStatus()?.proxyAuthIsAdmin ? ' Admin privileges confirmed.' : ''} + + {' '} + + Proxy logout + + +

+

+ Need configuration tips? Review the proxy auth guide in the docs.{' '} + + Read proxy auth guide → + +

+
+
+
+
+ ); +}; diff --git a/frontend-modern/src/components/Settings/Settings.tsx b/frontend-modern/src/components/Settings/Settings.tsx index 446886e..7d5c905 100644 --- a/frontend-modern/src/components/Settings/Settings.tsx +++ b/frontend-modern/src/components/Settings/Settings.tsx @@ -23,13 +23,17 @@ import { import { NodeModal } from './NodeModal'; import { ChangePasswordModal } from './ChangePasswordModal'; import { UnifiedAgents } from './UnifiedAgents'; -import APITokenManager from './APITokenManager'; import { OIDCPanel } from './OIDCPanel'; import { AISettings } from './AISettings'; import { AICostDashboard } from '@/components/AI/AICostDashboard'; -import { QuickSecuritySetup } from './QuickSecuritySetup'; -import { SecurityPostureSummary } from './SecurityPostureSummary'; import { DiagnosticsPanel } from './DiagnosticsPanel'; +import { GeneralSettingsPanel } from './GeneralSettingsPanel'; +import { NetworkSettingsPanel } from './NetworkSettingsPanel'; +import { UpdatesSettingsPanel } from './UpdatesSettingsPanel'; +import { BackupsSettingsPanel } from './BackupsSettingsPanel'; +import { SecurityAuthPanel } from './SecurityAuthPanel'; +import { APIAccessPanel } from './APIAccessPanel'; +import { SecurityOverviewPanel } from './SecurityOverviewPanel'; import { PveNodesTable, PbsNodesTable, @@ -68,13 +72,6 @@ import { eventBus } from '@/stores/events'; import { notificationStore } from '@/stores/notifications'; import { updateStore } from '@/stores/updates'; -const COMMON_DISCOVERY_SUBNETS = [ - '192.168.1.0/24', - '192.168.0.0/24', - '10.0.0.0/24', - '172.16.0.0/24', - '192.168.10.0/24', -]; // Type definitions interface DiscoveredServer { ip: string; @@ -398,8 +395,6 @@ const BACKUP_INTERVAL_OPTIONS = [ { label: '24 hours', value: 24 * 60 * 60 }, ]; -const BACKUP_INTERVAL_MAX_MINUTES = 7 * 24 * 60; // 7 days - const PVE_POLLING_MIN_SECONDS = 10; const PVE_POLLING_MAX_SECONDS = 3600; const PVE_POLLING_PRESETS = [ @@ -522,9 +517,6 @@ const Settings: Component = (props) => { description: 'Manage Pulse configuration.', }; - const _pveBackupsState = () => state.backups?.pve ?? state.pveBackups; - const _pbsBackupsState = () => state.backups?.pbs ?? state.pbsBackups; - // Keep tab state in sync with URL and handle /settings redirect without flicker createEffect( on( @@ -3406,1348 +3398,96 @@ const Settings: Component = (props) => { {/* System General Tab */} -
- -
-
-
- -
- -
-
-
-
-
-

Dark mode

-

- Toggle to match your environment. Pulse remembers this preference on - each browser. -

-
- { - const desired = (event.currentTarget as HTMLInputElement).checked; - if (desired !== props.darkMode()) { - props.toggleDarkMode(); - } - }} - /> -
-
-
- -
-
-
- -
- -
-
-
-
-

- Shorter intervals provide near-real-time updates at the cost of higher API - and CPU usage on each node. Set a longer interval to reduce load on busy - clusters. -

-

- Current cadence: {pvePollingInterval()} seconds ( - {pvePollingInterval() >= 60 - ? `${(pvePollingInterval() / 60).toFixed( - pvePollingInterval() % 60 === 0 ? 0 : 1, - )} minute${pvePollingInterval() / 60 === 1 ? '' : 's'}` - : 'under a minute'} - ). -

-
-
-
- - {(option) => ( - - )} - - -
- -
- - { - if (pvePollingEnvLocked()) return; - const parsed = Math.floor(Number(e.currentTarget.value)); - if (Number.isNaN(parsed)) { - return; - } - const clamped = Math.min( - PVE_POLLING_MAX_SECONDS, - Math.max(PVE_POLLING_MIN_SECONDS, parsed), - ); - setPVEPollingCustomSeconds(clamped); - setPVEPollingInterval(clamped); - setHasUnsavedChanges(true); - }} - /> -

- Applies to all PVE clusters and standalone nodes. -

-
-
- -
- - - - - - Managed via environment variable PVE_POLLING_INTERVAL. -
-
-
-
-
-
+
{/* System Network Tab */} -
- -
- - - -
-

Configuration Priority

-
    -
  • - • Some env vars override settings (API_TOKENS, legacy API_TOKEN, PORTS, - AUTH) -
  • -
  • • Changes made here are saved to system.json immediately
  • -
  • • Settings persist unless overridden by env vars
  • -
-
-
-
- -
-
-
- -
- -
-
-
-
- -
-
-

- Automatic scanning -

-

- Enable discovery to surface Proxmox VE, PBS, and PMG endpoints - automatically. -

-
- { - if (envOverrides().discoveryEnabled || savingDiscoverySettings()) { - e.preventDefault(); - return; - } - const success = await handleDiscoveryEnabledChange( - e.currentTarget.checked, - ); - if (!success) { - e.currentTarget.checked = discoveryEnabled(); - } - }} - disabled={envOverrides().discoveryEnabled || savingDiscoverySettings()} - containerClass="gap-2" - label={ - - {discoveryEnabled() ? 'On' : 'Off'} - - } - /> -
- - -
-
- - Scan scope - -
- - - - -
- - Common networks: - - - {(preset) => { - const baseValue = currentDraftSubnetValue(); - const currentSelections = parseSubnetList(baseValue); - const isActive = currentSelections.includes(preset); - return ( - - ); - }} - -
-
-
-
- -
-
- - - - - - - - -
- { - discoverySubnetInputRef = el; - }} - type="text" - value={discoverySubnetDraft()} - placeholder={ - discoveryMode() === 'auto' - ? 'auto (scan every network phase)' - : '192.168.1.0/24, 10.0.0.0/24' - } - class={`w-full rounded-lg border px-3 py-2 text-sm transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 ${envOverrides().discoverySubnet - ? 'border-amber-300 bg-amber-50 text-amber-800 dark:border-amber-600 dark:bg-amber-900/20 dark:text-amber-200 cursor-not-allowed opacity-60' - : 'border-gray-300 bg-white dark:border-gray-600 dark:bg-gray-900/70' - }`} - disabled={envOverrides().discoverySubnet} - onInput={(e) => { - if (envOverrides().discoverySubnet) { - return; - } - const rawValue = e.currentTarget.value; - setDiscoverySubnetDraft(rawValue); - if (discoveryMode() !== 'custom') { - setDiscoveryMode('custom'); - } - setLastCustomSubnet(rawValue); - const trimmed = rawValue.trim(); - if (!trimmed) { - setDiscoverySubnetError(undefined); - return; - } - if (!isValidCIDR(trimmed)) { - setDiscoverySubnetError( - 'Use CIDR format such as 192.168.1.0/24 (comma-separated for multiple)', - ); - } else { - setDiscoverySubnetError(undefined); - } - }} - onBlur={async (e) => { - if ( - envOverrides().discoverySubnet || - discoveryMode() !== 'custom' - ) { - return; - } - const rawValue = e.currentTarget.value; - setDiscoverySubnetDraft(rawValue); - const trimmed = rawValue.trim(); - if (!trimmed) { - setDiscoverySubnetError( - 'Enter at least one subnet in CIDR format (e.g., 192.168.1.0/24)', - ); - return; - } - if (!isValidCIDR(trimmed)) { - setDiscoverySubnetError( - 'Use CIDR format such as 192.168.1.0/24 (comma-separated for multiple)', - ); - return; - } - setDiscoverySubnetError(undefined); - await commitDiscoverySubnet(rawValue); - }} - onKeyDown={(e) => { - if (e.key === 'Enter') { - e.preventDefault(); - (e.currentTarget as HTMLInputElement).blur(); - } - }} - /> - -

- {discoverySubnetError()} -

-
- -

- Auto scans every reachable network phase. Large networks may time - out — switch to custom subnets to narrow the search. -

-
- -

- Example: 192.168.1.0/24, 10.0.0.0/24 (comma-separated). Smaller - ranges finish faster and avoid timeouts. -

-
-
-
-
- - -
- Discovery settings are locked by environment variables. Update the - service configuration and restart Pulse to change them here. -
-
-
- -
-

- - - - - Network Settings -

-
- -

- For reverse proxy setups (* = allow all, empty = same-origin only) -

-
- { - if (!envOverrides().allowedOrigins) { - setAllowedOrigins(e.currentTarget.value); - setHasUnsavedChanges(true); - } - }} - disabled={envOverrides().allowedOrigins} - placeholder="* or https://example.com" - class={`w-full px-3 py-1.5 text-sm border rounded-lg ${envOverrides().allowedOrigins - ? 'border-amber-300 dark:border-amber-600 bg-amber-50 dark:bg-amber-900/20 cursor-not-allowed opacity-75' - : 'border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800' - }`} - /> - {envOverrides().allowedOrigins && ( -
-
- - - - Overridden by ALLOWED_ORIGINS environment variable -
-
- Remove the env var and restart to enable UI configuration -
-
- )} -
-
-
- -
-

- - - - - Embedding -

-

- Allow Pulse to be embedded in iframes (e.g., Homepage dashboard) -

-
-
- { - setAllowEmbedding(e.currentTarget.checked); - setHasUnsavedChanges(true); - }} - class="rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500" - /> - -
- - -
- -

- Comma-separated list of origins that can embed Pulse (leave empty - for same-origin only) -

- { - setAllowedEmbedOrigins(e.currentTarget.value); - setHasUnsavedChanges(true); - }} - placeholder="https://my.domain, https://dashboard.example.com" - class="w-full px-3 py-1.5 text-sm border rounded-lg border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800" - /> -

- Example: If Pulse is at pulse.my.domain and your - dashboard is at my.domain, add{' '} - https://my.domain here. -

-
-
-
-
- - {/* Webhook Security Settings */} -
-

- - - - Webhook Security -

-
-
- -

- By default, webhooks to private IP addresses are blocked for - security. Enter trusted CIDR ranges to allow webhooks to internal - services (leave empty to block all private IPs). -

- { - setWebhookAllowedPrivateCIDRs(e.currentTarget.value); - setHasUnsavedChanges(true); - }} - placeholder="192.168.1.0/24, 10.0.0.0/8" - class="w-full px-3 py-1.5 text-sm border rounded-lg border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800" - /> -

- Example: 192.168.1.0/24,10.0.0.0/8 allows webhooks to - these private networks. Localhost and cloud metadata services - remain blocked. -

-
-
-
- - -

- Port Configuration: Use{' '} - - systemctl edit pulse - -

-

- [Service] -
- Environment="FRONTEND_PORT=8080" -
- - Then restart: sudo systemctl restart pulse - -

-
-
-
-
+ { + discoverySubnetInputRef = el; + }} + />
{/* System Updates Tab */} -
- -
-
-
- -
- -
-
-
-
-
-
-
- -

- {versionInfo()?.version || 'Loading...'} - {versionInfo()?.isDevelopment && ' (Development)'} - {versionInfo()?.isDocker && ' - Docker'} -

-
- -
- - -
-

- Docker Installation: Updates are managed through - Docker. Pull the latest image to update. -

-
-
- - -
-

- Built from source: Pull the latest code from git - and rebuild to update. -

-
-
- - -
-

- {updateInfo()?.warning} -

-
-
- - -
-
-

- Update Available: {updateInfo()?.latestVersion} -

-

- Released:{' '} - {updateInfo()?.releaseDate - ? new Date(updateInfo()!.releaseDate).toLocaleDateString() - : 'Unknown'} -

-
- -
-

- How to update: -

- -

- 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: -

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

- Pull latest changes and rebuild -

-
- -

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

-
-
- - -
- - Release Notes - -
-                                    {updateInfo()?.releaseNotes}
-                                  
-
-
-
-
- -
-
-
- -

- Choose between stable and release candidate versions -

-
- -
- -
-
- -

- Automatically check for updates (installation is manual) -

-
- -
- - -
-
-
- -

- How often to check for updates -

-
- -
- -
-
- -

- Preferred time to check for updates -

-
- { - setAutoUpdateTime(e.currentTarget.value); - setHasUnsavedChanges(true); - }} - class="px-3 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800" - /> -
-
-
-
-
-
-
-
-
+
{/* System Backups Tab */} -
- -
-
-
- -
- -
-
-
-
-

- - - - - Backup polling -

-

- Control how often Pulse queries Proxmox backup tasks, datastore contents, - and guest snapshots. Longer intervals reduce disk activity and API load. -

-
-
-
-

- Enable backup polling -

-

- Required for dashboard backup status, storage snapshots, and - alerting. -

-
- -
- - -
-
-
- -

- {backupIntervalSummary()} -

-
- -
- - -
- -
- { - const value = Number(e.currentTarget.value); - if (Number.isNaN(value)) { - return; - } - const clamped = Math.max( - 1, - Math.min(BACKUP_INTERVAL_MAX_MINUTES, Math.floor(value)), - ); - setBackupPollingCustomMinutes(clamped); - setBackupPollingInterval(clamped * 60); - if (!backupPollingEnvLocked()) { - setHasUnsavedChanges(true); - } - }} - class="w-24 px-3 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 disabled:opacity-50" - /> - - 1 – {BACKUP_INTERVAL_MAX_MINUTES} minutes (≈7 days max) - -
-
-
-
-
- - -
- - - -
-

Environment override detected

-

- The ENABLE_BACKUP_POLLING or{' '} - BACKUP_POLLING_INTERVAL environment - variables are set. Remove them and restart Pulse to manage backup - polling here. -

-
-
-
-
-
- - - -
- {/* Export Section */} -
-
-
- - - -
-
-

- Export Configuration -

-

- Download an encrypted backup of all nodes and settings -

- -
-
-
- - {/* Import Section */} -
-
-
- - - -
-
-

- Restore Configuration -

-

- Upload a backup file to restore nodes and settings -

- -
-
-
-
- -
-
- - - -
-

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
  • -
-
-
-
-
-
-
+
{/* AI Assistant Tab */} @@ -4760,403 +3500,42 @@ const Settings: Component = (props) => { {/* API Access */} -
- -
-
-
- -
- -
-
-
-

- Generate scoped tokens for Docker agents, host agents, and automation - pipelines. Tokens are shown once—store them securely and rotate when - infrastructure changes. -

- - View scope reference - -
-
- - { - void loadSecurityStatus(); - }} - refreshing={securityStatusLoading()} - /> -
+ { + void loadSecurityStatus(); + }} + refreshing={securityStatusLoading()} + />
{/* Security Overview Tab */} -
- - - - - - -
-
- - - - - Proxy authentication detected - -
-

- Requests are validated by an upstream proxy. The current proxied user is - {securityStatus()?.proxyAuthUsername - ? ` ${securityStatus()?.proxyAuthUsername}` - : ' available once a request is received'} - . - {securityStatus()?.proxyAuthIsAdmin ? ' Admin privileges confirmed.' : ''} - - {' '} - - Proxy logout - - -

-

- Need configuration tips? Review the proxy auth guide in the docs.{' '} - - Read proxy auth guide → - -

-
-
-
-
+
{/* Security Authentication Tab */} -
- {/* Show message when auth is disabled */} - - - {/* Header */} -
-
-
- - - -
- - - Controlled by DISABLE_AUTH - - } - > - - -
-
- - {/* Content */} -
-

- - Authentication is currently disabled. Set up password authentication - to protect your Pulse instance. - - } - > - Authentication settings are locked by the legacy{' '} - - DISABLE_AUTH - {' '} - environment variable. Remove it from your deployment and restart Pulse - before enabling security from this page. - -

- - - { - setShowQuickSecuritySetup(false); - loadSecurityStatus(); - }} - /> - -
-
-
- - {/* Authentication */} - - - {/* Header */} -
-
-
- -
- -
-
- - {/* Content */} -
-
- - - Remove DISABLE_AUTH to rotate credentials - - } - > - - -
-
- User:{' '} - {securityStatus()?.authUsername || 'Not configured'} -
-
- -
- handleHideLocalLoginChange(e.currentTarget.checked)} - disabled={hideLocalLoginLocked() || savingHideLocalLogin()} - locked={hideLocalLoginLocked()} - lockedMessage="This setting is managed by the PULSE_AUTH_HIDE_LOCAL_LOGIN environment variable" - /> -
- - -
- { - setShowQuickSecurityWizard(false); - loadSecurityStatus(); - }} - /> -
-
-
-
-
- - {/* Show pending restart message if configured but not loaded */} - -
-
-
- - - -
-
-

- Security Configured - Restart Required -

-

- Security settings have been configured but the service needs to be - restarted to activate them. -

-

- After restarting, you'll need to log in with your saved credentials. -

- -
-

- How to restart Pulse: -

- - -
-

- Type{' '} - - update - {' '} - in your ProxmoxVE console -

-

- Or restart manually with:{' '} - systemctl restart pulse -

-
-
- - -
-

- Restart your Docker container: -

- - docker restart pulse - -
-
- - -
-

- Restart the service: -

- - sudo systemctl restart pulse - -
-
- - -
-

- Restart the development server: -

- - sudo systemctl restart pulse-hot-dev - -
-
- - -
-

- Restart Pulse using your deployment method -

-
-
-
- -
-

- Tip: Make sure you've saved your credentials - before restarting! -

-
-
-
-
-
-
+
{/* Security Single Sign-On Tab */} diff --git a/frontend-modern/src/components/Settings/UpdatesSettingsPanel.tsx b/frontend-modern/src/components/Settings/UpdatesSettingsPanel.tsx new file mode 100644 index 0000000..fadf55b --- /dev/null +++ b/frontend-modern/src/components/Settings/UpdatesSettingsPanel.tsx @@ -0,0 +1,310 @@ +import { Component, Show, Accessor, Setter } from 'solid-js'; +import { Card } from '@/components/shared/Card'; +import { SectionHeader } from '@/components/shared/SectionHeader'; +import RefreshCw from 'lucide-solid/icons/refresh-cw'; +import type { UpdateInfo, VersionInfo } from '@/api/updates'; + +interface UpdatesSettingsPanelProps { + versionInfo: Accessor; + updateInfo: Accessor; + checkingForUpdates: Accessor; + updateChannel: Accessor<'stable' | 'rc'>; + setUpdateChannel: Setter<'stable' | 'rc'>; + autoUpdateEnabled: Accessor; + setAutoUpdateEnabled: Setter; + autoUpdateCheckInterval: Accessor; + setAutoUpdateCheckInterval: Setter; + autoUpdateTime: Accessor; + setAutoUpdateTime: Setter; + checkForUpdates: () => Promise; + setHasUnsavedChanges: Setter; +} + +export const UpdatesSettingsPanel: Component = (props) => { + return ( +
+ + {/* Header */} +
+
+
+ +
+ +
+
+ +
+
+
+ {/* Current Version */} +
+
+ +

+ {props.versionInfo()?.version || 'Loading...'} + {props.versionInfo()?.isDevelopment && ' (Development)'} + {props.versionInfo()?.isDocker && ' - Docker'} +

+
+ +
+ + {/* Docker installation notice */} + +
+

+ Docker Installation: Updates are managed through Docker. Pull + the latest image to update. +

+
+
+ + {/* Source build notice */} + +
+

+ Built from source: Pull the latest code from git and rebuild to + update. +

+
+
+ + {/* Warning message */} + +
+

+ {props.updateInfo()?.warning} +

+
+
+ + {/* Update available */} + +
+
+

+ Update Available: {props.updateInfo()?.latestVersion} +

+

+ Released:{' '} + {props.updateInfo()?.releaseDate + ? new Date(props.updateInfo()!.releaseDate).toLocaleDateString() + : 'Unknown'} +

+
+ +
+

+ How to update: +

+ +

+ 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: +

+ + 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 +

+
+ +

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

+
+
+ + {/* Release notes */} + +
+ + Release Notes + +
+                        {props.updateInfo()?.releaseNotes}
+                      
+
+
+
+
+ + {/* Update settings */} +
+ {/* Update Channel */} +
+
+ +

+ Choose between stable and release candidate versions +

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

+ Automatically check for updates (installation is manual) +

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

+ How often to check for updates +

+
+ +
+ + {/* Check Time */} +
+
+ +

+ Preferred time to check for updates +

+
+ { + props.setAutoUpdateTime(e.currentTarget.value); + props.setHasUnsavedChanges(true); + }} + class="px-3 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800" + /> +
+
+
+
+
+
+
+
+
+ ); +};