mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-22 11:13:26 +00:00
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:
@@ -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)**
|
||||
|
||||
@@ -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
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user