Skip to content

Commit bf27ef2

Browse files
committed
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.
1 parent e74f8bf commit bf27ef2

File tree

3 files changed

+167
-17
lines changed

3 files changed

+167
-17
lines changed

server/configApi.js

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,17 +63,24 @@ class ConfigApi {
6363
*/
6464
async saveConfig(config) {
6565
try {
66+
console.log('[ConfigApi.saveConfig] Called with:', JSON.stringify(config, null, 2));
67+
console.log('[ConfigApi.saveConfig] .env path:', this.envPath);
68+
6669
// Read existing .env file to preserve other settings
6770
const existingConfig = await this.readEnvFile();
71+
console.log('[ConfigApi.saveConfig] Existing config keys:', Object.keys(existingConfig));
6872

6973
// Update with new values
7074
if (config.proxmox) {
75+
console.log('[ConfigApi.saveConfig] Updating Proxmox config');
7176
existingConfig.PROXMOX_HOST = config.proxmox.host;
7277
existingConfig.PROXMOX_PORT = config.proxmox.port || '8006';
7378
existingConfig.PROXMOX_TOKEN_ID = config.proxmox.tokenId;
7479
existingConfig.PROXMOX_TOKEN_SECRET = config.proxmox.tokenSecret;
7580
// Always allow self-signed certificates by default for Proxmox
7681
existingConfig.PROXMOX_ALLOW_SELF_SIGNED_CERT = 'true';
82+
} else {
83+
console.log('[ConfigApi.saveConfig] No Proxmox config provided');
7784
}
7885

7986
if (config.pbs) {
@@ -126,10 +133,14 @@ class ConfigApi {
126133
}
127134

128135
// Write back to .env file
136+
console.log('[ConfigApi.saveConfig] Writing config with keys:', Object.keys(existingConfig));
129137
await this.writeEnvFile(existingConfig);
138+
console.log('[ConfigApi.saveConfig] .env file written successfully');
130139

131140
// Reload configuration in the application
141+
console.log('[ConfigApi.saveConfig] Reloading configuration...');
132142
await this.reloadConfiguration();
143+
console.log('[ConfigApi.saveConfig] Configuration reloaded successfully');
133144

134145
return { success: true };
135146
} catch (error) {
@@ -271,7 +282,13 @@ class ConfigApi {
271282
}
272283
});
273284

274-
await fs.writeFile(this.envPath, lines.join('\n'), 'utf8');
285+
try {
286+
await fs.writeFile(this.envPath, lines.join('\n'), 'utf8');
287+
console.log(`[ConfigApi.writeEnvFile] Successfully wrote ${lines.length} lines to ${this.envPath}`);
288+
} catch (writeError) {
289+
console.error('[ConfigApi.writeEnvFile] Error writing file:', writeError);
290+
throw writeError;
291+
}
275292
}
276293

277294
/**
@@ -339,9 +356,12 @@ class ConfigApi {
339356
// Save configuration
340357
app.post('/api/config', async (req, res) => {
341358
try {
342-
await this.saveConfig(req.body);
359+
console.log('[API /api/config] POST received with body:', JSON.stringify(req.body, null, 2));
360+
const result = await this.saveConfig(req.body);
361+
console.log('[API /api/config] Save result:', result);
343362
res.json({ success: true });
344363
} catch (error) {
364+
console.error('[API /api/config] Error:', error);
345365
res.status(500).json({
346366
success: false,
347367
error: error.message || 'Failed to save configuration'
@@ -374,6 +394,22 @@ class ConfigApi {
374394
});
375395
}
376396
});
397+
398+
// Debug endpoint to check .env file
399+
app.get('/api/config/debug', async (req, res) => {
400+
try {
401+
const fs = require('fs');
402+
const envExists = fs.existsSync(this.envPath);
403+
const envContent = envExists ? await fs.promises.readFile(this.envPath, 'utf8') : 'File does not exist';
404+
res.json({
405+
path: this.envPath,
406+
exists: envExists,
407+
content: envContent.substring(0, 500) + '...' // First 500 chars
408+
});
409+
} catch (error) {
410+
res.status(500).json({ error: error.message });
411+
}
412+
});
377413
}
378414
}
379415

server/index.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,17 @@ app.get('/', (req, res) => {
143143
});
144144
});
145145

146+
// Route to explicitly handle setup page
147+
app.get('/setup.html', (req, res) => {
148+
const setupPath = path.join(publicDir, 'setup.html');
149+
res.sendFile(setupPath, (err) => {
150+
if (err) {
151+
console.error(`Error sending setup.html: ${err.message}`);
152+
res.status(err.status || 500).send('Internal Server Error loading setup page.');
153+
}
154+
});
155+
});
156+
146157
// --- API Routes ---
147158
// Set up configuration API routes
148159
configApi.setupRoutes(app);

src/public/setup.html

Lines changed: 118 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ <h1 class="text-3xl font-bold text-gray-800 dark:text-gray-200">Pulse Setup</h1>
4444
</div>
4545

4646
<!-- Configuration Form -->
47-
<form id="config-form" class="space-y-6" autocomplete="off">
47+
<form id="config-form" class="space-y-6" autocomplete="off" onsubmit="return false;">
4848
<!-- Primary Proxmox VE Configuration -->
4949
<div class="border border-gray-200 dark:border-gray-700 rounded-lg p-6">
5050
<h2 class="text-xl font-semibold mb-4 text-gray-800 dark:text-gray-200">Primary Proxmox VE Server</h2>
@@ -274,9 +274,13 @@ <h3 class="text-sm font-medium text-red-800 dark:text-red-200">Configuration Err
274274
<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">
275275
<div class="flex">
276276
<div class="flex-shrink-0">
277-
<svg class="h-5 w-5 text-green-400" viewBox="0 0 20 20" fill="currentColor">
277+
<svg id="success-icon" class="h-5 w-5 text-green-400" viewBox="0 0 20 20" fill="currentColor">
278278
<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" />
279279
</svg>
280+
<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">
281+
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
282+
<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>
283+
</svg>
280284
</div>
281285
<div class="ml-3">
282286
<h3 class="text-sm font-medium text-green-800 dark:text-green-200">Success!</h3>
@@ -291,7 +295,7 @@ <h3 class="text-sm font-medium text-green-800 dark:text-green-200">Success!</h3>
291295
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">
292296
Save Configuration
293297
</button>
294-
<button type="button" onclick="testConnection()"
298+
<button type="button" onclick="testConnection(event)"
295299
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">
296300
Test Connection
297301
</button>
@@ -335,12 +339,27 @@ <h3 class="text-sm font-medium text-green-800 dark:text-green-200">Success!</h3>
335339
successDiv.classList.add('hidden');
336340
}
337341

338-
function showSuccess(message = 'Configuration saved. Redirecting to dashboard...') {
342+
function showSuccess(message = 'Configuration saved. Redirecting to dashboard...', showButton = false, showLoading = false) {
339343
const errorDiv = document.getElementById('error-message');
340344
const successDiv = document.getElementById('success-message');
341345
const successText = document.getElementById('success-text');
346+
const successIcon = document.getElementById('success-icon');
347+
const loadingIcon = document.getElementById('loading-icon');
348+
349+
// Toggle icons
350+
if (showLoading) {
351+
successIcon.classList.add('hidden');
352+
loadingIcon.classList.remove('hidden');
353+
} else {
354+
successIcon.classList.remove('hidden');
355+
loadingIcon.classList.add('hidden');
356+
}
342357

343-
successText.textContent = message;
358+
if (showButton) {
359+
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>';
360+
} else {
361+
successText.textContent = message;
362+
}
344363
errorDiv.classList.add('hidden');
345364
successDiv.classList.remove('hidden');
346365
}
@@ -350,7 +369,8 @@ <h3 class="text-sm font-medium text-green-800 dark:text-green-200">Success!</h3>
350369
document.getElementById('success-message').classList.add('hidden');
351370
}
352371

353-
async function testConnection() {
372+
async function testConnection(event) {
373+
if (event) event.preventDefault();
354374
const formData = new FormData(document.getElementById('config-form'));
355375
const config = {
356376
proxmox: {
@@ -367,7 +387,7 @@ <h3 class="text-sm font-medium text-green-800 dark:text-green-200">Success!</h3>
367387
}
368388

369389
hideMessages();
370-
const button = event.target;
390+
const button = event && event.target ? event.target : document.querySelector('button[onclick*="testConnection"]');
371391
button.disabled = true;
372392
button.textContent = 'Testing...';
373393

@@ -381,7 +401,7 @@ <h3 class="text-sm font-medium text-green-800 dark:text-green-200">Success!</h3>
381401
const result = await response.json();
382402

383403
if (response.ok && result.success) {
384-
showSuccess('Connection test successful!');
404+
showSuccess('Connection test successful!', false, false);
385405
setTimeout(() => hideMessages(), 3000);
386406
} else {
387407
showError(result.error || 'Connection test failed');
@@ -394,8 +414,25 @@ <h3 class="text-sm font-medium text-green-800 dark:text-green-200">Success!</h3>
394414
}
395415
}
396416

397-
document.getElementById('config-form').addEventListener('submit', async (e) => {
417+
// Ensure DOM is loaded before attaching event handlers
418+
if (document.readyState === 'loading') {
419+
document.addEventListener('DOMContentLoaded', setupFormHandlers);
420+
} else {
421+
setupFormHandlers();
422+
}
423+
424+
function setupFormHandlers() {
425+
console.log('Setting up form handlers...');
426+
427+
const form = document.getElementById('config-form');
428+
if (!form) {
429+
console.error('Config form not found!');
430+
return;
431+
}
432+
433+
form.addEventListener('submit', async (e) => {
398434
e.preventDefault();
435+
console.log('Form submit event triggered');
399436

400437
const formData = new FormData(e.target);
401438
const config = {
@@ -406,6 +443,12 @@ <h3 class="text-sm font-medium text-green-800 dark:text-green-200">Success!</h3>
406443
tokenSecret: formData.get('proxmox-token-secret')
407444
}
408445
};
446+
447+
// Validate required fields
448+
if (!config.proxmox.host || !config.proxmox.tokenId || !config.proxmox.tokenSecret) {
449+
showError('Please fill in all required Proxmox fields');
450+
return;
451+
}
409452

410453
// Add PBS config if provided
411454
if (formData.get('pbs-host')) {
@@ -448,6 +491,7 @@ <h3 class="text-sm font-medium text-green-800 dark:text-green-200">Success!</h3>
448491

449492
// Debug: Log what we're sending
450493
console.log('Saving configuration:', JSON.stringify(config, null, 2));
494+
console.log('Making POST request to /api/config...');
451495

452496
try {
453497
const response = await fetch('/api/config', {
@@ -456,24 +500,86 @@ <h3 class="text-sm font-medium text-green-800 dark:text-green-200">Success!</h3>
456500
body: JSON.stringify(config)
457501
});
458502

503+
console.log('Save response status:', response.status, 'ok:', response.ok);
459504
const result = await response.json();
505+
console.log('Save result:', result);
460506

461507
if (response.ok && result.success) {
462-
showSuccess();
508+
showSuccess('Configuration saved! Applying settings...', false, true);
509+
510+
// Wait a moment for the server to start reloading
463511
setTimeout(() => {
464-
window.location.href = '/';
512+
checkServerReady();
465513
}, 2000);
466514
} else {
467515
showError(result.error || 'Failed to save configuration');
468516
button.disabled = false;
469517
button.textContent = 'Save Configuration';
470518
}
471519
} catch (error) {
520+
console.error('Save configuration error:', error);
472521
showError('Failed to save configuration: ' + error.message);
473522
button.disabled = false;
474523
button.textContent = 'Save Configuration';
475524
}
476-
});
525+
});
526+
527+
// Load config on page load
528+
loadExistingConfig();
529+
}
530+
531+
// Check if server is ready after configuration save
532+
async function checkServerReady(attempts = 0) {
533+
const maxAttempts = 15; // 30 seconds total (15 * 2 seconds)
534+
535+
try {
536+
const response = await fetch('/api/health');
537+
538+
if (response.ok) {
539+
const health = await response.json();
540+
console.log('Health check response:', health);
541+
542+
// Check if the server has successfully loaded configuration
543+
// Look for signs that API clients are initialized and not in placeholder mode
544+
if (health.system && health.system.configPlaceholder === false) {
545+
showSuccess('Configuration applied successfully!', true, false);
546+
547+
// Re-enable the save button
548+
const button = document.getElementById('save-button');
549+
button.disabled = false;
550+
button.textContent = 'Save Configuration';
551+
return;
552+
}
553+
}
554+
} catch (error) {
555+
console.log('Health check failed:', error);
556+
}
557+
558+
// If we haven't exceeded max attempts, try again
559+
if (attempts < maxAttempts) {
560+
const messages = [
561+
'Applying configuration...',
562+
'Initializing connections...',
563+
'Connecting to Proxmox servers...',
564+
'Verifying credentials...',
565+
'Loading server data...'
566+
];
567+
const messageIndex = Math.min(Math.floor(attempts / 3), messages.length - 1);
568+
showSuccess(`${messages[messageIndex]} (${Math.floor((attempts / maxAttempts) * 100)}%)`, false, true);
569+
570+
setTimeout(() => {
571+
checkServerReady(attempts + 1);
572+
}, 2000);
573+
} else {
574+
// After 30 seconds, show the button anyway
575+
showSuccess('Configuration saved! The server is taking longer than expected to initialize.', true, false);
576+
577+
// Re-enable the save button
578+
const button = document.getElementById('save-button');
579+
button.disabled = false;
580+
button.textContent = 'Save Configuration';
581+
}
582+
}
477583

478584
// Load existing configuration if available
479585
async function loadExistingConfig() {
@@ -534,9 +640,6 @@ <h3 class="text-sm font-medium text-green-800 dark:text-green-200">Success!</h3>
534640
console.error('Failed to load existing configuration:', error);
535641
}
536642
}
537-
538-
// Load config on page load
539-
loadExistingConfig();
540643
</script>
541644
</body>
542645
</html>

0 commit comments

Comments
 (0)