feat: major installer improvements for Proxmox environments

- Auto-detects Proxmox VE hosts and creates LXC containers
- Quick mode with sensible defaults (1GB RAM, 4GB disk)
- Advanced mode for full customization
- Automatic cleanup on failure
- Simple 'update' command in containers
- Improved error handling and network detection
- Professional, clean output without verbose noise
- Docker detection to prevent container-in-container
- Removed all references to community scripts

This is now the primary recommended installation method.
This commit is contained in:
Pulse Monitor
2025-08-21 20:43:56 +00:00
parent 740fe36cec
commit ad00943717
9 changed files with 483 additions and 122 deletions
+6 -5
View File
@@ -48,20 +48,21 @@ Pulse is built by a solo developer in evenings and weekends. Your support helps:
### Install
```bash
# Option A: Official installer (recommended for systemd/bare metal)
# Official installer (auto-detects environment)
curl -fsSL https://raw.githubusercontent.com/rcourtman/Pulse/main/install.sh | sudo bash
# Option B: Docker
# Docker
docker run -d -p 7655:7655 -v pulse_data:/data rcourtman/pulse:latest
# Option C: Proxmox Helper Script (creates LXC container)
bash -c "$(wget -qLO - https://github.com/community-scripts/ProxmoxVE/raw/main/ct/pulse.sh)"
# Install specific version or release channel
curl -fsSL https://raw.githubusercontent.com/rcourtman/Pulse/main/install.sh | sudo bash -s -- --rc # Latest RC
curl -fsSL https://raw.githubusercontent.com/rcourtman/Pulse/main/install.sh | sudo bash -s -- --version v4.5.0 # Specific version
```
**Note for Proxmox users:** The installer automatically detects Proxmox VE hosts and creates an LXC container with optimized settings. Choose Quick mode for a one-minute automated setup.
📖 [Detailed Installation Guide →](docs/INSTALL.md)
### Initial Setup
**Option A: Interactive Setup (UI)**
+2
View File
@@ -1,5 +1,7 @@
# Docker Deployment Guide
> **Proxmox VE Users:** Consider using the [official installer](https://github.com/rcourtman/Pulse#install) instead, which automatically creates an optimized LXC container.
## Quick Start
```bash
+119
View File
@@ -0,0 +1,119 @@
# Installation Guide
## Quick Install
The official installer automatically detects your environment and chooses the best installation method:
```bash
curl -fsSL https://raw.githubusercontent.com/rcourtman/Pulse/main/install.sh | sudo bash
```
## Installation Methods
### Proxmox VE Hosts
When run on a Proxmox VE host, the installer automatically:
1. Creates a lightweight LXC container
2. Installs Pulse inside the container
3. Configures networking and security
**Quick Mode** (recommended):
- 1GB RAM, 4GB disk, 2 CPU cores
- Unprivileged container with firewall
- Auto-starts with your host
- Takes about 1 minute
**Advanced Mode**:
- Customize all container settings
- Choose specific network bridges and storage
- Configure static IP if needed
### Standard Linux Systems
On Debian/Ubuntu systems, the installer:
1. Installs required dependencies
2. Downloads the latest Pulse binary
3. Creates a systemd service
4. Starts Pulse automatically
### Docker
For containerized deployments:
```bash
docker run -d -p 7655:7655 -v pulse_data:/data rcourtman/pulse:latest
```
See [Docker Guide](DOCKER.md) for advanced options.
## Updating
### For LXC Containers
```bash
pct exec <container-id> -- update
```
### For Standard Installations
```bash
curl -fsSL https://raw.githubusercontent.com/rcourtman/Pulse/main/install.sh | sudo bash
```
### For Docker
```bash
docker pull rcourtman/pulse:latest
docker stop pulse
docker rm pulse
docker run -d --name pulse -p 7655:7655 -v pulse_data:/data rcourtman/pulse:latest
```
## Version Management
### Install Specific Version
```bash
curl -fsSL https://raw.githubusercontent.com/rcourtman/Pulse/main/install.sh | sudo bash -s -- --version v4.6.0
```
### Install Release Candidate
```bash
curl -fsSL https://raw.githubusercontent.com/rcourtman/Pulse/main/install.sh | sudo bash -s -- --rc
```
## Troubleshooting
### Permission Denied
Make sure to run with `sudo` on standard Linux systems.
### Container Creation Failed
Ensure you have:
- Available container IDs (check with `pct list`)
- Sufficient storage space
- Network bridge configured
### Port Already in Use
Pulse uses port 7655 by default. Check with:
```bash
sudo netstat -tlnp | grep 7655
```
## Uninstalling
### From LXC Container
```bash
pct stop <container-id>
pct destroy <container-id>
```
### From Standard System
```bash
sudo systemctl stop pulse
sudo systemctl disable pulse
sudo rm -rf /opt/pulse /etc/pulse
sudo rm /etc/systemd/system/pulse.service
```
### Docker
```bash
docker stop pulse
docker rm pulse
docker volume rm pulse_data # Warning: deletes all data
```
+50 -11
View File
@@ -15,6 +15,7 @@ import { POLLING_INTERVALS, STORAGE_KEYS } from './constants';
import { UpdatesAPI } from './api/updates';
import type { VersionInfo } from './api/updates';
import { apiFetch } from './utils/apiClient';
import { SettingsAPI } from './api/settings';
type TabType = 'main' | 'storage' | 'backups' | 'alerts' | 'settings';
@@ -77,7 +78,7 @@ function App() {
const [darkMode, setDarkMode] = createSignal(false);
// Toggle dark mode
const toggleDarkMode = () => {
const toggleDarkMode = async () => {
const newMode = !darkMode();
setDarkMode(newMode);
localStorage.setItem(STORAGE_KEYS.DARK_MODE, String(newMode));
@@ -87,6 +88,17 @@ function App() {
document.documentElement.classList.remove('dark');
}
logger.info('Theme changed', { mode: newMode ? 'dark' : 'light' });
// Save theme preference to server if authenticated
if (!needsAuth()) {
try {
await SettingsAPI.updateSystemSettings({ theme: newMode ? 'dark' : 'light' });
logger.info('Theme preference saved to server');
} catch (error) {
logger.error('Failed to save theme preference to server', error);
// Don't show error to user - local change still works
}
}
};
// Don't initialize dark mode here - will be handled based on auth state
@@ -167,16 +179,43 @@ function App() {
// Only initialize WebSocket after successful auth check
setWsStore(getGlobalWebSocketStore());
// Apply user's theme preference only after successful authentication
const savedDarkMode = localStorage.getItem(STORAGE_KEYS.DARK_MODE);
const prefersDark = savedDarkMode !== null
? savedDarkMode === 'true'
: window.matchMedia('(prefers-color-scheme: dark)').matches;
setDarkMode(prefersDark);
if (prefersDark) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
// Load theme preference from server
try {
const systemSettings = await SettingsAPI.getSystemSettings();
let prefersDark = false;
if (systemSettings.theme) {
// Use server-side theme preference
prefersDark = systemSettings.theme === 'dark';
// Also update localStorage to match server
localStorage.setItem(STORAGE_KEYS.DARK_MODE, String(prefersDark));
} else {
// No server preference, check localStorage then system preference
const savedDarkMode = localStorage.getItem(STORAGE_KEYS.DARK_MODE);
prefersDark = savedDarkMode !== null
? savedDarkMode === 'true'
: window.matchMedia('(prefers-color-scheme: dark)').matches;
}
setDarkMode(prefersDark);
if (prefersDark) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
} catch (error) {
// If loading theme from server fails, fall back to localStorage
console.error('Failed to load theme from server:', error);
const savedDarkMode = localStorage.getItem(STORAGE_KEYS.DARK_MODE);
const prefersDark = savedDarkMode !== null
? savedDarkMode === 'true'
: window.matchMedia('(prefers-color-scheme: dark)').matches;
setDarkMode(prefersDark);
if (prefersDark) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
}
}
} catch (error) {
+1
View File
@@ -36,6 +36,7 @@ export interface SystemConfig {
allowedOrigins?: string; // CORS allowed origins
backendPort?: number; // Backend API port (default: 7655)
frontendPort?: number; // Frontend UI port (default: 7655)
theme?: string; // Theme preference: 'light' | 'dark' | undefined (system default)
}
/**
+282 -106
View File
@@ -111,23 +111,28 @@ check_proxmox_host() {
return 1
}
check_docker_environment() {
# Detect if we're running inside Docker
if [[ -f /.dockerenv ]] || grep -q docker /proc/1/cgroup 2>/dev/null; then
print_error "Docker environment detected"
echo "Please use the Docker image directly: docker run -d -p 7655:7655 rcourtman/pulse:latest"
echo "See: https://github.com/rcourtman/Pulse/blob/main/docs/DOCKER.md"
exit 1
fi
}
create_lxc_container() {
# Set up trap to cleanup on interrupt (CTID will be set later)
trap 'echo ""; print_error "Installation cancelled"; exit 1' INT
print_header
echo -e "${YELLOW}Proxmox VE host detected!${NC}"
echo
echo "Installing directly on a PVE host is not recommended."
echo "Pulse should run in a container for better isolation."
echo
echo -e "${GREEN}This script will:${NC}"
echo "1. Create a new LXC container"
echo "2. Install Pulse inside the container"
echo "3. Start the service"
echo "Proxmox VE detected. Installing Pulse in a container."
echo
echo "Installation mode:"
echo "1) Quick (recommended defaults)"
echo "2) Advanced (customize all settings)"
echo "3) Cancel"
echo " 1) Quick (recommended)"
echo " 2) Advanced"
echo " 3) Cancel"
read -p "Select [1-3]: " -n 1 -r mode < /dev/tty
echo
@@ -144,28 +149,72 @@ create_lxc_container() {
;;
esac
# Find next available container ID
local CTID=100
while pct status $CTID &>/dev/null; do
((CTID++))
done
# Get next available container ID from Proxmox
local CTID=$(pvesh get /cluster/nextid 2>/dev/null || echo "100")
# If pvesh failed, fallback to manual search
if [[ "$CTID" == "100" ]]; then
while pct status $CTID &>/dev/null 2>&1 || qm status $CTID &>/dev/null 2>&1; do
((CTID++))
done
fi
if [[ "$ADVANCED_MODE" == "true" ]]; then
echo
# Try to get cluster-wide IDs, fall back to local
local USED_IDS=""
if command -v pvesh &>/dev/null; then
# Parse JSON output using grep and sed (works without jq)
USED_IDS=$(pvesh get /cluster/resources --type vm --output-format json 2>/dev/null | \
grep -o '"vmid":[0-9]*' | \
sed 's/"vmid"://' | \
sort -n | \
paste -sd',' -)
fi
if [[ -z "$USED_IDS" ]]; then
# Fallback: get local containers and VMs
local LOCAL_CTS=$(pct list 2>/dev/null | tail -n +2 | awk '{print $1}' | sort -n)
local LOCAL_VMS=$(qm list 2>/dev/null | tail -n +2 | awk '{print $1}' | sort -n)
USED_IDS=$(echo -e "$LOCAL_CTS\n$LOCAL_VMS" | grep -v '^$' | sort -n | paste -sd',' -)
fi
echo "Container/VM IDs in use: ${USED_IDS:-none}"
read -p "Container ID [$CTID]: " custom_ctid < /dev/tty
if [[ -n "$custom_ctid" ]] && [[ "$custom_ctid" =~ ^[0-9]+$ ]]; then
# Check if ID is in use
if pct status $custom_ctid &>/dev/null 2>&1 || qm status $custom_ctid &>/dev/null 2>&1; then
print_error "Container/VM ID $custom_ctid is already in use"
exit 1
fi
# Also check cluster if possible
if command -v pvesh &>/dev/null; then
if pvesh get /cluster/resources --type vm 2>/dev/null | jq -e ".[] | select(.vmid == $custom_ctid)" &>/dev/null; then
print_error "Container/VM ID $custom_ctid is already in use in the cluster"
exit 1
fi
fi
CTID=$custom_ctid
fi
fi
print_info "Using container ID: $CTID"
if [[ "$ADVANCED_MODE" == "true" ]]; then
echo
echo -e "${BLUE}Advanced Mode - Customize all settings${NC}"
echo -e "${YELLOW}Defaults shown are suitable for monitoring 10-20 nodes${NC}"
echo
# Container settings
read -p "Container hostname [pulse]: " hostname < /dev/tty
hostname=${hostname:-pulse}
read -p "Memory (MB) [2048]: " memory < /dev/tty
memory=${memory:-2048}
read -p "Memory (MB) [1024]: " memory < /dev/tty
memory=${memory:-1024}
read -p "Disk size (GB) [16]: " disk < /dev/tty
disk=${disk:-16}
read -p "Disk size (GB) [4]: " disk < /dev/tty
disk=${disk:-4}
read -p "CPU cores [2]: " cores < /dev/tty
cores=${cores:-2}
@@ -173,8 +222,8 @@ create_lxc_container() {
read -p "CPU limit (0=unlimited) [2]: " cpulimit < /dev/tty
cpulimit=${cpulimit:-2}
read -p "Swap (MB) [512]: " swap < /dev/tty
swap=${swap:-512}
read -p "Swap (MB) [256]: " swap < /dev/tty
swap=${swap:-256}
read -p "Start on boot? [Y/n]: " -n 1 -r onboot < /dev/tty
echo
@@ -200,44 +249,45 @@ create_lxc_container() {
unprivileged=1
fi
else
echo
echo -e "${BLUE}Quick Mode - Using optimized defaults:${NC}"
echo " • Memory: 2GB"
echo " • CPU: 2 cores (with limit)"
echo " • Disk: 16GB"
echo " • Swap: 512MB"
echo " • Auto-start: Yes"
echo " • Firewall: Enabled"
echo " • Unprivileged: Yes (secure)"
echo
# Quick mode - just use defaults silently
# Use optimized defaults
hostname="pulse"
memory=2048
disk=16
memory=1024
disk=4
cores=2
cpulimit=2
swap=512
swap=256
onboot=1
firewall=1
unprivileged=1
# Still ask for hostname in quick mode
read -p "Container hostname [pulse]: " custom_hostname < /dev/tty
hostname=${custom_hostname:-pulse}
fi
# Get network and storage settings
# Get available network bridges
echo
print_info "Detecting available resources..."
# Get available bridges
local BRIDGES=$(ip link show type bridge | grep -E '^[0-9]+:' | cut -d: -f2 | tr -d ' ' | paste -sd',' -)
local DEFAULT_BRIDGE=$(ip route | grep default | head -1 | grep -oP 'dev \K\S+' | grep -E '^vmbr')
DEFAULT_BRIDGE=${DEFAULT_BRIDGE:-vmbr0}
local DEFAULT_STORAGE=$(pvesm status -content rootdir | grep -E "^local" | head -1 | awk '{print $1}')
# Get available storage with usage info
local STORAGE_INFO=$(pvesm status -content rootdir 2>/dev/null | tail -n +2)
local DEFAULT_STORAGE=$(echo "$STORAGE_INFO" | awk '{print $1}' | head -1)
DEFAULT_STORAGE=${DEFAULT_STORAGE:-local-lvm}
if [[ "$ADVANCED_MODE" == "true" ]]; then
# Show available bridges
echo
echo "Available network bridges: ${BRIDGES:-none detected}"
read -p "Network bridge [$DEFAULT_BRIDGE]: " bridge < /dev/tty
bridge=${bridge:-$DEFAULT_BRIDGE}
# Show available storage with usage details
echo
echo "Available storage pools:"
echo "$STORAGE_INFO" | awk '{printf " %-15s %-8s %5s used\n", $1, $2, $6}' || echo " No storage pools found"
read -p "Storage [$DEFAULT_STORAGE]: " storage < /dev/tty
storage=${storage:-$DEFAULT_STORAGE}
@@ -256,15 +306,80 @@ create_lxc_container() {
startup=99
fi
print_info "Creating LXC container..."
# Download Debian template if not exists
local TEMPLATE="/var/lib/vz/template/cache/debian-12-standard_12.7-1_amd64.tar.zst"
if [[ ! -f "$TEMPLATE" ]]; then
print_info "Downloading Debian 12 template..."
pveam download local debian-12-standard_12.7-1_amd64.tar.zst
# Handle OS template selection
echo
if [[ "$ADVANCED_MODE" == "true" ]]; then
echo "Available OS templates:"
# Use pveam to list templates properly
local TEMPLATES=$(pveam list local 2>/dev/null | tail -n +2 | awk '{print $1}' | sed 's/local:vztmpl\///' | nl -w2 -s') ')
if [[ -n "$TEMPLATES" ]]; then
echo "$TEMPLATES"
echo
echo "Or download a new template:"
echo " d) Download Debian 12 (recommended)"
echo " u) Download Ubuntu 22.04 LTS"
echo " a) Download Alpine Linux (minimal)"
echo
read -p "Select template number or option [Enter for Debian 12]: " template_choice < /dev/tty
if [[ -n "$template_choice" ]]; then
case "$template_choice" in
d|D)
print_info "Downloading Debian 12..."
pveam download local debian-12-standard_12.7-1_amd64.tar.zst
TEMPLATE="/var/lib/vz/template/cache/debian-12-standard_12.7-1_amd64.tar.zst"
;;
u|U)
print_info "Downloading Ubuntu 22.04..."
pveam download local ubuntu-22.04-standard_22.04-1_amd64.tar.zst
TEMPLATE="/var/lib/vz/template/cache/ubuntu-22.04-standard_22.04-1_amd64.tar.zst"
;;
a|A)
print_info "Downloading Alpine Linux..."
pveam download local alpine-3.18-default_20230607_amd64.tar.xz
TEMPLATE="/var/lib/vz/template/cache/alpine-3.18-default_20230607_amd64.tar.xz"
;;
[0-9]*)
TEMPLATE_NAME=$(pveam list local 2>/dev/null | tail -n +2 | awk '{print $1}' | sed 's/local:vztmpl\///' | sed -n "${template_choice}p")
if [[ -n "$TEMPLATE_NAME" ]]; then
TEMPLATE="/var/lib/vz/template/cache/$TEMPLATE_NAME"
print_info "Using template: $TEMPLATE_NAME"
else
TEMPLATE="/var/lib/vz/template/cache/debian-12-standard_12.7-1_amd64.tar.zst"
print_info "Invalid selection, using Debian 12"
fi
;;
*)
TEMPLATE="/var/lib/vz/template/cache/debian-12-standard_12.7-1_amd64.tar.zst"
;;
esac
else
TEMPLATE="/var/lib/vz/template/cache/debian-12-standard_12.7-1_amd64.tar.zst"
fi
else
TEMPLATE="/var/lib/vz/template/cache/debian-12-standard_12.7-1_amd64.tar.zst"
fi
else
# Quick mode - use Debian 12
TEMPLATE="/var/lib/vz/template/cache/debian-12-standard_12.7-1_amd64.tar.zst"
fi
# Download template if it doesn't exist
if [[ ! -f "$TEMPLATE" ]]; then
print_info "Template not found, downloading Debian 12..."
if ! pveam download local debian-12-standard_12.7-1_amd64.tar.zst; then
print_error "Failed to download template. Please check your internet connection and try again."
print_info "You can manually download with: pveam download local debian-12-standard_12.7-1_amd64.tar.zst"
exit 1
fi
TEMPLATE="/var/lib/vz/template/cache/debian-12-standard_12.7-1_amd64.tar.zst"
if [[ ! -f "$TEMPLATE" ]]; then
print_error "Template download succeeded but file not found at expected location"
exit 1
fi
fi
print_info "Creating container..."
# Build network configuration
if [[ -n "$static_ip" ]]; then
NET_CONFIG="name=eth0,bridge=${bridge},ip=${static_ip},firewall=${firewall}"
@@ -295,88 +410,133 @@ create_lxc_container() {
CREATE_CMD="$CREATE_CMD --nameserver $nameserver"
fi
# Execute container creation
eval $CREATE_CMD
if [[ $? -ne 0 ]]; then
# Execute container creation (suppress verbose output)
if ! eval $CREATE_CMD >/dev/null 2>&1; then
print_error "Failed to create container"
exit 1
fi
# From this point on, cleanup container if we fail
cleanup_on_error() {
print_error "Installation failed, cleaning up container $CTID..."
pct stop $CTID 2>/dev/null || true
sleep 2
pct destroy $CTID 2>/dev/null || true
exit 1
}
# Start container
print_info "Starting container..."
pct start $CTID
sleep 5
if ! pct start $CTID >/dev/null 2>&1; then
print_error "Failed to start container"
cleanup_on_error
fi
sleep 3
# Wait for network to be ready
print_info "Waiting for container network..."
for i in {1..30}; do
if pct exec $CTID -- ping -c 1 8.8.8.8 &>/dev/null; then
print_info "Waiting for network..."
local network_ready=false
for i in {1..60}; do
if pct exec $CTID -- ping -c 1 8.8.8.8 &>/dev/null 2>&1; then
network_ready=true
break
fi
sleep 1
done
if [[ "$network_ready" != "true" ]]; then
print_error "Container network failed to come up after 60 seconds"
cleanup_on_error
fi
# Install dependencies and optimize container
print_info "Installing dependencies in container..."
pct exec $CTID -- bash -c "
apt-get update && apt-get install -y curl wget ca-certificates
print_info "Installing dependencies..."
if ! pct exec $CTID -- bash -c "
apt-get update -qq >/dev/null 2>&1 && apt-get install -y -qq curl wget ca-certificates >/dev/null 2>&1
# Set timezone to UTC for consistent logging
ln -sf /usr/share/zoneinfo/UTC /etc/localtime
ln -sf /usr/share/zoneinfo/UTC /etc/localtime 2>/dev/null
# Optimize sysctl for monitoring workload
echo 'net.core.somaxconn=1024' >> /etc/sysctl.conf
echo 'net.ipv4.tcp_keepalive_time=60' >> /etc/sysctl.conf
sysctl -p
"
sysctl -p >/dev/null 2>&1
# Create convenient update script
cat > /usr/local/bin/update << 'EOF'
#!/bin/bash
echo 'Updating Pulse...'
curl -sSL https://raw.githubusercontent.com/rcourtman/Pulse/main/install.sh | bash
EOF
chmod +x /usr/local/bin/update
"; then
print_error "Failed to install dependencies in container"
cleanup_on_error
fi
# Install Pulse inside container
print_info "Installing Pulse in container..."
print_info "Installing Pulse..."
# Copy this script to container and run it
pct push $CTID $0 /tmp/install.sh
pct exec $CTID -- bash /tmp/install.sh --in-container
if [[ $? -eq 0 ]]; then
# Get container IP
local IP=$(pct exec $CTID -- hostname -I | awk '{print $1}')
print_header
print_success "Pulse installed successfully in LXC container $CTID!"
echo
echo -e "${GREEN}Access Pulse at:${NC} http://${IP}:7655"
echo
echo -e "${YELLOW}Container management:${NC}"
echo " pct enter $CTID - Enter container console"
echo " pct stop $CTID - Stop container"
echo " pct start $CTID - Start container"
echo " pct destroy $CTID - Remove container"
echo
if [[ "$ADVANCED_MODE" == "true" ]]; then
echo -e "${BLUE}Container created with your custom settings${NC}"
else
echo -e "${BLUE}Container optimizations applied:${NC}"
echo " ✓ Unprivileged container (enhanced security)"
echo " ✓ Firewall enabled on network interface"
echo " ✓ Auto-start on boot with order 99"
echo " ✓ CPU limit set to prevent resource hogging"
echo " ✓ 512MB swap for memory flexibility"
echo " ✓ UTC timezone for consistent logging"
fi
echo
else
print_error "Installation failed"
exit 1
if ! pct push $CTID $0 /tmp/install.sh >/dev/null 2>&1; then
print_error "Failed to copy install script to container"
cleanup_on_error
fi
# Run installation quietly (suppress verbose output)
local install_output=$(pct exec $CTID -- bash /tmp/install.sh --in-container 2>&1)
local install_status=$?
if [[ $install_status -ne 0 ]]; then
print_error "Failed to install Pulse inside container"
cleanup_on_error
fi
# Get container IP
local IP=$(pct exec $CTID -- hostname -I | awk '{print $1}')
# Clean final output
echo
print_success "Pulse installation complete!"
echo
echo " Web UI: http://${IP}:7655"
echo " Container: $CTID"
echo
echo " Commands:"
echo " pct enter $CTID # Enter container"
echo " pct exec $CTID -- update # Update Pulse"
echo
exit 0
}
check_existing_installation() {
if systemctl is-active --quiet $SERVICE_NAME 2>/dev/null; then
print_info "Pulse is currently running"
return 0
local CURRENT_VERSION=""
local BINARY_PATH=""
# Check for the binary in expected locations
if [[ -f "$INSTALL_DIR/bin/pulse" ]]; then
BINARY_PATH="$INSTALL_DIR/bin/pulse"
elif [[ -f "$INSTALL_DIR/pulse" ]]; then
print_info "Pulse is installed but not running"
BINARY_PATH="$INSTALL_DIR/pulse"
fi
# Try to get version if binary exists
if [[ -n "$BINARY_PATH" ]]; then
CURRENT_VERSION=$($BINARY_PATH --version 2>/dev/null | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "unknown")
fi
if systemctl is-active --quiet $SERVICE_NAME 2>/dev/null; then
if [[ -n "$CURRENT_VERSION" && "$CURRENT_VERSION" != "unknown" ]]; then
print_info "Pulse $CURRENT_VERSION is currently running"
else
print_info "Pulse is currently running"
fi
return 0
elif [[ -n "$BINARY_PATH" ]]; then
if [[ -n "$CURRENT_VERSION" && "$CURRENT_VERSION" != "unknown" ]]; then
print_info "Pulse $CURRENT_VERSION is installed but not running"
else
print_info "Pulse is installed but not running"
fi
return 0
else
return 1
@@ -611,6 +771,10 @@ print_completion() {
echo " systemctl restart $SERVICE_NAME - Restart Pulse"
echo " journalctl -u $SERVICE_NAME -f - View logs"
echo
echo -e "${YELLOW}Updating Pulse:${NC}"
echo " curl -sSL https://raw.githubusercontent.com/rcourtman/Pulse/main/install.sh | bash"
echo " Or: wget -qO- https://raw.githubusercontent.com/rcourtman/Pulse/main/install.sh | bash"
echo
}
# Main installation flow
@@ -624,15 +788,27 @@ main() {
print_header
check_root
detect_os
check_docker_environment
check_pre_v4_installation
if check_existing_installation; then
print_info "Existing Pulse installation detected"
# Get the latest available version for comparison
local AVAILABLE_VERSION=""
if [[ "$UPDATE_CHANNEL" == "rc" ]]; then
AVAILABLE_VERSION=$(curl -s https://api.github.com/repos/$GITHUB_REPO/releases | grep '"tag_name":' | head -1 | sed -E 's/.*"([^"]+)".*/\1/' 2>/dev/null)
else
AVAILABLE_VERSION=$(curl -s https://api.github.com/repos/$GITHUB_REPO/releases/latest | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' 2>/dev/null)
fi
echo
echo "What would you like to do?"
echo "1) Update to latest version"
echo "2) Reinstall"
echo "3) Remove"
if [[ -n "$AVAILABLE_VERSION" ]]; then
echo "1) Update to $AVAILABLE_VERSION"
else
echo "1) Update to latest version"
fi
echo "2) Reinstall current version"
echo "3) Remove Pulse"
echo "4) Cancel"
read -p "Select option [1-4]: " choice < /dev/tty
+8
View File
@@ -1375,6 +1375,13 @@ func (h *ConfigHandlers) getNodeStatus(nodeType, nodeName string) string {
// HandleGetSystemSettings returns current system settings
func (h *ConfigHandlers) HandleGetSystemSettings(w http.ResponseWriter, r *http.Request) {
// Load settings from persistence to get all fields including theme
persistedSettings, err := h.persistence.LoadSystemSettings()
if err != nil {
log.Warn().Err(err).Msg("Failed to load persisted system settings")
persistedSettings = &config.SystemSettings{}
}
// Get current values from running config
settings := config.SystemSettings{
PollingInterval: int(h.config.PollingInterval.Seconds()),
@@ -1388,6 +1395,7 @@ func (h *ConfigHandlers) HandleGetSystemSettings(w http.ResponseWriter, r *http.
AutoUpdateEnabled: h.config.AutoUpdateEnabled,
AutoUpdateCheckInterval: int(h.config.AutoUpdateCheckInterval.Hours()),
AutoUpdateTime: h.config.AutoUpdateTime,
Theme: persistedSettings.Theme, // Include theme from persisted settings
}
w.Header().Set("Content-Type", "application/json")
+14
View File
@@ -37,6 +37,14 @@ func (h *SystemSettingsHandler) HandleGetSystemSettings(w http.ResponseWriter, r
PollingInterval: 5,
}
}
// Log loaded settings for debugging
if settings != nil {
log.Debug().
Int("pollingInterval", settings.PollingInterval).
Str("theme", settings.Theme).
Msg("Loaded system settings for API response")
}
// Include env override information
response := struct {
@@ -94,6 +102,12 @@ func (h *SystemSettingsHandler) HandleUpdateSystemSettings(w http.ResponseWriter
if settings.AutoUpdateTime != "" {
h.config.AutoUpdateTime = settings.AutoUpdateTime
}
// Validate theme if provided
if settings.Theme != "" && settings.Theme != "light" && settings.Theme != "dark" {
http.Error(w, "Invalid theme value. Must be 'light', 'dark', or empty", http.StatusBadRequest)
return
}
// Save to persistence
if err := h.persistence.SaveSystemSettings(settings); err != nil {
+1
View File
@@ -314,6 +314,7 @@ type SystemSettings struct {
AutoUpdateTime string `json:"autoUpdateTime,omitempty"`
LogLevel string `json:"logLevel,omitempty"`
DiscoverySubnet string `json:"discoverySubnet,omitempty"`
Theme string `json:"theme,omitempty"` // User theme preference: "light", "dark", or empty for system default
// APIToken removed - now handled via .env file only
}