feat: improve setup flow with health check and Continue button

- Add health endpoint polling after configuration save
- Show progress messages and loading spinner during initialization
- Display 'Continue to Pulse' button when server is ready
- Add explicit /setup.html route handler
- Improve error handling and logging in configApi
- Prevent form submission on button clicks
- Add percentage progress indicator during setup

This provides better user feedback during the configuration process and ensures users don't navigate to the dashboard before the server is ready.
This commit is contained in:
courtmanr@gmail.com
2025-05-30 11:35:40 +01:00
parent e74f8bf944
commit bf27ef2a6b
3 changed files with 167 additions and 17 deletions
+38 -2
View File
@@ -63,17 +63,24 @@ class ConfigApi {
*/
async saveConfig(config) {
try {
console.log('[ConfigApi.saveConfig] Called with:', JSON.stringify(config, null, 2));
console.log('[ConfigApi.saveConfig] .env path:', this.envPath);
// Read existing .env file to preserve other settings
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;
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) {
@@ -126,10 +133,14 @@ class ConfigApi {
}
// Write back to .env file
console.log('[ConfigApi.saveConfig] Writing config with keys:', Object.keys(existingConfig));
await this.writeEnvFile(existingConfig);
console.log('[ConfigApi.saveConfig] .env file written successfully');
// Reload configuration in the application
console.log('[ConfigApi.saveConfig] Reloading configuration...');
await this.reloadConfiguration();
console.log('[ConfigApi.saveConfig] Configuration reloaded successfully');
return { success: true };
} catch (error) {
@@ -271,7 +282,13 @@ class ConfigApi {
}
});
await fs.writeFile(this.envPath, lines.join('\n'), 'utf8');
try {
await fs.writeFile(this.envPath, lines.join('\n'), 'utf8');
console.log(`[ConfigApi.writeEnvFile] Successfully wrote ${lines.length} lines to ${this.envPath}`);
} catch (writeError) {
console.error('[ConfigApi.writeEnvFile] Error writing file:', writeError);
throw writeError;
}
}
/**
@@ -339,9 +356,12 @@ class ConfigApi {
// Save configuration
app.post('/api/config', async (req, res) => {
try {
await this.saveConfig(req.body);
console.log('[API /api/config] POST received with body:', JSON.stringify(req.body, null, 2));
const result = await this.saveConfig(req.body);
console.log('[API /api/config] Save result:', result);
res.json({ success: true });
} catch (error) {
console.error('[API /api/config] Error:', error);
res.status(500).json({
success: false,
error: error.message || 'Failed to save configuration'
@@ -374,6 +394,22 @@ class ConfigApi {
});
}
});
// Debug endpoint to check .env file
app.get('/api/config/debug', async (req, res) => {
try {
const fs = require('fs');
const envExists = fs.existsSync(this.envPath);
const envContent = envExists ? await fs.promises.readFile(this.envPath, 'utf8') : 'File does not exist';
res.json({
path: this.envPath,
exists: envExists,
content: envContent.substring(0, 500) + '...' // First 500 chars
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
}
}
+11
View File
@@ -143,6 +143,17 @@ app.get('/', (req, res) => {
});
});
// Route to explicitly handle setup page
app.get('/setup.html', (req, res) => {
const setupPath = path.join(publicDir, 'setup.html');
res.sendFile(setupPath, (err) => {
if (err) {
console.error(`Error sending setup.html: ${err.message}`);
res.status(err.status || 500).send('Internal Server Error loading setup page.');
}
});
});
// --- API Routes ---
// Set up configuration API routes
configApi.setupRoutes(app);
+118 -15
View File
@@ -44,7 +44,7 @@
</div>
<!-- Configuration Form -->
<form id="config-form" class="space-y-6" autocomplete="off">
<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>
@@ -274,9 +274,13 @@
<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 class="h-5 w-5 text-green-400" viewBox="0 0 20 20" fill="currentColor">
<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>
@@ -291,7 +295,7 @@
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()"
<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>
@@ -335,12 +339,27 @@
successDiv.classList.add('hidden');
}
function showSuccess(message = 'Configuration saved. Redirecting to dashboard...') {
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');
successText.textContent = message;
// 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');
}
@@ -350,7 +369,8 @@
document.getElementById('success-message').classList.add('hidden');
}
async function testConnection() {
async function testConnection(event) {
if (event) event.preventDefault();
const formData = new FormData(document.getElementById('config-form'));
const config = {
proxmox: {
@@ -367,7 +387,7 @@
}
hideMessages();
const button = event.target;
const button = event && event.target ? event.target : document.querySelector('button[onclick*="testConnection"]');
button.disabled = true;
button.textContent = 'Testing...';
@@ -381,7 +401,7 @@
const result = await response.json();
if (response.ok && result.success) {
showSuccess('Connection test successful!');
showSuccess('Connection test successful!', false, false);
setTimeout(() => hideMessages(), 3000);
} else {
showError(result.error || 'Connection test failed');
@@ -394,8 +414,25 @@
}
}
document.getElementById('config-form').addEventListener('submit', async (e) => {
// 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 = {
@@ -406,6 +443,12 @@
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')) {
@@ -448,6 +491,7 @@
// 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', {
@@ -456,12 +500,16 @@
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();
showSuccess('Configuration saved! Applying settings...', false, true);
// Wait a moment for the server to start reloading
setTimeout(() => {
window.location.href = '/';
checkServerReady();
}, 2000);
} else {
showError(result.error || 'Failed to save configuration');
@@ -469,11 +517,69 @@
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 signs that API clients are initialized and not in placeholder mode
if (health.system && health.system.configPlaceholder === false) {
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() {
@@ -534,9 +640,6 @@
console.error('Failed to load existing configuration:', error);
}
}
// Load config on page load
loadExistingConfig();
</script>
</body>
</html>