mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-21 18:53:37 +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>
330 lines
13 KiB
JavaScript
330 lines
13 KiB
JavaScript
const { URL } = require('url');
|
|
|
|
// Align placeholder values with install script and .env.example
|
|
const placeholderValues = [
|
|
// Hostname parts - case-insensitive matching might be better if OS env vars differ.
|
|
// For now, direct case-sensitive include check.
|
|
'your-proxmox-ip-or-hostname',
|
|
'proxmox_host', // Substring for https://proxmox_host:8006 or similar
|
|
'YOUR_PBS_IP_OR_HOSTNAME', // For PBS host
|
|
|
|
// Token ID parts - these are more specific to example/guidance values
|
|
'user@pam!your-token-name', // Matches common PVE example format
|
|
'user@pbs!your-token-name', // Matches common PBS example format
|
|
'your-api-token-id', // Generic part often seen in examples
|
|
'user@pam!tokenid', // From original install script comment
|
|
'user@pbs!tokenid', // PBS variant of install script comment
|
|
|
|
// Secret parts
|
|
'your-token-secret-uuid', // Common PVE secret example
|
|
'your-pbs-token-secret-uuid', // Common PBS secret example
|
|
'YOUR_API_SECRET_HERE', // From original install script comment
|
|
'secret-uuid', // Specific value used in config.test.js
|
|
'your-api-token-uuid', // Specific value used in config.test.js for PROXMOX_TOKEN_SECRET
|
|
'your-port' // Specific value used in config.test.js for PROXMOX_PORT
|
|
];
|
|
|
|
// Error class for configuration issues
|
|
class ConfigurationError extends Error {
|
|
constructor(message) {
|
|
super(message);
|
|
this.name = 'ConfigurationError';
|
|
}
|
|
}
|
|
|
|
// Function to get update channel preference
|
|
function getUpdateChannelPreference() {
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
// Try to read from config/.env file first, then fallback to default
|
|
const configDir = path.join(__dirname, '../config');
|
|
const configEnvPath = path.join(configDir, '.env');
|
|
|
|
let updateChannel = 'stable'; // Default value
|
|
|
|
if (fs.existsSync(configEnvPath)) {
|
|
try {
|
|
const configContent = fs.readFileSync(configEnvPath, 'utf8');
|
|
const updateChannelMatch = configContent.match(/^UPDATE_CHANNEL=(.+)$/m);
|
|
if (updateChannelMatch) {
|
|
updateChannel = updateChannelMatch[1].trim();
|
|
}
|
|
} catch (error) {
|
|
console.warn('WARN: Could not read UPDATE_CHANNEL from config file. Using default "stable".');
|
|
}
|
|
}
|
|
|
|
const validChannels = ['stable', 'rc'];
|
|
if (!validChannels.includes(updateChannel)) {
|
|
console.warn(`WARN: Invalid UPDATE_CHANNEL value "${updateChannel}". Using default "stable".`);
|
|
return 'stable';
|
|
}
|
|
|
|
return updateChannel;
|
|
}
|
|
|
|
// Function to load PBS configuration
|
|
function loadPbsConfig(index = null) {
|
|
const suffix = index ? `_${index}` : '';
|
|
const hostVar = `PBS_HOST${suffix}`;
|
|
const tokenIdVar = `PBS_TOKEN_ID${suffix}`;
|
|
const tokenSecretVar = `PBS_TOKEN_SECRET${suffix}`;
|
|
const nodeNameVar = `PBS_NODE_NAME${suffix}`;
|
|
const portVar = `PBS_PORT${suffix}`;
|
|
const selfSignedVar = `PBS_ALLOW_SELF_SIGNED_CERTS${suffix}`;
|
|
const enabledVar = `PBS_ENABLED${suffix}`;
|
|
const resilientDnsVar = `PBS_RESILIENT_DNS${suffix}`;
|
|
|
|
const pbsHostUrl = process.env[hostVar];
|
|
if (!pbsHostUrl) {
|
|
return false; // No more PBS configs if PBS_HOST is missing
|
|
}
|
|
|
|
let pbsHostname = pbsHostUrl;
|
|
try {
|
|
const parsedUrl = new URL(pbsHostUrl);
|
|
pbsHostname = parsedUrl.hostname;
|
|
} catch (e) {
|
|
// console.warn(`WARN: Could not parse PBS_HOST URL "${pbsHostUrl}". Using full value as fallback name.`);
|
|
}
|
|
|
|
const pbsTokenId = process.env[tokenIdVar];
|
|
const pbsTokenSecret = process.env[tokenSecretVar];
|
|
|
|
let config = null;
|
|
let idPrefix = index ? `pbs_endpoint_${index}` : 'pbs_primary';
|
|
|
|
if (pbsTokenId && pbsTokenSecret) {
|
|
const pbsPlaceholders = placeholderValues.filter(p =>
|
|
pbsHostUrl.includes(p) || pbsTokenId.includes(p) || pbsTokenSecret.includes(p)
|
|
);
|
|
if (pbsPlaceholders.length > 0) {
|
|
console.warn(`WARN: Skipping PBS configuration ${index || 'primary'} (Token). Placeholder values detected for: ${pbsPlaceholders.join(', ')}`);
|
|
} else {
|
|
config = {
|
|
id: `${idPrefix}_token`,
|
|
authMethod: 'token',
|
|
name: process.env[nodeNameVar] || pbsHostname,
|
|
host: pbsHostUrl,
|
|
port: process.env[portVar] || '8007',
|
|
tokenId: pbsTokenId,
|
|
tokenSecret: pbsTokenSecret,
|
|
nodeName: process.env[nodeNameVar], // Keep nodeName field
|
|
allowSelfSignedCerts: process.env[selfSignedVar] !== 'false',
|
|
enabled: process.env[enabledVar] !== 'false',
|
|
useResilientDns: process.env[resilientDnsVar] === 'true'
|
|
};
|
|
console.log(`INFO: Found PBS configuration ${index || 'primary'} with ID: ${config.id}, name: ${config.name}, host: ${config.host}`);
|
|
}
|
|
} else {
|
|
console.warn(`WARN: Partial PBS configuration found for ${hostVar}. Please set (${tokenIdVar} + ${tokenSecretVar}) along with ${hostVar}.`);
|
|
}
|
|
|
|
if (config) {
|
|
return { found: true, config: config }; // Return config if found
|
|
}
|
|
// Return found:true if host was set, but config was invalid/partial
|
|
return { found: !!pbsHostUrl, config: null };
|
|
}
|
|
|
|
|
|
// Main function to load all configurations
|
|
function loadConfiguration() {
|
|
// Only load .env file if not in test environment
|
|
if (process.env.NODE_ENV !== 'test') {
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const configDir = path.join(__dirname, '../config');
|
|
const configEnvPath = path.join(configDir, '.env');
|
|
const projectEnvPath = path.join(__dirname, '../.env');
|
|
|
|
if (fs.existsSync(configEnvPath)) {
|
|
require('dotenv').config({ path: configEnvPath });
|
|
} else {
|
|
require('dotenv').config({ path: projectEnvPath });
|
|
}
|
|
}
|
|
|
|
let isConfigPlaceholder = false; // Add this flag
|
|
|
|
// --- Proxmox Primary Endpoint Validation ---
|
|
const primaryRequiredEnvVars = [
|
|
'PROXMOX_HOST',
|
|
'PROXMOX_TOKEN_ID',
|
|
'PROXMOX_TOKEN_SECRET'
|
|
];
|
|
let missingVars = [];
|
|
let placeholderVars = [];
|
|
|
|
primaryRequiredEnvVars.forEach(varName => {
|
|
const value = process.env[varName];
|
|
if (!value) {
|
|
missingVars.push(varName);
|
|
} else if (placeholderValues.some(placeholder => value.includes(placeholder) || placeholder.includes(value))) {
|
|
placeholderVars.push(varName);
|
|
}
|
|
});
|
|
|
|
// Throw error only if required vars are MISSING
|
|
if (missingVars.length > 0) {
|
|
console.warn('--- Configuration Warning ---');
|
|
console.warn(`Missing required environment variables: ${missingVars.join(', ')}.`);
|
|
console.warn('Pulse will start in setup mode. Please configure via the web interface.');
|
|
|
|
// Return minimal configuration to allow server to start
|
|
return {
|
|
endpoints: [],
|
|
pbsConfigs: [],
|
|
isConfigPlaceholder: true
|
|
};
|
|
}
|
|
|
|
// Set the flag if placeholders were detected (but don't throw error)
|
|
if (placeholderVars.length > 0) {
|
|
isConfigPlaceholder = true;
|
|
// Ensure token ID placeholder is included if missing
|
|
if (process.env.PROXMOX_TOKEN_ID && !placeholderVars.includes('PROXMOX_TOKEN_ID')) {
|
|
const hostIdx = placeholderVars.indexOf('PROXMOX_HOST');
|
|
if (hostIdx !== -1) placeholderVars.splice(hostIdx + 1, 0, 'PROXMOX_TOKEN_ID');
|
|
else placeholderVars.push('PROXMOX_TOKEN_ID');
|
|
}
|
|
console.warn(`WARN: Primary Proxmox environment variables seem to contain placeholder values: ${placeholderVars.join(', ')}. Pulse may not function correctly until configured.`);
|
|
}
|
|
|
|
// --- Load All Proxmox Endpoint Configurations ---
|
|
const endpoints = [];
|
|
|
|
function createProxmoxEndpointConfig(idPrefix, index, hostEnv, portEnv, tokenIdEnv, tokenSecretEnv, enabledEnv, selfSignedEnv, nodeNameEnv) {
|
|
const host = process.env[hostEnv];
|
|
const tokenId = process.env[tokenIdEnv];
|
|
const tokenSecret = process.env[tokenSecretEnv];
|
|
const nodeName = process.env[nodeNameEnv];
|
|
|
|
// Basic validation for additional endpoints (primary is validated earlier)
|
|
if (index !== null && (!tokenId || !tokenSecret)) {
|
|
// console.warn(`WARN: Skipping endpoint ${index || idPrefix} (Host: ${host}). Missing token ID or secret.`);
|
|
return null;
|
|
}
|
|
if (index !== null && placeholderValues.some(p => host.includes(p) || tokenId.includes(p) || tokenSecret.includes(p))) {
|
|
// console.warn(`WARN: Skipping endpoint ${index || idPrefix} (Host: ${host}). Environment variables seem to contain placeholder values.`);
|
|
return null;
|
|
}
|
|
|
|
// Check for resilient DNS configuration
|
|
const resilientDnsEnv = index ? `PROXMOX_RESILIENT_DNS_${index}` : 'PROXMOX_RESILIENT_DNS';
|
|
const useResilientDns = process.env[resilientDnsEnv] === 'true';
|
|
|
|
return {
|
|
id: index ? `${idPrefix}_${index}` : idPrefix,
|
|
name: nodeName || null, // Only use explicitly configured names
|
|
host: host,
|
|
port: process.env[portEnv] || '8006',
|
|
tokenId: tokenId,
|
|
tokenSecret: tokenSecret,
|
|
enabled: process.env[enabledEnv] !== 'false',
|
|
allowSelfSignedCerts: process.env[selfSignedEnv] !== 'false',
|
|
useResilientDns: useResilientDns,
|
|
};
|
|
}
|
|
|
|
// Load primary endpoint (index null for helper)
|
|
const primaryEndpoint = createProxmoxEndpointConfig(
|
|
'primary',
|
|
null, // No index for primary
|
|
'PROXMOX_HOST',
|
|
'PROXMOX_PORT',
|
|
'PROXMOX_TOKEN_ID',
|
|
'PROXMOX_TOKEN_SECRET',
|
|
'PROXMOX_ENABLED',
|
|
'PROXMOX_ALLOW_SELF_SIGNED_CERTS',
|
|
'PROXMOX_NODE_NAME'
|
|
);
|
|
if (primaryEndpoint) { // Should always exist due to earlier checks, but good practice
|
|
endpoints.push(primaryEndpoint);
|
|
}
|
|
|
|
// Load additional Proxmox endpoints
|
|
// Check all environment variables for PROXMOX_HOST_N pattern to handle non-sequential numbering
|
|
const proxmoxHostKeys = Object.keys(process.env)
|
|
.filter(key => key.match(/^PROXMOX_HOST_\d+$/))
|
|
.map(key => {
|
|
const match = key.match(/^PROXMOX_HOST_(\d+)$/);
|
|
return match ? parseInt(match[1]) : null;
|
|
})
|
|
.filter(num => num !== null)
|
|
.sort((a, b) => a - b);
|
|
|
|
for (const i of proxmoxHostKeys) {
|
|
const additionalEndpoint = createProxmoxEndpointConfig(
|
|
'endpoint',
|
|
i,
|
|
`PROXMOX_HOST_${i}`,
|
|
`PROXMOX_PORT_${i}`,
|
|
`PROXMOX_TOKEN_ID_${i}`,
|
|
`PROXMOX_TOKEN_SECRET_${i}`,
|
|
`PROXMOX_ENABLED_${i}`,
|
|
`PROXMOX_ALLOW_SELF_SIGNED_CERTS_${i}`,
|
|
`PROXMOX_NODE_NAME_${i}`
|
|
);
|
|
if (additionalEndpoint) {
|
|
endpoints.push(additionalEndpoint);
|
|
}
|
|
}
|
|
|
|
if (endpoints.length > 1) {
|
|
// console.log(`INFO: Loaded configuration for ${endpoints.length} Proxmox endpoints.`);
|
|
}
|
|
|
|
// --- Load All PBS Configurations ---
|
|
const pbsConfigs = [];
|
|
// Load primary PBS config
|
|
const primaryPbsResult = loadPbsConfig();
|
|
/* istanbul ignore else */ // Ignore else path - tested by 'should not add primary PBS config if host is set but tokens are missing'
|
|
if (primaryPbsResult.config) {
|
|
pbsConfigs.push(primaryPbsResult.config);
|
|
}
|
|
|
|
// Load additional PBS configs
|
|
// Check all environment variables for PBS_HOST_N pattern to handle non-sequential numbering
|
|
const pbsHostKeys = Object.keys(process.env)
|
|
.filter(key => key.match(/^PBS_HOST_\d+$/))
|
|
.map(key => {
|
|
const match = key.match(/^PBS_HOST_(\d+)$/);
|
|
return match ? parseInt(match[1]) : null;
|
|
})
|
|
.filter(num => num !== null)
|
|
.sort((a, b) => a - b);
|
|
|
|
for (const pbsIndex of pbsHostKeys) {
|
|
const pbsResult = loadPbsConfig(pbsIndex);
|
|
if (pbsResult.config) {
|
|
pbsConfigs.push(pbsResult.config);
|
|
}
|
|
}
|
|
|
|
if (pbsConfigs.length > 0) {
|
|
// console.log(`INFO: Loaded configuration for ${pbsConfigs.length} PBS instances.`);
|
|
} else {
|
|
// console.log("INFO: No PBS instances configured.");
|
|
}
|
|
|
|
// --- Final Validation ---
|
|
const enabledEndpoints = endpoints.filter(e => e.enabled);
|
|
if (enabledEndpoints.length === 0 && pbsConfigs.length === 0) {
|
|
// Throw error instead of exiting
|
|
throw new ConfigurationError('\n--- Configuration Error ---\nNo enabled Proxmox VE or PBS endpoints could be configured. Please check your .env file and environment variables.\n');
|
|
}
|
|
|
|
// console.log('INFO: Configuration loaded successfully.');
|
|
|
|
// Load update channel preference
|
|
const updateChannel = getUpdateChannelPreference();
|
|
|
|
// Return the flag along with endpoints and pbsConfigs
|
|
return { endpoints, pbsConfigs, isConfigPlaceholder, updateChannel };
|
|
}
|
|
|
|
module.exports = { loadConfiguration, getUpdateChannelPreference, ConfigurationError }; // Export the function and error class
|