fix(cli(ups-handler), systemd): add type guards and null checks for UPS configs; improve SNMP handling and prompts; guard version display

This commit is contained in:
2026-01-29 17:07:57 +00:00
parent 91fe5f7ae6
commit c7786e9626
4 changed files with 67 additions and 29 deletions

View File

@@ -1,5 +1,15 @@
# Changelog # 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) ## 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 Centralize timeouts/constants, add CLI prompt helpers, and introduce webhook/script actions with safety and SNMP refactors

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@serve.zone/nupst', 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' description: 'Network UPS Shutdown Tool - Monitor SNMP-enabled UPS devices and orchestrate graceful system shutdowns during power emergencies'
} }

View File

@@ -467,18 +467,27 @@ export class UpsHandler {
* @param config Current configuration or individual UPS configuration * @param config Current configuration or individual UPS configuration
*/ */
private displayTestConfig(config: IUpsConfig | INupstConfig): void { private displayTestConfig(config: IUpsConfig | INupstConfig): void {
// Check if this is a UPS device or full configuration // Type guard: IUpsConfig has 'id' and 'name' at root level, INupstConfig doesn't
const isUpsConfig = config.snmp; const isUpsConfig = 'id' in config && 'name' in config;
const snmpConfig = isUpsConfig ? config.snmp : config.snmp || {};
const checkInterval = config.checkInterval || 30000;
// Get UPS name and ID if available // Get SNMP config and other values based on config type
const upsName = config.name ? config.name : 'Default UPS'; const snmpConfig: ISnmpConfig | undefined = isUpsConfig
const upsId = config.id ? config.id : 'default'; ? (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; const boxWidth = 45;
logger.logBoxTitle(`Testing Configuration: ${upsName}`, boxWidth); logger.logBoxTitle(`Testing Configuration: ${upsName}`, boxWidth);
logger.logBoxLine(`UPS ID: ${upsId}`); logger.logBoxLine(`UPS ID: ${upsId}`);
if (!snmpConfig) {
logger.logBoxLine('SNMP Settings: Not configured');
logger.logBoxEnd();
return;
}
logger.logBoxLine('SNMP Settings:'); logger.logBoxLine('SNMP Settings:');
logger.logBoxLine(` Host: ${snmpConfig.host}`); logger.logBoxLine(` Host: ${snmpConfig.host}`);
logger.logBoxLine(` Port: ${snmpConfig.port}`); logger.logBoxLine(` Port: ${snmpConfig.port}`);
@@ -514,9 +523,10 @@ export class UpsHandler {
logger.logBoxLine(` Battery Runtime: ${snmpConfig.customOIDs.BATTERY_RUNTIME || 'Not set'}`); logger.logBoxLine(` Battery Runtime: ${snmpConfig.customOIDs.BATTERY_RUNTIME || 'Not set'}`);
} }
// Show group assignments if this is a UPS config // 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( 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 * @param config Current UPS configuration or legacy config
*/ */
private async testConnection(config: IUpsConfig | INupstConfig): Promise<void> { private async testConnection(config: IUpsConfig | INupstConfig): Promise<void> {
const upsId = config.id || 'default'; // Type guard: IUpsConfig has 'id' and 'name' at root level
const upsName = config.name || 'Default UPS'; 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})...`); logger.log(`\nTesting connection to UPS: ${upsName} (${upsId})...`);
try { try {
// Create a test config with a short timeout // Get SNMP config based on config type
const snmpConfig = config.snmp ? config.snmp : config.snmp; 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, ...snmpConfig,
timeout: Math.min(snmpConfig.timeout, 10000), // Use at most 10 seconds for testing timeout: Math.min(snmpConfig.timeout, 10000), // Use at most 10 seconds for testing
}; };
@@ -675,17 +693,17 @@ export class UpsHandler {
if (secLevel === 1) { if (secLevel === 1) {
snmpConfig.securityLevel = 'noAuthNoPriv'; snmpConfig.securityLevel = 'noAuthNoPriv';
// No auth, no priv - clear out authentication and privacy settings // No auth, no priv - clear out authentication and privacy settings
snmpConfig.authProtocol = ''; snmpConfig.authProtocol = undefined;
snmpConfig.authKey = ''; snmpConfig.authKey = undefined;
snmpConfig.privProtocol = ''; snmpConfig.privProtocol = undefined;
snmpConfig.privKey = ''; snmpConfig.privKey = undefined;
// Set appropriate timeout for security level // Set appropriate timeout for security level
snmpConfig.timeout = 5000; // 5 seconds for basic security snmpConfig.timeout = 5000; // 5 seconds for basic security
} else if (secLevel === 2) { } else if (secLevel === 2) {
snmpConfig.securityLevel = 'authNoPriv'; snmpConfig.securityLevel = 'authNoPriv';
// Auth, no priv - clear out privacy settings // Auth, no priv - clear out privacy settings
snmpConfig.privProtocol = ''; snmpConfig.privProtocol = undefined;
snmpConfig.privKey = ''; snmpConfig.privKey = undefined;
// Set appropriate timeout for security level // Set appropriate timeout for security level
snmpConfig.timeout = 10000; // 10 seconds for authentication snmpConfig.timeout = 10000; // 10 seconds for authentication
} else { } else {
@@ -825,16 +843,21 @@ export class UpsHandler {
logger.info('Enter custom OIDs for your UPS:'); logger.info('Enter custom OIDs for your UPS:');
logger.dim('(Leave blank to use standard RFC 1628 OIDs as fallback)'); 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 powerStatusOID = await prompt('Power Status OID: ');
const batteryCapacityOID = await prompt('Battery Capacity OID: '); const batteryCapacityOID = await prompt('Battery Capacity OID: ');
const batteryRuntimeOID = await prompt('Battery Runtime 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 = { snmpConfig.customOIDs = {
POWER_STATUS: powerStatusOID.trim(), POWER_STATUS: powerStatusOID.trim(),
BATTERY_CAPACITY: batteryCapacityOID.trim(), BATTERY_CAPACITY: batteryCapacityOID.trim(),
BATTERY_RUNTIME: batteryRuntimeOID.trim(), BATTERY_RUNTIME: batteryRuntimeOID.trim(),
OUTPUT_LOAD: '',
OUTPUT_POWER: '',
OUTPUT_VOLTAGE: '',
OUTPUT_CURRENT: '',
}; };
} }
} }

View File

@@ -142,11 +142,14 @@ WantedBy=multi-user.target
private async displayVersionInfo(): Promise<void> { private async displayVersionInfo(): Promise<void> {
try { try {
const nupst = this.daemon.getNupstSnmp().getNupst(); const nupst = this.daemon.getNupstSnmp().getNupst();
if (!nupst) {
return;
}
const version = nupst.getVersion(); const version = nupst.getVersion();
// Check for updates // Check for updates
const updateAvailable = await nupst.checkForUpdates(); const updateAvailable = await nupst.checkForUpdates();
// Display version info // Display version info
if (updateAvailable) { if (updateAvailable) {
const updateStatus = nupst.getUpdateStatus(); 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')}`, `${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 // If version check fails, show at least the current version
try { try {
const nupst = this.daemon.getNupstSnmp().getNupst(); const nupst = this.daemon.getNupstSnmp().getNupst();
const version = nupst.getVersion(); if (nupst) {
logger.log(''); const version = nupst.getVersion();
logger.log(`${theme.dim('NUPST')} ${theme.dim('v' + version)}`); logger.log('');
logger.log(`${theme.dim('NUPST')} ${theme.dim('v' + version)}`);
}
} catch (_innerError) { } catch (_innerError) {
// Silently fail if we can't even get the version // Silently fail if we can't even get the version
} }