diff --git a/changelog.md b/changelog.md index 7ba0ed5..397443d 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,15 @@ # Changelog +## 2026-01-29 - 5.2.1 - fix(cli(ups-handler), systemd) +add type guards and null checks for UPS configs; improve SNMP handling and prompts; guard version display + +- Introduce a type guard ('id' in config && 'name' in config) to distinguish IUpsConfig from legacy INupstConfig and route fields (snmp, checkInterval, name, id) accordingly. +- displayTestConfig now handles missing SNMP by logging 'Not configured' and returning, computes checkInterval/upsName/upsId correctly, and uses groups only for true UPS configs. +- testConnection now safely derives snmpConfig for both config types, throws if SNMP is missing, and caps test timeout to 10s for probes. +- Clear auth/priv credentials by setting undefined (instead of empty strings) when disabling security levels to avoid invalid/empty string values. +- Expanded customOIDs to include OUTPUT_LOAD, OUTPUT_POWER, OUTPUT_VOLTAGE, OUTPUT_CURRENT with defaults; trim prompt input and document RFC 1628 fallbacks. +- systemd.displayVersionInfo: guard against missing nupst (silent return) and avoid errors when printing version info; use ignored catch variables for clarity. + ## 2026-01-29 - 5.2.0 - feat(core) Centralize timeouts/constants, add CLI prompt helpers, and introduce webhook/script actions with safety and SNMP refactors diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index d186c6a..782f990 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@serve.zone/nupst', - version: '5.2.0', + version: '5.2.1', description: 'Network UPS Shutdown Tool - Monitor SNMP-enabled UPS devices and orchestrate graceful system shutdowns during power emergencies' } diff --git a/ts/cli/ups-handler.ts b/ts/cli/ups-handler.ts index 95dfc96..808aadc 100644 --- a/ts/cli/ups-handler.ts +++ b/ts/cli/ups-handler.ts @@ -467,18 +467,27 @@ export class UpsHandler { * @param config Current configuration or individual UPS configuration */ private displayTestConfig(config: IUpsConfig | INupstConfig): void { - // Check if this is a UPS device or full configuration - const isUpsConfig = config.snmp; - const snmpConfig = isUpsConfig ? config.snmp : config.snmp || {}; - const checkInterval = config.checkInterval || 30000; + // Type guard: IUpsConfig has 'id' and 'name' at root level, INupstConfig doesn't + const isUpsConfig = 'id' in config && 'name' in config; - // Get UPS name and ID if available - const upsName = config.name ? config.name : 'Default UPS'; - const upsId = config.id ? config.id : 'default'; + // Get SNMP config and other values based on config type + const snmpConfig: ISnmpConfig | undefined = isUpsConfig + ? (config as IUpsConfig).snmp + : (config as INupstConfig).snmp; + const checkInterval = isUpsConfig ? 30000 : (config as INupstConfig).checkInterval || 30000; + const upsName = isUpsConfig ? (config as IUpsConfig).name : 'Default UPS'; + const upsId = isUpsConfig ? (config as IUpsConfig).id : 'default'; const boxWidth = 45; logger.logBoxTitle(`Testing Configuration: ${upsName}`, boxWidth); logger.logBoxLine(`UPS ID: ${upsId}`); + + if (!snmpConfig) { + logger.logBoxLine('SNMP Settings: Not configured'); + logger.logBoxEnd(); + return; + } + logger.logBoxLine('SNMP Settings:'); logger.logBoxLine(` Host: ${snmpConfig.host}`); logger.logBoxLine(` Port: ${snmpConfig.port}`); @@ -514,9 +523,10 @@ export class UpsHandler { logger.logBoxLine(` Battery Runtime: ${snmpConfig.customOIDs.BATTERY_RUNTIME || 'Not set'}`); } // Show group assignments if this is a UPS config - if (config.groups && Array.isArray(config.groups)) { + if (isUpsConfig) { + const groups = (config as IUpsConfig).groups; logger.logBoxLine( - `Group Assignments: ${config.groups.length === 0 ? 'None' : config.groups.join(', ')}`, + `Group Assignments: ${groups.length === 0 ? 'None' : groups.join(', ')}`, ); } @@ -529,15 +539,23 @@ export class UpsHandler { * @param config Current UPS configuration or legacy config */ private async testConnection(config: IUpsConfig | INupstConfig): Promise { - const upsId = config.id || 'default'; - const upsName = config.name || 'Default UPS'; + // Type guard: IUpsConfig has 'id' and 'name' at root level + const isUpsConfig = 'id' in config && 'name' in config; + const upsId = isUpsConfig ? (config as IUpsConfig).id : 'default'; + const upsName = isUpsConfig ? (config as IUpsConfig).name : 'Default UPS'; logger.log(`\nTesting connection to UPS: ${upsName} (${upsId})...`); try { - // Create a test config with a short timeout - const snmpConfig = config.snmp ? config.snmp : config.snmp; + // Get SNMP config based on config type + const snmpConfig: ISnmpConfig | undefined = isUpsConfig + ? (config as IUpsConfig).snmp + : (config as INupstConfig).snmp; - const testConfig = { + if (!snmpConfig) { + throw new Error('SNMP configuration not found'); + } + + const testConfig: ISnmpConfig = { ...snmpConfig, timeout: Math.min(snmpConfig.timeout, 10000), // Use at most 10 seconds for testing }; @@ -675,17 +693,17 @@ export class UpsHandler { if (secLevel === 1) { snmpConfig.securityLevel = 'noAuthNoPriv'; // No auth, no priv - clear out authentication and privacy settings - snmpConfig.authProtocol = ''; - snmpConfig.authKey = ''; - snmpConfig.privProtocol = ''; - snmpConfig.privKey = ''; + snmpConfig.authProtocol = undefined; + snmpConfig.authKey = undefined; + snmpConfig.privProtocol = undefined; + snmpConfig.privKey = undefined; // Set appropriate timeout for security level snmpConfig.timeout = 5000; // 5 seconds for basic security } else if (secLevel === 2) { snmpConfig.securityLevel = 'authNoPriv'; // Auth, no priv - clear out privacy settings - snmpConfig.privProtocol = ''; - snmpConfig.privKey = ''; + snmpConfig.privProtocol = undefined; + snmpConfig.privKey = undefined; // Set appropriate timeout for security level snmpConfig.timeout = 10000; // 10 seconds for authentication } else { @@ -825,16 +843,21 @@ export class UpsHandler { logger.info('Enter custom OIDs for your UPS:'); logger.dim('(Leave blank to use standard RFC 1628 OIDs as fallback)'); - // Custom OIDs + // Custom OIDs - prompt for essential OIDs const powerStatusOID = await prompt('Power Status OID: '); const batteryCapacityOID = await prompt('Battery Capacity OID: '); const batteryRuntimeOID = await prompt('Battery Runtime OID: '); - // Create custom OIDs object + // Create custom OIDs object with all required fields + // Empty strings will use RFC 1628 fallback for non-essential OIDs snmpConfig.customOIDs = { POWER_STATUS: powerStatusOID.trim(), BATTERY_CAPACITY: batteryCapacityOID.trim(), BATTERY_RUNTIME: batteryRuntimeOID.trim(), + OUTPUT_LOAD: '', + OUTPUT_POWER: '', + OUTPUT_VOLTAGE: '', + OUTPUT_CURRENT: '', }; } } diff --git a/ts/systemd.ts b/ts/systemd.ts index 0d8d9a1..2f97208 100644 --- a/ts/systemd.ts +++ b/ts/systemd.ts @@ -142,11 +142,14 @@ WantedBy=multi-user.target private async displayVersionInfo(): Promise { try { const nupst = this.daemon.getNupstSnmp().getNupst(); + if (!nupst) { + return; + } const version = nupst.getVersion(); - + // Check for updates const updateAvailable = await nupst.checkForUpdates(); - + // Display version info if (updateAvailable) { const updateStatus = nupst.getUpdateStatus(); @@ -161,13 +164,15 @@ WantedBy=multi-user.target `${theme.dim('NUPST')} ${theme.dim('v' + version)} ${symbols.success} ${theme.success('Up to date')}`, ); } - } catch (error) { + } catch (_error) { // If version check fails, show at least the current version try { const nupst = this.daemon.getNupstSnmp().getNupst(); - const version = nupst.getVersion(); - logger.log(''); - logger.log(`${theme.dim('NUPST')} ${theme.dim('v' + version)}`); + if (nupst) { + const version = nupst.getVersion(); + logger.log(''); + logger.log(`${theme.dim('NUPST')} ${theme.dim('v' + version)}`); + } } catch (_innerError) { // Silently fail if we can't even get the version }