mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-11 14:00:29 +00:00
f53550fc6c
- Remove confusing "preview mode" - channels switch and save immediately - Eliminate complex localStorage channel persistence logic - Reduce update reload time from 10+ seconds to 3 seconds - Remove redundant "Switch to X & Update" buttons - Make channel switching instant with immediate feedback Users can now simply change the dropdown to switch channels instantly, then click Apply Update for a fast 3-second reload experience. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
648 lines
31 KiB
HTML
648 lines
31 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en" class="scrollbar">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Pulse - Configuration Setup</title>
|
|
<link rel="icon" href="/logo.svg" type="image/svg+xml">
|
|
<link rel="stylesheet" href="/output.css">
|
|
<style>
|
|
@keyframes pulse {
|
|
0%, 100% {
|
|
opacity: 1;
|
|
transform: scale(1);
|
|
}
|
|
50% {
|
|
opacity: 0.7;
|
|
transform: scale(1.1);
|
|
}
|
|
}
|
|
.pulse-logo-circle {
|
|
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
|
transform-origin: center;
|
|
}
|
|
.pulse-logo-outer { fill: #2563eb; }
|
|
.pulse-logo-inner { fill: #ffffff; }
|
|
.dark .pulse-logo-outer { fill: #3b82f6; }
|
|
.dark .pulse-logo-inner { fill: #dbeafe; }
|
|
</style>
|
|
</head>
|
|
<body class="bg-gray-100 dark:bg-gray-900 text-gray-800 dark:text-gray-200 min-h-screen flex items-center justify-center p-4">
|
|
<div class="max-w-2xl w-full">
|
|
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg p-8">
|
|
<!-- Header -->
|
|
<div class="text-center mb-8">
|
|
<div class="flex items-center justify-center gap-2 mb-4">
|
|
<svg width="40" height="40" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" class="pulse-logo">
|
|
<title>Pulse Logo</title>
|
|
<circle class="pulse-logo-outer" cx="50" cy="50" r="45"/>
|
|
<circle class="pulse-logo-inner pulse-logo-circle" cx="50" cy="50" r="25"/>
|
|
</svg>
|
|
<h1 class="text-3xl font-bold text-gray-800 dark:text-gray-200">Pulse Setup</h1>
|
|
</div>
|
|
<p class="text-gray-600 dark:text-gray-400">Configure your Proxmox connection to get started</p>
|
|
</div>
|
|
|
|
<!-- Configuration Form -->
|
|
<form id="config-form" class="space-y-6" autocomplete="off" onsubmit="return false;">
|
|
<!-- Primary Proxmox VE Configuration -->
|
|
<div class="border border-gray-200 dark:border-gray-700 rounded-lg p-6">
|
|
<h2 class="text-xl font-semibold mb-4 text-gray-800 dark:text-gray-200">Primary Proxmox VE Server</h2>
|
|
|
|
<div class="space-y-4">
|
|
<div>
|
|
<label for="proxmox-host" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Host Address <span class="text-red-500">*</span>
|
|
</label>
|
|
<input type="text" id="proxmox-host" name="proxmox-host" required
|
|
placeholder="192.168.1.100 or proxmox.example.com"
|
|
autocomplete="off"
|
|
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">IP address or hostname only (without port number)</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label for="proxmox-port" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Port
|
|
</label>
|
|
<input type="number" id="proxmox-port" name="proxmox-port"
|
|
placeholder="8006 (default)"
|
|
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Default Proxmox VE web interface port</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label for="proxmox-token-id" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
API Token ID <span class="text-red-500">*</span>
|
|
</label>
|
|
<input type="text" id="proxmox-token-id" name="proxmox-token-id" required
|
|
placeholder="user@pam!token-name"
|
|
autocomplete="off"
|
|
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Format: username@realm!token-name</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label for="proxmox-token-secret" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
API Token Secret <span class="text-red-500">*</span>
|
|
</label>
|
|
<div class="relative">
|
|
<input type="password" id="proxmox-token-secret" name="proxmox-token-secret" required
|
|
placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
|
autocomplete="new-password"
|
|
class="w-full px-3 py-2 pr-10 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
|
<button type="button" onclick="togglePasswordVisibility('proxmox-token-secret')"
|
|
class="absolute right-2 top-1/2 transform -translate-y-1/2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Advanced Settings (Optional) -->
|
|
<details class="border border-gray-200 dark:border-gray-700 rounded-lg">
|
|
<summary class="px-6 py-4 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors">
|
|
<span class="text-lg font-medium text-gray-800 dark:text-gray-200">Advanced Settings (Optional)</span>
|
|
</summary>
|
|
<div class="p-6 pt-0 space-y-4">
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<label for="metric-interval" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Metric Update Interval (ms)
|
|
</label>
|
|
<input type="number" id="metric-interval" name="metric-interval"
|
|
placeholder="2000 (default)"
|
|
min="1000" max="60000"
|
|
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">How often to fetch VM/Container metrics</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label for="discovery-interval" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Discovery Interval (ms)
|
|
</label>
|
|
<input type="number" id="discovery-interval" name="discovery-interval"
|
|
placeholder="30000 (default)"
|
|
min="5000" max="300000"
|
|
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">How often to discover nodes and VMs</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Alert Settings -->
|
|
<div class="border-t border-gray-200 dark:border-gray-700 pt-4">
|
|
<h3 class="text-md font-medium text-gray-800 dark:text-gray-200 mb-3">Alert Settings</h3>
|
|
|
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4">
|
|
<label class="flex items-center">
|
|
<input type="checkbox" id="alert-cpu-enabled" name="alert-cpu-enabled" checked
|
|
class="mr-2 rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500">
|
|
<span class="text-sm text-gray-700 dark:text-gray-300">CPU Alerts</span>
|
|
</label>
|
|
<label class="flex items-center">
|
|
<input type="checkbox" id="alert-memory-enabled" name="alert-memory-enabled" checked
|
|
class="mr-2 rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500">
|
|
<span class="text-sm text-gray-700 dark:text-gray-300">Memory Alerts</span>
|
|
</label>
|
|
<label class="flex items-center">
|
|
<input type="checkbox" id="alert-disk-enabled" name="alert-disk-enabled" checked
|
|
class="mr-2 rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500">
|
|
<span class="text-sm text-gray-700 dark:text-gray-300">Disk Alerts</span>
|
|
</label>
|
|
<label class="flex items-center">
|
|
<input type="checkbox" id="alert-down-enabled" name="alert-down-enabled" checked
|
|
class="mr-2 rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500">
|
|
<span class="text-sm text-gray-700 dark:text-gray-300">Down Alerts</span>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<div>
|
|
<label for="alert-cpu-threshold" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
CPU Alert Threshold (%)
|
|
</label>
|
|
<input type="number" id="alert-cpu-threshold" name="alert-cpu-threshold"
|
|
placeholder="85 (default)"
|
|
min="50" max="100"
|
|
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
|
</div>
|
|
<div>
|
|
<label for="alert-memory-threshold" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Memory Alert Threshold (%)
|
|
</label>
|
|
<input type="number" id="alert-memory-threshold" name="alert-memory-threshold"
|
|
placeholder="90 (default)"
|
|
min="50" max="100"
|
|
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
|
</div>
|
|
<div>
|
|
<label for="alert-disk-threshold" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Disk Alert Threshold (%)
|
|
</label>
|
|
<input type="number" id="alert-disk-threshold" name="alert-disk-threshold"
|
|
placeholder="95 (default)"
|
|
min="50" max="100"
|
|
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</details>
|
|
|
|
<!-- PBS Configuration (Optional) -->
|
|
<div class="border border-gray-200 dark:border-gray-700 rounded-lg p-6">
|
|
<h2 class="text-xl font-semibold mb-4 text-gray-800 dark:text-gray-200">Proxmox Backup Server (Optional)</h2>
|
|
|
|
<div class="space-y-4">
|
|
<div>
|
|
<label for="pbs-host" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Host Address
|
|
</label>
|
|
<input type="text" id="pbs-host" name="pbs-host"
|
|
placeholder="192.168.1.101 or pbs.example.com"
|
|
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">IP address or hostname only (without port number)</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label for="pbs-port" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
Port
|
|
</label>
|
|
<input type="number" id="pbs-port" name="pbs-port"
|
|
placeholder="8007 (default)"
|
|
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Default Proxmox Backup Server web interface port</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label for="pbs-node-name" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
PBS Node Name
|
|
</label>
|
|
<input type="text" id="pbs-node-name" name="pbs-node-name"
|
|
placeholder="Internal hostname (run 'hostname' on PBS)"
|
|
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
|
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Required unless token has Sys.Audit permission</p>
|
|
</div>
|
|
|
|
<div>
|
|
<label for="pbs-token-id" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
API Token ID
|
|
</label>
|
|
<input type="text" id="pbs-token-id" name="pbs-token-id"
|
|
placeholder="user@pbs!token-name"
|
|
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
|
</div>
|
|
|
|
<div>
|
|
<label for="pbs-token-secret" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
API Token Secret
|
|
</label>
|
|
<div class="relative">
|
|
<input type="password" id="pbs-token-secret" name="pbs-token-secret"
|
|
placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
|
autocomplete="new-password"
|
|
class="w-full px-3 py-2 pr-10 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
|
<button type="button" onclick="togglePasswordVisibility('pbs-token-secret')"
|
|
class="absolute right-2 top-1/2 transform -translate-y-1/2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Error Display -->
|
|
<div id="error-message" class="hidden bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4">
|
|
<div class="flex">
|
|
<div class="flex-shrink-0">
|
|
<svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
|
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
|
|
</svg>
|
|
</div>
|
|
<div class="ml-3">
|
|
<h3 class="text-sm font-medium text-red-800 dark:text-red-200">Configuration Error</h3>
|
|
<div class="mt-2 text-sm text-red-700 dark:text-red-300" id="error-text"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Success Display -->
|
|
<div id="success-message" class="hidden bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg p-4">
|
|
<div class="flex">
|
|
<div class="flex-shrink-0">
|
|
<svg id="success-icon" class="h-5 w-5 text-green-400" viewBox="0 0 20 20" fill="currentColor">
|
|
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
|
|
</svg>
|
|
<svg id="loading-icon" class="hidden animate-spin h-5 w-5 text-green-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
</svg>
|
|
</div>
|
|
<div class="ml-3">
|
|
<h3 class="text-sm font-medium text-green-800 dark:text-green-200">Success!</h3>
|
|
<div class="mt-2 text-sm text-green-700 dark:text-green-300" id="success-text">Configuration saved. Redirecting to dashboard...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Action Buttons -->
|
|
<div class="flex gap-4">
|
|
<button type="submit" id="save-button"
|
|
class="flex-1 bg-blue-600 hover:bg-blue-700 text-white font-medium py-2 px-4 rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800">
|
|
Save Configuration
|
|
</button>
|
|
<button type="button" onclick="testConnection(event)"
|
|
class="flex-1 bg-gray-600 hover:bg-gray-700 text-white font-medium py-2 px-4 rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-gray-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800">
|
|
Test Connection
|
|
</button>
|
|
</div>
|
|
</form>
|
|
|
|
<!-- Help Text -->
|
|
<div class="mt-8 text-center">
|
|
<p class="text-sm text-gray-600 dark:text-gray-400">
|
|
Need help? Check out the
|
|
<a href="https://github.com/Daemonslayer2048/pulse#configuration" target="_blank"
|
|
class="text-blue-600 dark:text-blue-400 hover:underline">configuration guide</a>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Initialize PulseApp if it doesn't exist
|
|
window.PulseApp = window.PulseApp || {};
|
|
</script>
|
|
<script src="/js/theme.js"></script>
|
|
<script>
|
|
// Initialize theme
|
|
if (PulseApp.theme && PulseApp.theme.init) {
|
|
PulseApp.theme.init();
|
|
}
|
|
|
|
function togglePasswordVisibility(inputId) {
|
|
const input = document.getElementById(inputId);
|
|
input.type = input.type === 'password' ? 'text' : 'password';
|
|
}
|
|
|
|
function showError(message) {
|
|
const errorDiv = document.getElementById('error-message');
|
|
const errorText = document.getElementById('error-text');
|
|
const successDiv = document.getElementById('success-message');
|
|
|
|
errorText.textContent = message;
|
|
errorDiv.classList.remove('hidden');
|
|
successDiv.classList.add('hidden');
|
|
}
|
|
|
|
function showSuccess(message = 'Configuration saved. Redirecting to dashboard...', showButton = false, showLoading = false) {
|
|
const errorDiv = document.getElementById('error-message');
|
|
const successDiv = document.getElementById('success-message');
|
|
const successText = document.getElementById('success-text');
|
|
const successIcon = document.getElementById('success-icon');
|
|
const loadingIcon = document.getElementById('loading-icon');
|
|
|
|
// Toggle icons
|
|
if (showLoading) {
|
|
successIcon.classList.add('hidden');
|
|
loadingIcon.classList.remove('hidden');
|
|
} else {
|
|
successIcon.classList.remove('hidden');
|
|
loadingIcon.classList.add('hidden');
|
|
}
|
|
|
|
if (showButton) {
|
|
successText.innerHTML = message + '<br><button onclick="window.location.href=\'/\'" class="mt-3 bg-green-600 hover:bg-green-700 text-white font-medium py-2 px-4 rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800">Continue to Pulse</button>';
|
|
} else {
|
|
successText.textContent = message;
|
|
}
|
|
errorDiv.classList.add('hidden');
|
|
successDiv.classList.remove('hidden');
|
|
}
|
|
|
|
function hideMessages() {
|
|
document.getElementById('error-message').classList.add('hidden');
|
|
document.getElementById('success-message').classList.add('hidden');
|
|
}
|
|
|
|
async function testConnection(event) {
|
|
if (event) event.preventDefault();
|
|
const formData = new FormData(document.getElementById('config-form'));
|
|
const config = {
|
|
proxmox: {
|
|
host: formData.get('proxmox-host'),
|
|
port: formData.get('proxmox-port') || '8006',
|
|
tokenId: formData.get('proxmox-token-id'),
|
|
tokenSecret: formData.get('proxmox-token-secret')
|
|
}
|
|
};
|
|
|
|
if (!config.proxmox.host || !config.proxmox.tokenId || !config.proxmox.tokenSecret) {
|
|
showError('Please fill in all required Proxmox fields');
|
|
return;
|
|
}
|
|
|
|
hideMessages();
|
|
const button = event && event.target ? event.target : document.querySelector('button[onclick*="testConnection"]');
|
|
button.disabled = true;
|
|
button.textContent = 'Testing...';
|
|
|
|
try {
|
|
const response = await fetch('/api/config/test', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(config)
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (response.ok && result.success) {
|
|
showSuccess('Connection test successful!', false, false);
|
|
setTimeout(() => hideMessages(), 3000);
|
|
} else {
|
|
showError(result.error || 'Connection test failed');
|
|
}
|
|
} catch (error) {
|
|
showError('Failed to test connection: ' + error.message);
|
|
} finally {
|
|
button.disabled = false;
|
|
button.textContent = 'Test Connection';
|
|
}
|
|
}
|
|
|
|
// Ensure DOM is loaded before attaching event handlers
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', setupFormHandlers);
|
|
} else {
|
|
setupFormHandlers();
|
|
}
|
|
|
|
function setupFormHandlers() {
|
|
console.log('Setting up form handlers...');
|
|
|
|
const form = document.getElementById('config-form');
|
|
if (!form) {
|
|
console.error('Config form not found!');
|
|
return;
|
|
}
|
|
|
|
form.addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
console.log('Form submit event triggered');
|
|
|
|
const formData = new FormData(e.target);
|
|
const config = {
|
|
proxmox: {
|
|
host: formData.get('proxmox-host'),
|
|
port: formData.get('proxmox-port') || '8006',
|
|
tokenId: formData.get('proxmox-token-id'),
|
|
tokenSecret: formData.get('proxmox-token-secret')
|
|
}
|
|
};
|
|
|
|
// Validate required fields
|
|
if (!config.proxmox.host || !config.proxmox.tokenId || !config.proxmox.tokenSecret) {
|
|
showError('Please fill in all required Proxmox fields');
|
|
return;
|
|
}
|
|
|
|
// Add PBS config if provided
|
|
if (formData.get('pbs-host')) {
|
|
config.pbs = {
|
|
host: formData.get('pbs-host'),
|
|
port: formData.get('pbs-port') || '8007',
|
|
tokenId: formData.get('pbs-token-id'),
|
|
tokenSecret: formData.get('pbs-token-secret'),
|
|
nodeName: formData.get('pbs-node-name')
|
|
};
|
|
}
|
|
|
|
// Add advanced settings
|
|
config.advanced = {
|
|
metricInterval: formData.get('metric-interval'),
|
|
discoveryInterval: formData.get('discovery-interval'),
|
|
alerts: {
|
|
cpu: {
|
|
enabled: formData.get('alert-cpu-enabled') === 'on',
|
|
threshold: formData.get('alert-cpu-threshold')
|
|
},
|
|
memory: {
|
|
enabled: formData.get('alert-memory-enabled') === 'on',
|
|
threshold: formData.get('alert-memory-threshold')
|
|
},
|
|
disk: {
|
|
enabled: formData.get('alert-disk-enabled') === 'on',
|
|
threshold: formData.get('alert-disk-threshold')
|
|
},
|
|
down: {
|
|
enabled: formData.get('alert-down-enabled') === 'on'
|
|
}
|
|
}
|
|
};
|
|
|
|
hideMessages();
|
|
const button = document.getElementById('save-button');
|
|
button.disabled = true;
|
|
button.textContent = 'Saving...';
|
|
|
|
// Debug: Log what we're sending
|
|
console.log('Saving configuration:', JSON.stringify(config, null, 2));
|
|
console.log('Making POST request to /api/config...');
|
|
|
|
try {
|
|
const response = await fetch('/api/config', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(config)
|
|
});
|
|
|
|
console.log('Save response status:', response.status, 'ok:', response.ok);
|
|
const result = await response.json();
|
|
console.log('Save result:', result);
|
|
|
|
if (response.ok && result.success) {
|
|
showSuccess('Configuration saved! Applying settings...', false, true);
|
|
|
|
// Wait a moment for the server to start reloading
|
|
setTimeout(() => {
|
|
checkServerReady();
|
|
}, 2000);
|
|
} else {
|
|
showError(result.error || 'Failed to save configuration');
|
|
button.disabled = false;
|
|
button.textContent = 'Save Configuration';
|
|
}
|
|
} catch (error) {
|
|
console.error('Save configuration error:', error);
|
|
showError('Failed to save configuration: ' + error.message);
|
|
button.disabled = false;
|
|
button.textContent = 'Save Configuration';
|
|
}
|
|
});
|
|
|
|
// Load config on page load
|
|
loadExistingConfig();
|
|
}
|
|
|
|
// Check if server is ready after configuration save
|
|
async function checkServerReady(attempts = 0) {
|
|
const maxAttempts = 15; // 30 seconds total (15 * 2 seconds)
|
|
|
|
try {
|
|
const response = await fetch('/api/health');
|
|
|
|
if (response.ok) {
|
|
const health = await response.json();
|
|
console.log('Health check response:', health);
|
|
|
|
// Check if the server has successfully loaded configuration
|
|
// Look for overall health and recent activity
|
|
if (health.overall === 'healthy' && health.lastUpdate && health.connections && health.connections.total > 0) {
|
|
showSuccess('Configuration applied successfully!', true, false);
|
|
|
|
// Re-enable the save button
|
|
const button = document.getElementById('save-button');
|
|
button.disabled = false;
|
|
button.textContent = 'Save Configuration';
|
|
return;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.log('Health check failed:', error);
|
|
}
|
|
|
|
// If we haven't exceeded max attempts, try again
|
|
if (attempts < maxAttempts) {
|
|
const messages = [
|
|
'Applying configuration...',
|
|
'Initializing connections...',
|
|
'Connecting to Proxmox servers...',
|
|
'Verifying credentials...',
|
|
'Loading server data...'
|
|
];
|
|
const messageIndex = Math.min(Math.floor(attempts / 3), messages.length - 1);
|
|
showSuccess(`${messages[messageIndex]} (${Math.floor((attempts / maxAttempts) * 100)}%)`, false, true);
|
|
|
|
setTimeout(() => {
|
|
checkServerReady(attempts + 1);
|
|
}, 2000);
|
|
} else {
|
|
// After 30 seconds, show the button anyway
|
|
showSuccess('Configuration saved! The server is taking longer than expected to initialize.', true, false);
|
|
|
|
// Re-enable the save button
|
|
const button = document.getElementById('save-button');
|
|
button.disabled = false;
|
|
button.textContent = 'Save Configuration';
|
|
}
|
|
}
|
|
|
|
// Load existing configuration if available
|
|
async function loadExistingConfig() {
|
|
try {
|
|
const response = await fetch('/api/config');
|
|
if (response.ok) {
|
|
const config = await response.json();
|
|
|
|
if (config.proxmox) {
|
|
document.getElementById('proxmox-host').value = config.proxmox.host || '';
|
|
document.getElementById('proxmox-port').value = config.proxmox.port || '';
|
|
document.getElementById('proxmox-token-id').value = config.proxmox.tokenId || '';
|
|
}
|
|
|
|
if (config.pbs) {
|
|
document.getElementById('pbs-host').value = config.pbs.host || '';
|
|
document.getElementById('pbs-port').value = config.pbs.port || '';
|
|
document.getElementById('pbs-token-id').value = config.pbs.tokenId || '';
|
|
document.getElementById('pbs-node-name').value = config.pbs.nodeName || '';
|
|
}
|
|
|
|
if (config.advanced) {
|
|
if (config.advanced.metricInterval) {
|
|
document.getElementById('metric-interval').value = config.advanced.metricInterval;
|
|
}
|
|
if (config.advanced.discoveryInterval) {
|
|
document.getElementById('discovery-interval').value = config.advanced.discoveryInterval;
|
|
}
|
|
|
|
// Load alert settings
|
|
if (config.advanced.alerts) {
|
|
const alerts = config.advanced.alerts;
|
|
if (alerts.cpu !== undefined) {
|
|
document.getElementById('alert-cpu-enabled').checked = alerts.cpu.enabled !== false;
|
|
if (alerts.cpu.threshold) {
|
|
document.getElementById('alert-cpu-threshold').value = alerts.cpu.threshold;
|
|
}
|
|
}
|
|
if (alerts.memory !== undefined) {
|
|
document.getElementById('alert-memory-enabled').checked = alerts.memory.enabled !== false;
|
|
if (alerts.memory.threshold) {
|
|
document.getElementById('alert-memory-threshold').value = alerts.memory.threshold;
|
|
}
|
|
}
|
|
if (alerts.disk !== undefined) {
|
|
document.getElementById('alert-disk-enabled').checked = alerts.disk.enabled !== false;
|
|
if (alerts.disk.threshold) {
|
|
document.getElementById('alert-disk-threshold').value = alerts.disk.threshold;
|
|
}
|
|
}
|
|
if (alerts.down !== undefined) {
|
|
document.getElementById('alert-down-enabled').checked = alerts.down.enabled !== false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to load existing configuration:', error);
|
|
}
|
|
}
|
|
</script>
|
|
</body>
|
|
</html> |