mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-03 12:42:43 +00:00
feat: redesign settings as comprehensive modal-based configuration system
- Replace settings tab with settings icon in top-right header - Implement comprehensive modal interface for ALL .env variables - Add support for multiple PVE and PBS endpoints with dynamic forms - Include all service settings (intervals) and alert configuration - Support both old structured format (setup) and new raw .env format - Add connection testing and form validation - Enable complete web-based configuration without SSH access 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+112
-64
@@ -21,6 +21,7 @@ class ConfigApi {
|
||||
port: config.PROXMOX_PORT || '8006',
|
||||
tokenId: config.PROXMOX_TOKEN_ID,
|
||||
nodeName: config.PROXMOX_NODE_NAME,
|
||||
enabled: config.PROXMOX_ENABLED !== 'false',
|
||||
// Don't send the secret
|
||||
} : null,
|
||||
pbs: config.PBS_HOST ? {
|
||||
@@ -70,71 +71,13 @@ class ConfigApi {
|
||||
const existingConfig = await this.readEnvFile();
|
||||
console.log('[ConfigApi.saveConfig] Existing config keys:', Object.keys(existingConfig));
|
||||
|
||||
// Update with new values
|
||||
if (config.proxmox) {
|
||||
console.log('[ConfigApi.saveConfig] Updating Proxmox config');
|
||||
existingConfig.PROXMOX_HOST = config.proxmox.host;
|
||||
existingConfig.PROXMOX_PORT = config.proxmox.port || '8006';
|
||||
existingConfig.PROXMOX_TOKEN_ID = config.proxmox.tokenId;
|
||||
|
||||
// Only update token secret if provided (allows keeping existing secret)
|
||||
if (config.proxmox.tokenSecret) {
|
||||
existingConfig.PROXMOX_TOKEN_SECRET = config.proxmox.tokenSecret;
|
||||
}
|
||||
|
||||
// Always allow self-signed certificates by default for Proxmox
|
||||
existingConfig.PROXMOX_ALLOW_SELF_SIGNED_CERT = 'true';
|
||||
// Handle both old structured format and new raw .env variable format
|
||||
if (config.proxmox || config.pbs || config.advanced) {
|
||||
// Old structured format
|
||||
this.handleStructuredConfig(config, existingConfig);
|
||||
} else {
|
||||
console.log('[ConfigApi.saveConfig] No Proxmox config provided');
|
||||
}
|
||||
|
||||
if (config.pbs) {
|
||||
existingConfig.PBS_HOST = config.pbs.host;
|
||||
existingConfig.PBS_PORT = config.pbs.port || '8007';
|
||||
existingConfig.PBS_TOKEN_ID = config.pbs.tokenId;
|
||||
existingConfig.PBS_TOKEN_SECRET = config.pbs.tokenSecret;
|
||||
if (config.pbs.nodeName) {
|
||||
existingConfig.PBS_NODE_NAME = config.pbs.nodeName;
|
||||
}
|
||||
// Always allow self-signed certificates by default for PBS
|
||||
existingConfig.PBS_ALLOW_SELF_SIGNED_CERT = 'true';
|
||||
}
|
||||
|
||||
// Add advanced settings
|
||||
if (config.advanced) {
|
||||
// Service intervals
|
||||
if (config.advanced.metricInterval) {
|
||||
existingConfig.PULSE_METRIC_INTERVAL_MS = config.advanced.metricInterval;
|
||||
}
|
||||
if (config.advanced.discoveryInterval) {
|
||||
existingConfig.PULSE_DISCOVERY_INTERVAL_MS = config.advanced.discoveryInterval;
|
||||
}
|
||||
|
||||
// Alert settings
|
||||
if (config.advanced.alerts) {
|
||||
const alerts = config.advanced.alerts;
|
||||
if (alerts.cpu) {
|
||||
existingConfig.ALERT_CPU_ENABLED = alerts.cpu.enabled ? 'true' : 'false';
|
||||
if (alerts.cpu.threshold) {
|
||||
existingConfig.ALERT_CPU_THRESHOLD = alerts.cpu.threshold;
|
||||
}
|
||||
}
|
||||
if (alerts.memory) {
|
||||
existingConfig.ALERT_MEMORY_ENABLED = alerts.memory.enabled ? 'true' : 'false';
|
||||
if (alerts.memory.threshold) {
|
||||
existingConfig.ALERT_MEMORY_THRESHOLD = alerts.memory.threshold;
|
||||
}
|
||||
}
|
||||
if (alerts.disk) {
|
||||
existingConfig.ALERT_DISK_ENABLED = alerts.disk.enabled ? 'true' : 'false';
|
||||
if (alerts.disk.threshold) {
|
||||
existingConfig.ALERT_DISK_THRESHOLD = alerts.disk.threshold;
|
||||
}
|
||||
}
|
||||
if (alerts.down) {
|
||||
existingConfig.ALERT_DOWN_ENABLED = alerts.down.enabled ? 'true' : 'false';
|
||||
}
|
||||
}
|
||||
// New raw .env variable format from settings form
|
||||
this.handleRawEnvConfig(config, existingConfig);
|
||||
}
|
||||
|
||||
// Write back to .env file
|
||||
@@ -154,6 +97,111 @@ class ConfigApi {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle structured configuration format (old setup flow)
|
||||
*/
|
||||
handleStructuredConfig(config, existingConfig) {
|
||||
// Update with new values
|
||||
if (config.proxmox) {
|
||||
console.log('[ConfigApi.saveConfig] Updating Proxmox config');
|
||||
existingConfig.PROXMOX_HOST = config.proxmox.host;
|
||||
existingConfig.PROXMOX_PORT = config.proxmox.port || '8006';
|
||||
existingConfig.PROXMOX_TOKEN_ID = config.proxmox.tokenId;
|
||||
|
||||
// Only update token secret if provided (allows keeping existing secret)
|
||||
if (config.proxmox.tokenSecret) {
|
||||
existingConfig.PROXMOX_TOKEN_SECRET = config.proxmox.tokenSecret;
|
||||
}
|
||||
|
||||
// Always allow self-signed certificates by default for Proxmox
|
||||
existingConfig.PROXMOX_ALLOW_SELF_SIGNED_CERT = 'true';
|
||||
} else {
|
||||
console.log('[ConfigApi.saveConfig] No Proxmox config provided');
|
||||
}
|
||||
|
||||
if (config.pbs) {
|
||||
existingConfig.PBS_HOST = config.pbs.host;
|
||||
existingConfig.PBS_PORT = config.pbs.port || '8007';
|
||||
existingConfig.PBS_TOKEN_ID = config.pbs.tokenId;
|
||||
existingConfig.PBS_TOKEN_SECRET = config.pbs.tokenSecret;
|
||||
if (config.pbs.nodeName) {
|
||||
existingConfig.PBS_NODE_NAME = config.pbs.nodeName;
|
||||
}
|
||||
// Always allow self-signed certificates by default for PBS
|
||||
existingConfig.PBS_ALLOW_SELF_SIGNED_CERT = 'true';
|
||||
}
|
||||
|
||||
// Add advanced settings
|
||||
if (config.advanced) {
|
||||
// Service intervals
|
||||
if (config.advanced.metricInterval) {
|
||||
existingConfig.PULSE_METRIC_INTERVAL_MS = config.advanced.metricInterval;
|
||||
}
|
||||
if (config.advanced.discoveryInterval) {
|
||||
existingConfig.PULSE_DISCOVERY_INTERVAL_MS = config.advanced.discoveryInterval;
|
||||
}
|
||||
|
||||
// Alert settings
|
||||
if (config.advanced.alerts) {
|
||||
const alerts = config.advanced.alerts;
|
||||
if (alerts.cpu) {
|
||||
existingConfig.ALERT_CPU_ENABLED = alerts.cpu.enabled ? 'true' : 'false';
|
||||
if (alerts.cpu.threshold) {
|
||||
existingConfig.ALERT_CPU_THRESHOLD = alerts.cpu.threshold;
|
||||
}
|
||||
}
|
||||
if (alerts.memory) {
|
||||
existingConfig.ALERT_MEMORY_ENABLED = alerts.memory.enabled ? 'true' : 'false';
|
||||
if (alerts.memory.threshold) {
|
||||
existingConfig.ALERT_MEMORY_THRESHOLD = alerts.memory.threshold;
|
||||
}
|
||||
}
|
||||
if (alerts.disk) {
|
||||
existingConfig.ALERT_DISK_ENABLED = alerts.disk.enabled ? 'true' : 'false';
|
||||
if (alerts.disk.threshold) {
|
||||
existingConfig.ALERT_DISK_THRESHOLD = alerts.disk.threshold;
|
||||
}
|
||||
}
|
||||
if (alerts.down) {
|
||||
existingConfig.ALERT_DOWN_ENABLED = alerts.down.enabled ? 'true' : 'false';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle raw .env variable format (new settings form)
|
||||
*/
|
||||
handleRawEnvConfig(config, existingConfig) {
|
||||
console.log('[ConfigApi.saveConfig] Processing raw .env variable format');
|
||||
|
||||
// Directly update existing config with new values
|
||||
Object.entries(config).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== '') {
|
||||
console.log(`[ConfigApi.saveConfig] Setting ${key} = ${value}`);
|
||||
existingConfig[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
// Set default self-signed cert allowance for any Proxmox/PBS endpoints
|
||||
Object.keys(existingConfig).forEach(key => {
|
||||
if (key.startsWith('PROXMOX_HOST') && existingConfig[key]) {
|
||||
const suffix = key.replace('PROXMOX_HOST', '');
|
||||
const certKey = `PROXMOX_ALLOW_SELF_SIGNED_CERT${suffix}`;
|
||||
if (!existingConfig[certKey]) {
|
||||
existingConfig[certKey] = 'true';
|
||||
}
|
||||
}
|
||||
if (key.startsWith('PBS_HOST') && existingConfig[key]) {
|
||||
const suffix = key.replace('PBS_HOST', '');
|
||||
const certKey = `PBS_ALLOW_SELF_SIGNED_CERT${suffix}`;
|
||||
if (!existingConfig[certKey]) {
|
||||
existingConfig[certKey] = 'true';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Test configuration by attempting to connect
|
||||
*/
|
||||
|
||||
+34
-107
@@ -461,6 +461,11 @@
|
||||
</svg>
|
||||
<span id="diagnostics-badge" class="absolute -top-1 -right-1 h-2 w-2 bg-red-500 rounded-full hidden animate-pulse"></span>
|
||||
</a>
|
||||
<button id="settings-button" type="button" class="p-1 h-11 w-11 sm:h-auto sm:w-auto rounded-md text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 focus:outline-none" title="Settings" aria-label="Open settings">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"></path><circle cx="12" cy="12" r="3"></circle>
|
||||
</svg>
|
||||
</button>
|
||||
<button id="theme-toggle-button" type="button" class="p-1 h-11 w-11 sm:h-auto sm:w-auto rounded-md text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 focus:outline-none" aria-label="Toggle theme">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 block dark:hidden" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
@@ -489,10 +494,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="inline-block align-middle"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path></svg>
|
||||
<span class="hidden md:inline">PBS</span>
|
||||
</div>
|
||||
<div class="tab px-3 py-1.5 cursor-pointer bg-gray-100 dark:bg-gray-700/50 border-transparent text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-t flex items-center gap-1.5" data-tab="settings" role="tab" aria-selected="false" aria-controls="settings" tabindex="-1">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="inline-block align-middle"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"></path><circle cx="12" cy="12" r="3"></circle></svg>
|
||||
<span class="hidden md:inline">Settings</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="main" class="tab-content block bg-white dark:bg-gray-800 rounded-b rounded-tr shadow mb-2">
|
||||
<!-- Nested Tab Content START -->
|
||||
@@ -864,113 +865,39 @@
|
||||
</div>
|
||||
<span id="backups-status-text" class="text-xs text-gray-500 dark:text-gray-400">Loading backup status...</span>
|
||||
</div>
|
||||
<div id="settings" class="tab-content hidden bg-white dark:bg-gray-800 rounded-b rounded-tr shadow mb-2">
|
||||
<div class="p-3">
|
||||
<h2 class="text-lg font-semibold text-gray-700 dark:text-gray-300 mb-4">Settings</h2>
|
||||
<div id="settings-content">
|
||||
<!-- Configuration Form -->
|
||||
<form id="settings-config-form" class="space-y-4 max-w-4xl mx-auto">
|
||||
<!-- Primary Proxmox VE Configuration -->
|
||||
<div class="border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
||||
<h3 class="text-md font-semibold mb-3 text-gray-800 dark:text-gray-200">Primary Proxmox VE Server</h3>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label for="settings-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="settings-proxmox-host" name="proxmox-host" required
|
||||
class="w-full px-3 py-2 text-sm 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="settings-proxmox-port" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Port
|
||||
</label>
|
||||
<input type="number" id="settings-proxmox-port" name="proxmox-port"
|
||||
placeholder="8006"
|
||||
class="w-full px-3 py-2 text-sm 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="settings-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="settings-proxmox-token-id" name="proxmox-token-id" required
|
||||
class="w-full px-3 py-2 text-sm 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="settings-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="settings-proxmox-token-secret" name="proxmox-token-secret" required
|
||||
autocomplete="new-password"
|
||||
class="w-full px-3 py-2 pr-10 text-sm 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="PulseApp.ui.settings.togglePasswordVisibility('settings-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>
|
||||
|
||||
<!-- Message Display -->
|
||||
<div id="settings-error-message" class="hidden bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-3">
|
||||
<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">
|
||||
<p class="text-sm text-red-700 dark:text-red-300" id="settings-error-text"></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="settings-success-message" class="hidden bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg p-3">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg 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>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-green-700 dark:text-green-300" id="settings-success-text">Settings saved successfully!</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex gap-3 justify-end">
|
||||
<button type="button" onclick="PulseApp.ui.settings.testConnection()"
|
||||
class="px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white text-sm font-medium 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>
|
||||
<button type="submit" id="settings-save-button"
|
||||
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium 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>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="mt-6 p-4 bg-gray-50 dark:bg-gray-900 rounded-lg">
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
<strong>Note:</strong> Changes will be applied immediately. The server will automatically reload the configuration.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="custom-tooltip" class="custom-tooltip absolute hidden z-50 px-2 py-1 text-xs text-white bg-gray-900/90 rounded shadow-lg pointer-events-none transition-opacity duration-100"></div>
|
||||
<div id="slider-value-tooltip" class="absolute hidden z-[60] px-1.5 py-0.5 text-xs text-white bg-blue-600/90 dark:bg-blue-500/90 rounded shadow-lg pointer-events-none"></div>
|
||||
<!-- Settings Modal -->
|
||||
<div id="settings-modal" class="fixed inset-0 z-50 hidden items-center justify-center bg-black bg-opacity-50">
|
||||
<div class="modal-content bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-6xl max-h-[90vh] flex flex-col m-4">
|
||||
<div class="modal-header flex justify-between items-center border-b border-gray-300 dark:border-gray-700 p-6">
|
||||
<h2 class="text-xl font-semibold text-gray-900 dark:text-gray-100">Configuration Settings</h2>
|
||||
<button id="settings-modal-close" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div id="settings-modal-body" class="overflow-y-auto flex-grow p-6 scrollbar">
|
||||
<p class="text-gray-500 dark:text-gray-400">Loading configuration...</p>
|
||||
</div>
|
||||
<div class="modal-footer border-t border-gray-300 dark:border-gray-700 p-6">
|
||||
<div class="flex gap-3 justify-end">
|
||||
<button type="button" id="settings-cancel-button" class="px-4 py-2 bg-gray-500 hover:bg-gray-600 text-white text-sm font-medium rounded-md transition-colors">
|
||||
Cancel
|
||||
</button>
|
||||
<button type="button" id="settings-test-button" class="px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white text-sm font-medium rounded-md transition-colors">
|
||||
Test Connections
|
||||
</button>
|
||||
<button type="button" id="settings-save-button" class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-md transition-colors">
|
||||
Save Configuration
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="custom-tooltip" class="custom-tooltip absolute hidden z-50 px-2 py-1 text-xs text-white bg-gray-900/90 rounded shadow-lg pointer-events-none transition-opacity duration-100"></div>
|
||||
<div id="slider-value-tooltip" class="absolute hidden z-[60] px-1.5 py-0.5 text-xs text-white bg-blue-600/90 dark:bg-blue-500/90 rounded shadow-lg pointer-events-none"></div>
|
||||
|
||||
<div id="snapshot-modal" class="fixed inset-0 z-50 hidden items-center justify-center bg-black bg-opacity-50"><div class="modal-content bg-white dark:bg-gray-800 rounded-lg shadow-xl p-6 w-full max-w-3xl max-h-[80vh] flex flex-col"><div class="modal-header flex justify-between items-center border-b border-gray-300 dark:border-gray-700 pb-3 mb-4"><h3 id="snapshot-modal-title" class="text-lg font-semibold text-gray-900 dark:text-gray-100">Snapshot Details</h3>
|
||||
<button id="snapshot-modal-close" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"><svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" /></svg></button>
|
||||
</div>
|
||||
|
||||
+501
-133
@@ -1,6 +1,7 @@
|
||||
PulseApp.ui = PulseApp.ui || {};
|
||||
|
||||
PulseApp.ui.settings = (() => {
|
||||
let currentConfig = {};
|
||||
let isInitialized = false;
|
||||
|
||||
function init() {
|
||||
@@ -8,19 +9,84 @@ PulseApp.ui.settings = (() => {
|
||||
|
||||
console.log('[Settings] Initializing settings module...');
|
||||
|
||||
// Set up form submission handler
|
||||
const form = document.getElementById('settings-config-form');
|
||||
if (form) {
|
||||
form.addEventListener('submit', handleFormSubmission);
|
||||
// Set up modal event listeners
|
||||
const settingsButton = document.getElementById('settings-button');
|
||||
const modal = document.getElementById('settings-modal');
|
||||
const closeButton = document.getElementById('settings-modal-close');
|
||||
const cancelButton = document.getElementById('settings-cancel-button');
|
||||
const testButton = document.getElementById('settings-test-button');
|
||||
const saveButton = document.getElementById('settings-save-button');
|
||||
|
||||
if (settingsButton) {
|
||||
settingsButton.addEventListener('click', openModal);
|
||||
}
|
||||
|
||||
if (closeButton) {
|
||||
closeButton.addEventListener('click', closeModal);
|
||||
}
|
||||
|
||||
if (cancelButton) {
|
||||
cancelButton.addEventListener('click', closeModal);
|
||||
}
|
||||
|
||||
if (testButton) {
|
||||
testButton.addEventListener('click', testConnections);
|
||||
}
|
||||
|
||||
if (saveButton) {
|
||||
saveButton.addEventListener('click', saveConfiguration);
|
||||
}
|
||||
|
||||
// Close modal when clicking outside
|
||||
if (modal) {
|
||||
modal.addEventListener('click', (e) => {
|
||||
if (e.target === modal) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Handle escape key
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && !modal.classList.contains('hidden')) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
|
||||
isInitialized = true;
|
||||
console.log('[Settings] Settings module initialized');
|
||||
}
|
||||
|
||||
function load() {
|
||||
console.log('[Settings] Loading current configuration...');
|
||||
loadCurrentConfig();
|
||||
async function openModal() {
|
||||
console.log('[Settings] Opening settings modal...');
|
||||
|
||||
const modal = document.getElementById('settings-modal');
|
||||
const body = document.getElementById('settings-modal-body');
|
||||
|
||||
if (!modal || !body) return;
|
||||
|
||||
// Show modal
|
||||
modal.classList.remove('hidden');
|
||||
modal.classList.add('flex');
|
||||
|
||||
// Load current configuration
|
||||
try {
|
||||
await loadCurrentConfig();
|
||||
renderConfigurationForm();
|
||||
} catch (error) {
|
||||
console.error('[Settings] Failed to load configuration:', error);
|
||||
body.innerHTML = `<div class="text-red-600 dark:text-red-400">Failed to load configuration: ${error.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
console.log('[Settings] Closing settings modal...');
|
||||
|
||||
const modal = document.getElementById('settings-modal');
|
||||
if (modal) {
|
||||
modal.classList.add('hidden');
|
||||
modal.classList.remove('flex');
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCurrentConfig() {
|
||||
@@ -30,103 +96,354 @@ PulseApp.ui.settings = (() => {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const config = await response.json();
|
||||
console.log('[Settings] Current config loaded:', config);
|
||||
|
||||
// Populate Proxmox fields
|
||||
if (config.proxmox) {
|
||||
document.getElementById('settings-proxmox-host').value = config.proxmox.host || '';
|
||||
document.getElementById('settings-proxmox-port').value = config.proxmox.port || '';
|
||||
document.getElementById('settings-proxmox-token-id').value = config.proxmox.tokenId || '';
|
||||
// Don't populate the secret for security
|
||||
document.getElementById('settings-proxmox-token-secret').placeholder = 'Enter new secret or leave blank to keep current';
|
||||
}
|
||||
currentConfig = await response.json();
|
||||
console.log('[Settings] Current config loaded:', currentConfig);
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Settings] Failed to load current configuration:', error);
|
||||
showError('Failed to load current configuration: ' + error.message);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleFormSubmission(e) {
|
||||
e.preventDefault();
|
||||
function renderConfigurationForm() {
|
||||
const body = document.getElementById('settings-modal-body');
|
||||
if (!body) return;
|
||||
|
||||
const html = `
|
||||
<form id="settings-form" class="space-y-6">
|
||||
<!-- Proxmox VE Primary Endpoint -->
|
||||
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-4">
|
||||
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-4">Primary Proxmox VE Server</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label 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" name="PROXMOX_HOST" required
|
||||
value="${currentConfig.proxmox?.host || ''}"
|
||||
placeholder="https://proxmox.example.com:8006"
|
||||
class="w-full px-3 py-2 text-sm 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 class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Port</label>
|
||||
<input type="number" name="PROXMOX_PORT"
|
||||
value="${currentConfig.proxmox?.port || ''}"
|
||||
placeholder="8006"
|
||||
class="w-full px-3 py-2 text-sm 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 class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Node Name
|
||||
</label>
|
||||
<input type="text" name="PROXMOX_NODE_NAME"
|
||||
value="${currentConfig.proxmox?.nodeName || ''}"
|
||||
placeholder="Display name (optional)"
|
||||
class="w-full px-3 py-2 text-sm 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 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" name="PROXMOX_TOKEN_ID" required
|
||||
value="${currentConfig.proxmox?.tokenId || ''}"
|
||||
placeholder="root@pam!token-name"
|
||||
class="w-full px-3 py-2 text-sm 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 class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
API Token Secret
|
||||
</label>
|
||||
<input type="password" name="PROXMOX_TOKEN_SECRET"
|
||||
placeholder="Leave blank to keep current"
|
||||
class="w-full px-3 py-2 text-sm 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 class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Enabled</label>
|
||||
<input type="checkbox" name="PROXMOX_ENABLED" ${currentConfig.proxmox?.enabled !== false ? 'checked' : ''}
|
||||
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Additional Proxmox VE Endpoints -->
|
||||
<div id="additional-pve-endpoints">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200">Additional Proxmox VE Servers</h3>
|
||||
<button type="button" onclick="PulseApp.ui.settings.addPveEndpoint()"
|
||||
class="px-3 py-1 bg-blue-600 hover:bg-blue-700 text-white text-sm rounded-md">
|
||||
Add PVE Server
|
||||
</button>
|
||||
</div>
|
||||
<div id="pve-endpoints-container"></div>
|
||||
</div>
|
||||
|
||||
<!-- Primary PBS Configuration -->
|
||||
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-4">
|
||||
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-4">Proxmox Backup Server (Optional)</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Host Address</label>
|
||||
<input type="text" name="PBS_HOST"
|
||||
value="${currentConfig.pbs?.host || ''}"
|
||||
placeholder="https://pbs.example.com:8007"
|
||||
class="w-full px-3 py-2 text-sm 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 class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Port</label>
|
||||
<input type="number" name="PBS_PORT"
|
||||
value="${currentConfig.pbs?.port || ''}"
|
||||
placeholder="8007"
|
||||
class="w-full px-3 py-2 text-sm 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 class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Node Name
|
||||
</label>
|
||||
<input type="text" name="PBS_NODE_NAME"
|
||||
value="${currentConfig.pbs?.nodeName || ''}"
|
||||
placeholder="PBS internal hostname"
|
||||
class="w-full px-3 py-2 text-sm 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 class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">API Token ID</label>
|
||||
<input type="text" name="PBS_TOKEN_ID"
|
||||
value="${currentConfig.pbs?.tokenId || ''}"
|
||||
placeholder="root@pam!token-name"
|
||||
class="w-full px-3 py-2 text-sm 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 class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">API Token Secret</label>
|
||||
<input type="password" name="PBS_TOKEN_SECRET"
|
||||
placeholder="Leave blank to keep current"
|
||||
class="w-full px-3 py-2 text-sm 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>
|
||||
|
||||
<!-- Additional PBS Endpoints -->
|
||||
<div id="additional-pbs-endpoints">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200">Additional PBS Servers</h3>
|
||||
<button type="button" onclick="PulseApp.ui.settings.addPbsEndpoint()"
|
||||
class="px-3 py-1 bg-green-600 hover:bg-green-700 text-white text-sm rounded-md">
|
||||
Add PBS Server
|
||||
</button>
|
||||
</div>
|
||||
<div id="pbs-endpoints-container"></div>
|
||||
</div>
|
||||
|
||||
<!-- Service Settings -->
|
||||
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-4">
|
||||
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-4">Service Settings</h3>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Metric Update Interval (ms)
|
||||
</label>
|
||||
<input type="number" name="PULSE_METRIC_INTERVAL_MS"
|
||||
value="${currentConfig.advanced?.metricInterval || ''}"
|
||||
placeholder="2000 (default)"
|
||||
min="1000" max="60000"
|
||||
class="w-full px-3 py-2 text-sm 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 class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Discovery Interval (ms)
|
||||
</label>
|
||||
<input type="number" name="PULSE_DISCOVERY_INTERVAL_MS"
|
||||
value="${currentConfig.advanced?.discoveryInterval || ''}"
|
||||
placeholder="30000 (default)"
|
||||
min="5000" max="300000"
|
||||
class="w-full px-3 py-2 text-sm 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>
|
||||
</div>
|
||||
|
||||
<!-- Alert Settings -->
|
||||
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-4">
|
||||
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-4">Alert Configuration</h3>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-4">
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" name="ALERT_CPU_ENABLED" ${currentConfig.advanced?.alerts?.cpu?.enabled !== false ? 'checked' : ''}
|
||||
class="mr-2 h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">CPU Alerts</span>
|
||||
</label>
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" name="ALERT_MEMORY_ENABLED" ${currentConfig.advanced?.alerts?.memory?.enabled !== false ? 'checked' : ''}
|
||||
class="mr-2 h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">Memory Alerts</span>
|
||||
</label>
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" name="ALERT_DISK_ENABLED" ${currentConfig.advanced?.alerts?.disk?.enabled !== false ? 'checked' : ''}
|
||||
class="mr-2 h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">Disk Alerts</span>
|
||||
</label>
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" name="ALERT_DOWN_ENABLED" ${currentConfig.advanced?.alerts?.down?.enabled !== false ? 'checked' : ''}
|
||||
class="mr-2 h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
|
||||
<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 class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
CPU Threshold (%)
|
||||
</label>
|
||||
<input type="number" name="ALERT_CPU_THRESHOLD"
|
||||
value="${currentConfig.advanced?.alerts?.cpu?.threshold || ''}"
|
||||
placeholder="85 (default)"
|
||||
min="50" max="100"
|
||||
class="w-full px-3 py-2 text-sm 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 class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Memory Threshold (%)
|
||||
</label>
|
||||
<input type="number" name="ALERT_MEMORY_THRESHOLD"
|
||||
value="${currentConfig.advanced?.alerts?.memory?.threshold || ''}"
|
||||
placeholder="90 (default)"
|
||||
min="50" max="100"
|
||||
class="w-full px-3 py-2 text-sm 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 class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
||||
Disk Threshold (%)
|
||||
</label>
|
||||
<input type="number" name="ALERT_DISK_THRESHOLD"
|
||||
value="${currentConfig.advanced?.alerts?.disk?.threshold || ''}"
|
||||
placeholder="95 (default)"
|
||||
min="50" max="100"
|
||||
class="w-full px-3 py-2 text-sm 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>
|
||||
|
||||
<!-- Status Messages -->
|
||||
<div id="settings-messages"></div>
|
||||
</form>
|
||||
`;
|
||||
|
||||
body.innerHTML = html;
|
||||
|
||||
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) {
|
||||
showError('Please fill in all required fields (Host and Token ID)');
|
||||
return;
|
||||
}
|
||||
|
||||
// If token secret is empty, don't include it (keep existing)
|
||||
if (!config.proxmox.tokenSecret) {
|
||||
delete config.proxmox.tokenSecret;
|
||||
}
|
||||
|
||||
hideMessages();
|
||||
const button = document.getElementById('settings-save-button');
|
||||
const originalText = button.textContent;
|
||||
button.disabled = true;
|
||||
button.textContent = 'Saving...';
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/config', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(config)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok && result.success) {
|
||||
showSuccess('Configuration saved successfully!');
|
||||
// Reload the current config to show updated values
|
||||
setTimeout(() => {
|
||||
loadCurrentConfig();
|
||||
}, 1000);
|
||||
} else {
|
||||
showError(result.error || 'Failed to save configuration');
|
||||
}
|
||||
} catch (error) {
|
||||
showError('Failed to save configuration: ' + error.message);
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
button.textContent = originalText;
|
||||
}
|
||||
// Render additional endpoints
|
||||
renderAdditionalEndpoints();
|
||||
}
|
||||
|
||||
async function testConnection() {
|
||||
const formData = new FormData(document.getElementById('settings-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')
|
||||
}
|
||||
};
|
||||
function renderAdditionalEndpoints() {
|
||||
// This will be populated based on what additional endpoints exist in config
|
||||
// For now, we'll support dynamic addition
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if (!config.proxmox.host || !config.proxmox.tokenId || !config.proxmox.tokenSecret) {
|
||||
showError('Please fill in all required fields to test connection');
|
||||
return;
|
||||
}
|
||||
function addPveEndpoint() {
|
||||
const container = document.getElementById('pve-endpoints-container');
|
||||
if (!container) return;
|
||||
|
||||
hideMessages();
|
||||
const button = event.target;
|
||||
const originalText = button.textContent;
|
||||
button.disabled = true;
|
||||
button.textContent = 'Testing...';
|
||||
const index = container.children.length + 2; // Start from _2
|
||||
const endpointHtml = `
|
||||
<div class="border border-gray-200 dark:border-gray-700 rounded-lg p-4 mb-4 relative">
|
||||
<button type="button" onclick="this.parentElement.remove()"
|
||||
class="absolute top-2 right-2 text-red-600 hover:text-red-800">
|
||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<h4 class="text-md font-medium text-gray-800 dark:text-gray-200 mb-3">PVE Server #${index}</h4>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Host Address</label>
|
||||
<input type="text" name="PROXMOX_HOST_${index}" placeholder="https://pve${index}.example.com:8006"
|
||||
class="w-full px-3 py-2 text-sm 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 class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Port</label>
|
||||
<input type="number" name="PROXMOX_PORT_${index}" placeholder="8006"
|
||||
class="w-full px-3 py-2 text-sm 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 class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Node Name</label>
|
||||
<input type="text" name="PROXMOX_NODE_NAME_${index}" placeholder="PVE Server ${index}"
|
||||
class="w-full px-3 py-2 text-sm 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 class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">API Token ID</label>
|
||||
<input type="text" name="PROXMOX_TOKEN_ID_${index}" placeholder="root@pam!token-name"
|
||||
class="w-full px-3 py-2 text-sm 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 class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">API Token Secret</label>
|
||||
<input type="password" name="PROXMOX_TOKEN_SECRET_${index}" placeholder="Enter token secret"
|
||||
class="w-full px-3 py-2 text-sm 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 class="flex items-center mt-6">
|
||||
<input type="checkbox" name="PROXMOX_ENABLED_${index}" checked
|
||||
class="mr-2 h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">Enabled</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.insertAdjacentHTML('beforeend', endpointHtml);
|
||||
}
|
||||
|
||||
function addPbsEndpoint() {
|
||||
const container = document.getElementById('pbs-endpoints-container');
|
||||
if (!container) return;
|
||||
|
||||
const index = container.children.length + 2; // Start from _2
|
||||
const endpointHtml = `
|
||||
<div class="border border-gray-200 dark:border-gray-700 rounded-lg p-4 mb-4 relative">
|
||||
<button type="button" onclick="this.parentElement.remove()"
|
||||
class="absolute top-2 right-2 text-red-600 hover:text-red-800">
|
||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<h4 class="text-md font-medium text-gray-800 dark:text-gray-200 mb-3">PBS Server #${index}</h4>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Host Address</label>
|
||||
<input type="text" name="PBS_HOST_${index}" placeholder="https://pbs${index}.example.com:8007"
|
||||
class="w-full px-3 py-2 text-sm 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 class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Port</label>
|
||||
<input type="number" name="PBS_PORT_${index}" placeholder="8007"
|
||||
class="w-full px-3 py-2 text-sm 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 class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Node Name</label>
|
||||
<input type="text" name="PBS_NODE_NAME_${index}" placeholder="PBS internal hostname"
|
||||
class="w-full px-3 py-2 text-sm 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 class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">API Token ID</label>
|
||||
<input type="text" name="PBS_TOKEN_ID_${index}" placeholder="root@pam!token-name"
|
||||
class="w-full px-3 py-2 text-sm 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 class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">API Token Secret</label>
|
||||
<input type="password" name="PBS_TOKEN_SECRET_${index}" placeholder="Enter token secret"
|
||||
class="w-full px-3 py-2 text-sm 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>
|
||||
`;
|
||||
|
||||
container.insertAdjacentHTML('beforeend', endpointHtml);
|
||||
}
|
||||
|
||||
async function testConnections() {
|
||||
showMessage('Testing connections...', 'info');
|
||||
|
||||
const config = collectFormData();
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/config/test', {
|
||||
method: 'POST',
|
||||
@@ -137,63 +454,114 @@ PulseApp.ui.settings = (() => {
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok && result.success) {
|
||||
showSuccess('Connection test successful!');
|
||||
setTimeout(() => hideMessages(), 3000);
|
||||
showMessage('All connections tested successfully!', 'success');
|
||||
} else {
|
||||
showError(result.error || 'Connection test failed');
|
||||
showMessage(result.error || 'Connection test failed', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showError('Failed to test connection: ' + error.message);
|
||||
showMessage('Failed to test connections: ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function saveConfiguration() {
|
||||
const saveButton = document.getElementById('settings-save-button');
|
||||
if (!saveButton) return;
|
||||
|
||||
const originalText = saveButton.textContent;
|
||||
saveButton.disabled = true;
|
||||
saveButton.textContent = 'Saving...';
|
||||
|
||||
try {
|
||||
const config = collectFormData();
|
||||
|
||||
const response = await fetch('/api/config', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(config)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok && result.success) {
|
||||
showMessage('Configuration saved successfully!', 'success');
|
||||
setTimeout(() => {
|
||||
closeModal();
|
||||
}, 1500);
|
||||
} else {
|
||||
showMessage(result.error || 'Failed to save configuration', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showMessage('Failed to save configuration: ' + error.message, 'error');
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
button.textContent = originalText;
|
||||
saveButton.disabled = false;
|
||||
saveButton.textContent = originalText;
|
||||
}
|
||||
}
|
||||
|
||||
function togglePasswordVisibility(inputId) {
|
||||
const input = document.getElementById(inputId);
|
||||
if (input) {
|
||||
input.type = input.type === 'password' ? 'text' : 'password';
|
||||
function collectFormData() {
|
||||
const form = document.getElementById('settings-form');
|
||||
if (!form) return {};
|
||||
|
||||
const formData = new FormData(form);
|
||||
const config = {};
|
||||
|
||||
// Build the config object from form data
|
||||
for (const [name, value] of formData.entries()) {
|
||||
if (value.trim() === '') continue; // Skip empty values
|
||||
|
||||
// Handle checkbox values
|
||||
if (form.querySelector(`[name="${name}"]`).type === 'checkbox') {
|
||||
config[name] = 'true';
|
||||
} else {
|
||||
config[name] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Also handle unchecked checkboxes
|
||||
const checkboxes = form.querySelectorAll('input[type="checkbox"]');
|
||||
checkboxes.forEach(checkbox => {
|
||||
if (!checkbox.checked && !config[checkbox.name]) {
|
||||
config[checkbox.name] = 'false';
|
||||
}
|
||||
});
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
const errorDiv = document.getElementById('settings-error-message');
|
||||
const errorText = document.getElementById('settings-error-text');
|
||||
const successDiv = document.getElementById('settings-success-message');
|
||||
|
||||
if (errorText && errorDiv && successDiv) {
|
||||
errorText.textContent = message;
|
||||
errorDiv.classList.remove('hidden');
|
||||
successDiv.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
function showMessage(message, type) {
|
||||
const container = document.getElementById('settings-messages');
|
||||
if (!container) return;
|
||||
|
||||
function showSuccess(message) {
|
||||
const errorDiv = document.getElementById('settings-error-message');
|
||||
const successDiv = document.getElementById('settings-success-message');
|
||||
const successText = document.getElementById('settings-success-text');
|
||||
|
||||
if (successText && successDiv && errorDiv) {
|
||||
successText.textContent = message;
|
||||
errorDiv.classList.add('hidden');
|
||||
successDiv.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
const typeClasses = {
|
||||
error: 'bg-red-50 dark:bg-red-900/20 border-red-200 dark:border-red-800 text-red-700 dark:text-red-300',
|
||||
success: 'bg-green-50 dark:bg-green-900/20 border-green-200 dark:border-green-800 text-green-700 dark:text-green-300',
|
||||
info: 'bg-blue-50 dark:bg-blue-900/20 border-blue-200 dark:border-blue-800 text-blue-700 dark:text-blue-300'
|
||||
};
|
||||
|
||||
function hideMessages() {
|
||||
const errorDiv = document.getElementById('settings-error-message');
|
||||
const successDiv = document.getElementById('settings-success-message');
|
||||
|
||||
if (errorDiv) errorDiv.classList.add('hidden');
|
||||
if (successDiv) successDiv.classList.add('hidden');
|
||||
const html = `
|
||||
<div class="border rounded-lg p-3 ${typeClasses[type] || typeClasses.info}">
|
||||
${message}
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.innerHTML = html;
|
||||
|
||||
// Clear message after 5 seconds for non-error messages
|
||||
if (type !== 'error') {
|
||||
setTimeout(() => {
|
||||
container.innerHTML = '';
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
// Public API
|
||||
return {
|
||||
init,
|
||||
load,
|
||||
testConnection,
|
||||
togglePasswordVisibility
|
||||
openModal,
|
||||
closeModal,
|
||||
addPveEndpoint,
|
||||
addPbsEndpoint,
|
||||
testConnections,
|
||||
saveConfiguration
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user