fix(core): tidy formatting and minor fixes across CLI, SNMP, HTTP server, migrations and packaging

This commit is contained in:
2026-01-29 17:10:17 +00:00
parent fda072d15e
commit ff2dc00f31
31 changed files with 693 additions and 362 deletions

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@serve.zone/nupst',
version: '5.2.1',
version: '5.2.2',
description: 'Network UPS Shutdown Tool - Monitor SNMP-enabled UPS devices and orchestrate graceful system shutdowns during power emergencies'
}

View File

@@ -26,7 +26,11 @@ export class ScriptAction extends Action {
async execute(context: IActionContext): Promise<void> {
// Check if we should execute based on trigger mode
if (!this.shouldExecute(context)) {
logger.info(`Script action skipped (trigger mode: ${this.config.triggerMode || 'powerChangesAndThresholds'})`);
logger.info(
`Script action skipped (trigger mode: ${
this.config.triggerMode || 'powerChangesAndThresholds'
})`,
);
return;
}

View File

@@ -35,7 +35,9 @@ export class ShutdownAction extends Action {
// CRITICAL SAFETY CHECK: Shutdown should NEVER trigger unless UPS is on battery
// A low battery while on grid power is not an emergency (the battery is charging)
if (context.powerStatus !== 'onBattery') {
logger.info(`Shutdown action skipped: UPS is not on battery (status: ${context.powerStatus})`);
logger.info(
`Shutdown action skipped: UPS is not on battery (status: ${context.powerStatus})`,
);
return false;
}
@@ -54,7 +56,9 @@ export class ShutdownAction extends Action {
if (context.triggerReason === 'powerStatusChange') {
// 'onlyThresholds' mode ignores power status changes
if (mode === 'onlyThresholds') {
logger.info('Shutdown action skipped: triggerMode is onlyThresholds, ignoring power change');
logger.info(
'Shutdown action skipped: triggerMode is onlyThresholds, ignoring power change',
);
return false;
}
@@ -71,15 +75,22 @@ export class ShutdownAction extends Action {
// the daemon for testing, or the UPS may have been on battery for a while.
// Only trigger if mode explicitly includes power changes.
if (prev === 'unknown') {
if (mode === 'onlyPowerChanges' || mode === 'powerChangesAndThresholds' || mode === 'anyChange') {
logger.info('Shutdown action triggered: UPS on battery at daemon startup (unknown → onBattery)');
if (
mode === 'onlyPowerChanges' || mode === 'powerChangesAndThresholds' ||
mode === 'anyChange'
) {
logger.info(
'Shutdown action triggered: UPS on battery at daemon startup (unknown → onBattery)',
);
return true;
}
return false;
}
// Other transitions (e.g., onBattery → onBattery) should not trigger
logger.info(`Shutdown action skipped: non-emergency transition (${prev}${context.powerStatus})`);
logger.info(
`Shutdown action skipped: non-emergency transition (${prev}${context.powerStatus})`,
);
return false;
}
@@ -98,7 +109,11 @@ export class ShutdownAction extends Action {
async execute(context: IActionContext): Promise<void> {
// Check if we should execute based on trigger mode and thresholds
if (!this.shouldExecute(context)) {
logger.info(`Shutdown action skipped (trigger mode: ${this.config.triggerMode || 'powerChangesAndThresholds'})`);
logger.info(
`Shutdown action skipped (trigger mode: ${
this.config.triggerMode || 'powerChangesAndThresholds'
})`,
);
return;
}

View File

@@ -46,7 +46,11 @@ export class WebhookAction extends Action {
async execute(context: IActionContext): Promise<void> {
// Check if we should execute based on trigger mode
if (!this.shouldExecute(context)) {
logger.info(`Webhook action skipped (trigger mode: ${this.config.triggerMode || 'powerChangesAndThresholds'})`);
logger.info(
`Webhook action skipped (trigger mode: ${
this.config.triggerMode || 'powerChangesAndThresholds'
})`,
);
return;
}
@@ -109,7 +113,7 @@ export class WebhookAction extends Action {
url.searchParams.append('powerStatus', payload.powerStatus);
url.searchParams.append('batteryCapacity', String(payload.batteryCapacity));
url.searchParams.append('batteryRuntime', String(payload.batteryRuntime));
url.searchParams.append('triggerReason', payload.triggerReason);
url.searchParams.append('timestamp', String(payload.timestamp));
}

187
ts/cli.ts
View File

@@ -1,7 +1,7 @@
import { execSync } from 'node:child_process';
import { Nupst } from './nupst.ts';
import { logger, type ITableColumn } from './logger.ts';
import { theme, symbols } from './colors.ts';
import { type ITableColumn, logger } from './logger.ts';
import { symbols, theme } from './colors.ts';
/**
* Class for handling CLI commands
@@ -287,10 +287,15 @@ export class NupstCli {
try {
await this.nupst.getDaemon().loadConfig();
} catch (_error) {
logger.logBox('Configuration Error', [
'No configuration found.',
"Please run 'nupst ups add' first to create a configuration.",
], 50, 'error');
logger.logBox(
'Configuration Error',
[
'No configuration found.',
"Please run 'nupst ups add' first to create a configuration.",
],
50,
'error',
);
return;
}
@@ -300,17 +305,22 @@ export class NupstCli {
// Check if multi-UPS config
if (config.upsDevices && Array.isArray(config.upsDevices)) {
// === Multi-UPS Configuration ===
// Overview Box
logger.log('');
logger.logBox('NUPST Configuration', [
`UPS Devices: ${theme.highlight(String(config.upsDevices.length))}`,
`Groups: ${theme.highlight(String(config.groups ? config.groups.length : 0))}`,
`Check Interval: ${theme.info(String(config.checkInterval / 1000))} seconds`,
'',
theme.dim('Configuration File:'),
` ${theme.path('/etc/nupst/config.json')}`,
], 60, 'info');
logger.logBox(
'NUPST Configuration',
[
`UPS Devices: ${theme.highlight(String(config.upsDevices.length))}`,
`Groups: ${theme.highlight(String(config.groups ? config.groups.length : 0))}`,
`Check Interval: ${theme.info(String(config.checkInterval / 1000))} seconds`,
'',
theme.dim('Configuration File:'),
` ${theme.path('/etc/nupst/config.json')}`,
],
60,
'info',
);
// HTTP Server Status (if configured)
if (config.httpServer) {
@@ -319,17 +329,24 @@ export class NupstCli {
: theme.dim('Disabled');
logger.log('');
logger.logBox('HTTP Server', [
`Status: ${serverStatus}`,
...(config.httpServer.enabled ? [
`Port: ${theme.highlight(String(config.httpServer.port))}`,
`Path: ${theme.highlight(config.httpServer.path)}`,
`Auth Token: ${theme.dim('***' + config.httpServer.authToken.slice(-4))}`,
'',
theme.dim('Usage:'),
` curl -H "Authorization: Bearer TOKEN" http://localhost:${config.httpServer.port}${config.httpServer.path}`,
] : []),
], 70, config.httpServer.enabled ? 'success' : 'default');
logger.logBox(
'HTTP Server',
[
`Status: ${serverStatus}`,
...(config.httpServer.enabled
? [
`Port: ${theme.highlight(String(config.httpServer.port))}`,
`Path: ${theme.highlight(config.httpServer.path)}`,
`Auth Token: ${theme.dim('***' + config.httpServer.authToken.slice(-4))}`,
'',
theme.dim('Usage:'),
` curl -H "Authorization: Bearer TOKEN" http://localhost:${config.httpServer.port}${config.httpServer.path}`,
]
: []),
],
70,
config.httpServer.enabled ? 'success' : 'default',
);
}
// UPS Devices Table
@@ -369,8 +386,8 @@ export class NupstCli {
id: theme.dim(group.id),
mode: group.mode,
upsCount: String(upsInGroup.length),
ups: upsInGroup.length > 0
? upsInGroup.map((ups) => ups.name).join(', ')
ups: upsInGroup.length > 0
? upsInGroup.map((ups) => ups.name).join(', ')
: theme.dim('None'),
description: group.description || theme.dim('—'),
};
@@ -392,62 +409,68 @@ export class NupstCli {
}
} else {
// === Legacy Single UPS Configuration ===
if (!config.snmp) {
logger.logBox('Configuration Error', [
'Error: Legacy configuration missing SNMP settings',
], 60, 'error');
logger.logBox(
'Configuration Error',
[
'Error: Legacy configuration missing SNMP settings',
],
60,
'error',
);
return;
}
logger.log('');
logger.logBox('NUPST Configuration (Legacy)', [
theme.warning('Legacy single-UPS configuration format'),
'',
theme.dim('SNMP Settings:'),
` Host: ${theme.info(config.snmp.host)}`,
` Port: ${theme.info(String(config.snmp.port))}`,
` Version: ${config.snmp.version}`,
` UPS Model: ${config.snmp.upsModel || 'cyberpower'}`,
...(config.snmp.version === 1 || config.snmp.version === 2
? [` Community: ${config.snmp.community}`]
: []
),
...(config.snmp.version === 3
? [
logger.logBox(
'NUPST Configuration (Legacy)',
[
theme.warning('Legacy single-UPS configuration format'),
'',
theme.dim('SNMP Settings:'),
` Host: ${theme.info(config.snmp.host)}`,
` Port: ${theme.info(String(config.snmp.port))}`,
` Version: ${config.snmp.version}`,
` UPS Model: ${config.snmp.upsModel || 'cyberpower'}`,
...(config.snmp.version === 1 || config.snmp.version === 2
? [` Community: ${config.snmp.community}`]
: []),
...(config.snmp.version === 3
? [
` Security Level: ${config.snmp.securityLevel}`,
` Username: ${config.snmp.username}`,
...(config.snmp.securityLevel === 'authNoPriv' || config.snmp.securityLevel === 'authPriv'
...(config.snmp.securityLevel === 'authNoPriv' ||
config.snmp.securityLevel === 'authPriv'
? [` Auth Protocol: ${config.snmp.authProtocol || 'None'}`]
: []
),
: []),
...(config.snmp.securityLevel === 'authPriv'
? [` Privacy Protocol: ${config.snmp.privProtocol || 'None'}`]
: []
),
: []),
` Timeout: ${config.snmp.timeout / 1000} seconds`,
]
: []
),
...(config.snmp.upsModel === 'custom' && config.snmp.customOIDs
? [
: []),
...(config.snmp.upsModel === 'custom' && config.snmp.customOIDs
? [
theme.dim('Custom OIDs:'),
` Power Status: ${config.snmp.customOIDs.POWER_STATUS || 'Not set'}`,
` Battery Capacity: ${config.snmp.customOIDs.BATTERY_CAPACITY || 'Not set'}`,
` Battery Runtime: ${config.snmp.customOIDs.BATTERY_RUNTIME || 'Not set'}`,
]
: []
),
'',
` Check Interval: ${config.checkInterval / 1000} seconds`,
'',
theme.dim('Configuration File:'),
` ${theme.path('/etc/nupst/config.json')}`,
'',
theme.warning('Note: Using legacy single-UPS configuration format.'),
`Consider using ${theme.command('nupst ups add')} to migrate to multi-UPS format.`,
], 70, 'warning');
: []),
'',
` Check Interval: ${config.checkInterval / 1000} seconds`,
'',
theme.dim('Configuration File:'),
` ${theme.path('/etc/nupst/config.json')}`,
'',
theme.warning('Note: Using legacy single-UPS configuration format.'),
`Consider using ${theme.command('nupst ups add')} to migrate to multi-UPS format.`,
],
70,
'warning',
);
}
// Service Status
@@ -458,10 +481,15 @@ export class NupstCli {
execSync('systemctl is-enabled nupst.service || true').toString().trim() === 'enabled';
logger.log('');
logger.logBox('Service Status', [
`Active: ${isActive ? theme.success('Yes') : theme.dim('No')}`,
`Enabled: ${isEnabled ? theme.success('Yes') : theme.dim('No')}`,
], 50, isActive ? 'success' : 'default');
logger.logBox(
'Service Status',
[
`Active: ${isActive ? theme.success('Yes') : theme.dim('No')}`,
`Enabled: ${isEnabled ? theme.success('Yes') : theme.dim('No')}`,
],
50,
isActive ? 'success' : 'default',
);
logger.log('');
} catch (_error) {
// Ignore errors checking service status
@@ -514,8 +542,16 @@ export class NupstCli {
// Service subcommands
logger.log(theme.info('Service Subcommands:'));
this.printCommand('nupst service enable', 'Install and enable systemd service', theme.dim('(requires root)'));
this.printCommand('nupst service disable', 'Stop and disable systemd service', theme.dim('(requires root)'));
this.printCommand(
'nupst service enable',
'Install and enable systemd service',
theme.dim('(requires root)'),
);
this.printCommand(
'nupst service disable',
'Stop and disable systemd service',
theme.dim('(requires root)'),
);
this.printCommand('nupst service start', 'Start the systemd service');
this.printCommand('nupst service stop', 'Stop the systemd service');
this.printCommand('nupst service restart', 'Restart the systemd service');
@@ -545,7 +581,10 @@ export class NupstCli {
logger.log(theme.info('Action Subcommands:'));
this.printCommand('nupst action add <target-id>', 'Add a new action to a UPS or group');
this.printCommand('nupst action remove <target-id> <index>', 'Remove an action by index');
this.printCommand('nupst action list [target-id]', 'List all actions (optionally for specific target)');
this.printCommand(
'nupst action list [target-id]',
'List all actions (optionally for specific target)',
);
console.log('');
// Feature subcommands

View File

@@ -1,9 +1,9 @@
import process from 'node:process';
import { Nupst } from '../nupst.ts';
import { logger, type ITableColumn } from '../logger.ts';
import { theme, symbols } from '../colors.ts';
import { type ITableColumn, logger } from '../logger.ts';
import { symbols, theme } from '../colors.ts';
import type { IActionConfig } from '../actions/base-action.ts';
import type { IUpsConfig, IGroupConfig } from '../daemon.ts';
import type { IGroupConfig, IUpsConfig } from '../daemon.ts';
import * as helpers from '../helpers/index.ts';
/**
@@ -48,7 +48,9 @@ export class ActionHandler {
if (!ups && !group) {
logger.error(`UPS or Group with ID '${targetId}' not found`);
logger.log('');
logger.log(` ${theme.dim('List available UPS devices:')} ${theme.command('nupst ups list')}`);
logger.log(
` ${theme.dim('List available UPS devices:')} ${theme.command('nupst ups list')}`,
);
logger.log(` ${theme.dim('List available groups:')} ${theme.command('nupst group list')}`);
logger.log('');
process.exit(1);
@@ -90,12 +92,16 @@ export class ActionHandler {
// Trigger mode
logger.log('');
logger.log(` ${theme.dim('Trigger mode:')}`);
logger.log(` ${theme.dim('1)')} onlyPowerChanges - Trigger only when power status changes`);
logger.log(
` ${theme.dim('1)')} onlyPowerChanges - Trigger only when power status changes`,
);
logger.log(
` ${theme.dim('2)')} onlyThresholds - Trigger only when thresholds are violated`,
);
logger.log(
` ${theme.dim('3)')} powerChangesAndThresholds - Trigger on power change AND thresholds`,
` ${
theme.dim('3)')
} powerChangesAndThresholds - Trigger on power change AND thresholds`,
);
logger.log(` ${theme.dim('4)')} anyChange - Trigger on any status change`);
const triggerChoice = await prompt(` ${theme.dim('Choice')} ${theme.dim('[2]:')} `);
@@ -158,7 +164,9 @@ export class ActionHandler {
if (!targetId || !actionIndexStr) {
logger.error('Target ID and action index are required');
logger.log(
` ${theme.dim('Usage:')} ${theme.command('nupst action remove <ups-id|group-id> <action-index>')}`,
` ${theme.dim('Usage:')} ${
theme.command('nupst action remove <ups-id|group-id> <action-index>')
}`,
);
logger.log('');
logger.log(` ${theme.dim('List actions:')} ${theme.command('nupst action list')}`);
@@ -182,7 +190,9 @@ export class ActionHandler {
if (!ups && !group) {
logger.error(`UPS or Group with ID '${targetId}' not found`);
logger.log('');
logger.log(` ${theme.dim('List available UPS devices:')} ${theme.command('nupst ups list')}`);
logger.log(
` ${theme.dim('List available UPS devices:')} ${theme.command('nupst ups list')}`,
);
logger.log(` ${theme.dim('List available groups:')} ${theme.command('nupst group list')}`);
logger.log('');
process.exit(1);
@@ -200,7 +210,9 @@ export class ActionHandler {
if (actionIndex >= target!.actions.length) {
logger.error(
`Invalid action index. ${targetType} '${targetName}' has ${target!.actions.length} action(s) (index 0-${target!.actions.length - 1})`,
`Invalid action index. ${targetType} '${targetName}' has ${
target!.actions.length
} action(s) (index 0-${target!.actions.length - 1})`,
);
logger.log('');
logger.log(
@@ -220,7 +232,9 @@ export class ActionHandler {
logger.log(` ${theme.dim('Type:')} ${removedAction.type}`);
if (removedAction.thresholds) {
logger.log(
` ${theme.dim('Thresholds:')} Battery: ${removedAction.thresholds.battery}%, Runtime: ${removedAction.thresholds.runtime}min`,
` ${
theme.dim('Thresholds:')
} Battery: ${removedAction.thresholds.battery}%, Runtime: ${removedAction.thresholds.runtime}min`,
);
}
logger.log(` ${theme.dim('Changes saved and will be applied automatically')}`);
@@ -248,8 +262,12 @@ export class ActionHandler {
if (!ups && !group) {
logger.error(`UPS or Group with ID '${targetId}' not found`);
logger.log('');
logger.log(` ${theme.dim('List available UPS devices:')} ${theme.command('nupst ups list')}`);
logger.log(` ${theme.dim('List available groups:')} ${theme.command('nupst group list')}`);
logger.log(
` ${theme.dim('List available UPS devices:')} ${theme.command('nupst ups list')}`,
);
logger.log(
` ${theme.dim('List available groups:')} ${theme.command('nupst group list')}`,
);
logger.log('');
process.exit(1);
}
@@ -287,7 +305,9 @@ export class ActionHandler {
logger.log(` ${theme.dim('No actions configured')}`);
logger.log('');
logger.log(
` ${theme.dim('Add an action:')} ${theme.command('nupst action add <ups-id|group-id>')}`,
` ${theme.dim('Add an action:')} ${
theme.command('nupst action add <ups-id|group-id>')
}`,
);
logger.log('');
}
@@ -308,7 +328,9 @@ export class ActionHandler {
targetType: 'UPS' | 'Group',
): void {
logger.log(
`${symbols.info} ${targetType} ${theme.highlight(target.name)} ${theme.dim(`(${target.id})`)}`,
`${symbols.info} ${targetType} ${theme.highlight(target.name)} ${
theme.dim(`(${target.id})`)
}`,
);
logger.log('');

View File

@@ -29,7 +29,9 @@ export class FeatureHandler {
await this.runHttpServerConfig(prompt);
});
} catch (error) {
logger.error(`HTTP Server config error: ${error instanceof Error ? error.message : String(error)}`);
logger.error(
`HTTP Server config error: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
@@ -149,7 +151,9 @@ export class FeatureHandler {
logger.logBoxLine(`Auth Token: ${theme.warning(authToken)}`);
logger.logBoxLine('');
logger.logBoxLine(theme.dim('Usage examples:'));
logger.logBoxLine(` curl -H "Authorization: Bearer ${authToken}" http://localhost:${port}${finalPath}`);
logger.logBoxLine(
` curl -H "Authorization: Bearer ${authToken}" http://localhost:${port}${finalPath}`,
);
logger.logBoxLine(` curl "http://localhost:${port}${finalPath}?token=${authToken}"`);
logger.logBoxEnd();
logger.log('');
@@ -165,7 +169,8 @@ export class FeatureHandler {
*/
private async restartServiceIfRunning(): Promise<void> {
try {
const isActive = execSync('systemctl is-active nupst.service || true').toString().trim() === 'active';
const isActive =
execSync('systemctl is-active nupst.service || true').toString().trim() === 'active';
if (isActive) {
logger.log('');

View File

@@ -1,9 +1,9 @@
import process from 'node:process';
import { Nupst } from '../nupst.ts';
import { logger, type ITableColumn } from '../logger.ts';
import { type ITableColumn, logger } from '../logger.ts';
import { theme } from '../colors.ts';
import * as helpers from '../helpers/index.ts';
import type { IGroupConfig, IUpsConfig, INupstConfig } from '../daemon.ts';
import type { IGroupConfig, INupstConfig, IUpsConfig } from '../daemon.ts';
/**
* Class for handling group-related CLI commands
@@ -29,10 +29,15 @@ export class GroupHandler {
try {
await this.nupst.getDaemon().loadConfig();
} catch (error) {
logger.logBox('Configuration Error', [
'No configuration found.',
"Please run 'nupst ups add' first to create a configuration.",
], 50, 'error');
logger.logBox(
'Configuration Error',
[
'No configuration found.',
"Please run 'nupst ups add' first to create a configuration.",
],
50,
'error',
);
return;
}
@@ -41,21 +46,35 @@ export class GroupHandler {
// Check if multi-UPS config
if (!config.groups || !Array.isArray(config.groups)) {
logger.logBox('UPS Groups', [
'No groups configured.',
'',
`${theme.dim('Run')} ${theme.command('nupst group add')} ${theme.dim('to add a group')}`,
], 50, 'info');
logger.logBox(
'UPS Groups',
[
'No groups configured.',
'',
`${theme.dim('Run')} ${theme.command('nupst group add')} ${
theme.dim('to add a group')
}`,
],
50,
'info',
);
return;
}
// Display group list with modern table
if (config.groups.length === 0) {
logger.logBox('UPS Groups', [
'No UPS groups configured.',
'',
`${theme.dim('Run')} ${theme.command('nupst group add')} ${theme.dim('to add a group')}`,
], 60, 'info');
logger.logBox(
'UPS Groups',
[
'No UPS groups configured.',
'',
`${theme.dim('Run')} ${theme.command('nupst group add')} ${
theme.dim('to add a group')
}`,
],
60,
'info',
);
return;
}

View File

@@ -147,8 +147,12 @@ export class ServiceHandler {
const latestVersion = release.tag_name; // e.g., "v4.0.7"
// Normalize versions for comparison (ensure both have "v" prefix)
const normalizedCurrent = currentVersion.startsWith('v') ? currentVersion : `v${currentVersion}`;
const normalizedLatest = latestVersion.startsWith('v') ? latestVersion : `v${latestVersion}`;
const normalizedCurrent = currentVersion.startsWith('v')
? currentVersion
: `v${currentVersion}`;
const normalizedLatest = latestVersion.startsWith('v')
? latestVersion
: `v${latestVersion}`;
logger.dim(`Current version: ${normalizedCurrent}`);
logger.dim(`Latest version: ${normalizedLatest}`);

View File

@@ -1,10 +1,10 @@
import process from 'node:process';
import { execSync } from 'node:child_process';
import { Nupst } from '../nupst.ts';
import { logger, type ITableColumn } from '../logger.ts';
import { type ITableColumn, logger } from '../logger.ts';
import { theme } from '../colors.ts';
import * as helpers from '../helpers/index.ts';
import type { ISnmpConfig, TUpsModel, IUpsStatus as ISnmpUpsStatus } from '../snmp/types.ts';
import type { ISnmpConfig, IUpsStatus as ISnmpUpsStatus, TUpsModel } from '../snmp/types.ts';
import type { INupstConfig, IUpsConfig, IUpsStatus } from '../daemon.ts';
import type { IActionConfig } from '../actions/base-action.ts';
@@ -66,10 +66,10 @@ export class UpsHandler {
checkInterval: config.checkInterval,
upsDevices: [{
id: 'default',
name: 'Default UPS',
snmp: config.snmp,
groups: [],
actions: [],
name: 'Default UPS',
snmp: config.snmp,
groups: [],
actions: [],
}],
groups: [],
};
@@ -123,7 +123,7 @@ export class UpsHandler {
await groupHandler.assignUpsToGroups(newUps, config.groups, prompt);
}
// Gather action settings
// Gather action settings
await this.gatherActionSettings(newUps.actions, prompt);
// Add the new UPS to the config
@@ -343,10 +343,15 @@ export class UpsHandler {
try {
await this.nupst.getDaemon().loadConfig();
} catch (error) {
logger.logBox('Configuration Error', [
'No configuration found.',
"Please run 'nupst ups add' first to create a configuration.",
], 50, 'error');
logger.logBox(
'Configuration Error',
[
'No configuration found.',
"Please run 'nupst ups add' first to create a configuration.",
],
50,
'error',
);
return;
}
@@ -356,31 +361,38 @@ export class UpsHandler {
// Check if multi-UPS config
if (!config.upsDevices || !Array.isArray(config.upsDevices)) {
// Legacy single UPS configuration
logger.logBox('UPS Devices', [
'Legacy single-UPS configuration detected.',
'',
...(!config.snmp
? ['Error: Configuration missing SNMP settings']
: [
'Default UPS:',
` Host: ${config.snmp.host}:${config.snmp.port}`,
` Model: ${config.snmp.upsModel || 'cyberpower'}`,
'',
'Use "nupst ups add" to add more UPS devices and migrate',
'to the multi-UPS configuration format.',
]
),
], 60, 'warning');
logger.logBox(
'UPS Devices',
[
'Legacy single-UPS configuration detected.',
'',
...(!config.snmp ? ['Error: Configuration missing SNMP settings'] : [
'Default UPS:',
` Host: ${config.snmp.host}:${config.snmp.port}`,
` Model: ${config.snmp.upsModel || 'cyberpower'}`,
'',
'Use "nupst ups add" to add more UPS devices and migrate',
'to the multi-UPS configuration format.',
]),
],
60,
'warning',
);
return;
}
// Display UPS list with modern table
if (config.upsDevices.length === 0) {
logger.logBox('UPS Devices', [
'No UPS devices configured.',
'',
`${theme.dim('Run')} ${theme.command('nupst ups add')} ${theme.dim('to add a device')}`,
], 60, 'info');
logger.logBox(
'UPS Devices',
[
'No UPS devices configured.',
'',
`${theme.dim('Run')} ${theme.command('nupst ups add')} ${theme.dim('to add a device')}`,
],
60,
'info',
);
return;
}
@@ -569,8 +581,6 @@ export class UpsHandler {
logger.logBoxLine(` Battery Capacity: ${status.batteryCapacity}%`);
logger.logBoxLine(` Runtime Remaining: ${status.batteryRuntime} minutes`);
logger.logBoxEnd();
} catch (error) {
const errorBoxWidth = 45;
logger.logBoxTitle(`Connection Failed: ${upsName}`, errorBoxWidth);
@@ -959,7 +969,7 @@ export class UpsHandler {
logger.dim(' 4) Any change (every ~30s check)');
const triggerInput = await prompt('Select trigger mode [1]: ');
const triggerValue = parseInt(triggerInput, 10) || 1;
switch (triggerValue) {
case 2:
action.triggerMode = 'onlyPowerChanges';
@@ -975,11 +985,16 @@ export class UpsHandler {
}
// Configure thresholds if needed for onlyThresholds or powerChangesAndThresholds modes
if (action.triggerMode === 'onlyThresholds' || action.triggerMode === 'powerChangesAndThresholds') {
if (
action.triggerMode === 'onlyThresholds' ||
action.triggerMode === 'powerChangesAndThresholds'
) {
logger.log('');
logger.info('Action Thresholds:');
logger.dim('Action will trigger when battery or runtime falls below these values (while on battery)');
logger.dim(
'Action will trigger when battery or runtime falls below these values (while on battery)',
);
const batteryInput = await prompt('Battery threshold percentage [60]: ');
const battery = parseInt(batteryInput, 10);
const batteryThreshold = (batteryInput.trim() && !isNaN(battery)) ? battery : 60;
@@ -995,7 +1010,11 @@ export class UpsHandler {
}
actions.push(action as IActionConfig);
logger.success(`${action.type!.charAt(0).toUpperCase() + action.type!.slice(1)} action added (mode: ${action.triggerMode || 'powerChangesAndThresholds'})`);
logger.success(
`${action.type!.charAt(0).toUpperCase() + action.type!.slice(1)} action added (mode: ${
action.triggerMode || 'powerChangesAndThresholds'
})`,
);
const more = await prompt('Add another action? (y/N): ');
addMore = more.toLowerCase() === 'y';
@@ -1019,7 +1038,7 @@ export class UpsHandler {
logger.logBoxLine(`SNMP Host: ${ups.snmp.host}:${ups.snmp.port}`);
logger.logBoxLine(`SNMP Version: ${ups.snmp.version}`);
logger.logBoxLine(`UPS Model: ${ups.snmp.upsModel}`);
if (ups.groups && ups.groups.length > 0) {
logger.logBoxLine(`Groups: ${ups.groups.join(', ')}`);
} else {

View File

@@ -5,13 +5,13 @@ import { exec, execFile } from 'node:child_process';
import { promisify } from 'node:util';
import { NupstSnmp } from './snmp/manager.ts';
import type { ISnmpConfig, IUpsStatus as ISnmpUpsStatus } from './snmp/types.ts';
import { logger, type ITableColumn } from './logger.ts';
import { type ITableColumn, logger } from './logger.ts';
import { MigrationRunner } from './migrations/index.ts';
import { theme, symbols, getBatteryColor, getRuntimeColor, formatPowerStatus } from './colors.ts';
import { formatPowerStatus, getBatteryColor, getRuntimeColor, symbols, theme } from './colors.ts';
import type { IActionConfig } from './actions/base-action.ts';
import { ActionManager, type IActionContext, type TPowerStatus } from './actions/index.ts';
import { NupstHttpServer } from './http-server.ts';
import { TIMING, THRESHOLDS, UI } from './constants.ts';
import { THRESHOLDS, TIMING, UI } from './constants.ts';
const execAsync = promisify(exec);
const execFileAsync = promisify(execFile);
@@ -100,10 +100,10 @@ export interface IUpsStatus {
powerStatus: 'online' | 'onBattery' | 'unknown';
batteryCapacity: number;
batteryRuntime: number;
outputLoad: number; // Load percentage (0-100%)
outputPower: number; // Power in watts
outputVoltage: number; // Voltage in volts
outputCurrent: number; // Current in amps
outputLoad: number; // Load percentage (0-100%)
outputPower: number; // Power in watts
outputVoltage: number; // Voltage in volts
outputCurrent: number; // Current in amps
lastStatusChange: number;
lastCheckTime: number;
}
@@ -155,7 +155,7 @@ export class NupstDaemon {
],
groups: [],
checkInterval: TIMING.CHECK_INTERVAL_MS, // Check every 30 seconds
}
};
private config: INupstConfig;
private snmp: NupstSnmp;
@@ -249,7 +249,12 @@ export class NupstDaemon {
* Helper method to log configuration errors consistently
*/
private logConfigError(message: string): void {
logger.logBox('Configuration Error', [message, "Please run 'nupst setup' first to create a configuration."], 45, 'error');
logger.logBox(
'Configuration Error',
[message, "Please run 'nupst setup' first to create a configuration."],
45,
'error',
);
}
/**
@@ -311,11 +316,15 @@ export class NupstDaemon {
this.config.httpServer.port,
this.config.httpServer.path,
this.config.httpServer.authToken,
() => this.upsStatus
() => this.upsStatus,
);
this.httpServer.start();
} catch (error) {
logger.error(`Failed to start HTTP server: ${error instanceof Error ? error.message : String(error)}`);
logger.error(
`Failed to start HTTP server: ${
error instanceof Error ? error.message : String(error)
}`,
);
}
}
@@ -364,7 +373,6 @@ export class NupstDaemon {
* Log the loaded configuration settings
*/
private logConfigLoaded(): void {
logger.log('');
logger.logBoxTitle('Configuration Loaded', 70, 'success');
logger.logBoxLine(`Check Interval: ${this.config.checkInterval / 1000} seconds`);
@@ -374,8 +382,10 @@ export class NupstDaemon {
// Display UPS devices in a table
if (this.config.upsDevices && this.config.upsDevices.length > 0) {
logger.info(`UPS Devices (${this.config.upsDevices.length}):`);
const upsColumns: Array<{ header: string; key: string; align?: 'left' | 'right'; color?: (val: string) => string }> = [
const upsColumns: Array<
{ header: string; key: string; align?: 'left' | 'right'; color?: (val: string) => string }
> = [
{ header: 'Name', key: 'name', align: 'left', color: theme.highlight },
{ header: 'ID', key: 'id', align: 'left', color: theme.dim },
{ header: 'Host:Port', key: 'host', align: 'left', color: theme.info },
@@ -399,8 +409,10 @@ export class NupstDaemon {
// Display groups in a table
if (this.config.groups && this.config.groups.length > 0) {
logger.info(`Groups (${this.config.groups.length}):`);
const groupColumns: Array<{ header: string; key: string; align?: 'left' | 'right'; color?: (val: string) => string }> = [
const groupColumns: Array<
{ header: string; key: string; align?: 'left' | 'right'; color?: (val: string) => string }
> = [
{ header: 'Name', key: 'name', align: 'left', color: theme.highlight },
{ header: 'ID', key: 'id', align: 'left', color: theme.dim },
{ header: 'Mode', key: 'mode', align: 'left', color: theme.info },
@@ -538,7 +550,7 @@ export class NupstDaemon {
// Only check when on battery power
if (status.powerStatus === 'onBattery' && ups.actions && ups.actions.length > 0) {
let anyThresholdExceeded = false;
for (const actionConfig of ups.actions) {
if (actionConfig.thresholds) {
if (
@@ -575,7 +587,7 @@ export class NupstDaemon {
*/
private logAllUpsStatus(): void {
const timestamp = new Date().toISOString();
logger.log('');
logger.logBoxTitle('Periodic Status Update', 70, 'info');
logger.logBoxLine(`Timestamp: ${timestamp}`);
@@ -583,7 +595,9 @@ export class NupstDaemon {
logger.log('');
// Build table data
const columns: Array<{ header: string; key: string; align?: 'left' | 'right'; color?: (val: string) => string }> = [
const columns: Array<
{ header: string; key: string; align?: 'left' | 'right'; color?: (val: string) => string }
> = [
{ header: 'UPS Name', key: 'name', align: 'left', color: theme.highlight },
{ header: 'ID', key: 'id', align: 'left', color: theme.dim },
{ header: 'Power Status', key: 'powerStatus', align: 'left' },
@@ -595,7 +609,7 @@ export class NupstDaemon {
for (const [id, status] of this.upsStatus.entries()) {
const batteryColor = getBatteryColor(status.batteryCapacity);
const runtimeColor = getRuntimeColor(status.batteryRuntime);
rows.push({
name: status.name,
id: id,
@@ -609,10 +623,6 @@ export class NupstDaemon {
logger.log('');
}
/**
* Build action context from UPS state
* @param ups UPS configuration
@@ -796,7 +806,9 @@ export class NupstDaemon {
logger.log('');
logger.logBoxTitle('Shutdown Monitoring Active', UI.WIDE_BOX_WIDTH, 'warning');
logger.logBoxLine(`Emergency threshold: ${THRESHOLDS.EMERGENCY_RUNTIME_MINUTES} minutes runtime`);
logger.logBoxLine(
`Emergency threshold: ${THRESHOLDS.EMERGENCY_RUNTIME_MINUTES} minutes runtime`,
);
logger.logBoxLine(`Check interval: ${TIMING.SHUTDOWN_CHECK_INTERVAL_MS / 1000} seconds`);
logger.logBoxLine(`Max monitoring time: ${TIMING.MAX_SHUTDOWN_MONITORING_MS / 1000} seconds`);
logger.logBoxEnd();
@@ -808,7 +820,9 @@ export class NupstDaemon {
logger.info('Checking UPS status during shutdown...');
// Build table for UPS status during shutdown
const columns: Array<{ header: string; key: string; align?: 'left' | 'right'; color?: (val: string) => string }> = [
const columns: Array<
{ header: string; key: string; align?: 'left' | 'right'; color?: (val: string) => string }
> = [
{ header: 'UPS Name', key: 'name', align: 'left', color: theme.highlight },
{ header: 'Battery', key: 'battery', align: 'right' },
{ header: 'Runtime', key: 'runtime', align: 'right' },
@@ -828,7 +842,7 @@ export class NupstDaemon {
const runtimeColor = getRuntimeColor(status.batteryRuntime);
const isCritical = status.batteryRuntime < THRESHOLDS.EMERGENCY_RUNTIME_MINUTES;
rows.push({
name: ups.name,
battery: batteryColor(status.batteryCapacity + '%'),
@@ -848,7 +862,7 @@ export class NupstDaemon {
runtime: theme.error('N/A'),
status: theme.error('ERROR'),
});
logger.error(
`Error checking UPS ${ups.name} during shutdown: ${
upsError instanceof Error ? upsError.message : String(upsError)
@@ -991,7 +1005,9 @@ export class NupstDaemon {
let lastConfigCheck = Date.now();
logger.log('Entering idle monitoring mode...');
logger.log(`Daemon will check for config changes every ${TIMING.IDLE_CHECK_INTERVAL_MS / 1000} seconds`);
logger.log(
`Daemon will check for config changes every ${TIMING.IDLE_CHECK_INTERVAL_MS / 1000} seconds`,
);
// Start file watcher for hot-reload
this.watchConfigFile();

View File

@@ -25,7 +25,7 @@ export class NupstHttpServer {
port: number,
path: string,
authToken: string,
getUpsStatus: () => Map<string, IUpsStatus>
getUpsStatus: () => Map<string, IUpsStatus>,
) {
this.port = port;
this.path = path;
@@ -70,7 +70,7 @@ export class NupstHttpServer {
if (!this.isAuthenticated(req)) {
res.writeHead(401, {
'Content-Type': 'application/json',
'WWW-Authenticate': 'Bearer'
'WWW-Authenticate': 'Bearer',
});
res.end(JSON.stringify({ error: 'Unauthorized' }));
return;
@@ -82,7 +82,7 @@ export class NupstHttpServer {
res.writeHead(200, {
'Content-Type': 'application/json',
'Cache-Control': 'no-cache'
'Cache-Control': 'no-cache',
});
res.end(JSON.stringify(statusArray, null, 2));
} else {
@@ -95,7 +95,7 @@ export class NupstHttpServer {
logger.success(`HTTP server started on port ${this.port} at ${this.path}`);
});
this.server.on('error', (error: any) => {
this.server.on('error', (error: Error) => {
logger.error(`HTTP server error: ${error.message}`);
});
}

View File

@@ -1,4 +1,4 @@
import { theme, symbols } from './colors.ts';
import { symbols, theme } from './colors.ts';
/**
* Table column alignment options

View File

@@ -31,7 +31,9 @@ export abstract class BaseMigration {
* @param config - Raw configuration object to check (unknown schema for migrations)
* @returns True if migration should run, false otherwise
*/
abstract shouldRun(config: Record<string, unknown>): Promise<boolean>;
abstract shouldRun(
config: Record<string, unknown>,
): boolean | Promise<boolean>;
/**
* Perform the migration on the given config
@@ -39,7 +41,9 @@ export abstract class BaseMigration {
* @param config - Raw configuration object to migrate (unknown schema for migrations)
* @returns Migrated configuration object
*/
abstract migrate(config: Record<string, unknown>): Promise<Record<string, unknown>>;
abstract migrate(
config: Record<string, unknown>,
): Record<string, unknown> | Promise<Record<string, unknown>>;
/**
* Get human-readable name for this migration

View File

@@ -23,12 +23,12 @@ export class MigrationV1ToV2 extends BaseMigration {
readonly fromVersion = '1.x';
readonly toVersion = '2.0';
async shouldRun(config: any): Promise<boolean> {
shouldRun(config: Record<string, unknown>): boolean {
// V1 format has snmp field directly at root, no upsDevices or upsList
return !!config.snmp && !config.upsDevices && !config.upsList;
}
async migrate(config: any): Promise<any> {
migrate(config: Record<string, unknown>): Record<string, unknown> {
logger.info(`${this.getName()}: Converting single SNMP config to multi-UPS format...`);
const migrated = {

View File

@@ -42,15 +42,16 @@ export class MigrationV3ToV4 extends BaseMigration {
readonly fromVersion = '3.x';
readonly toVersion = '4.0';
async shouldRun(config: any): Promise<boolean> {
shouldRun(config: Record<string, unknown>): boolean {
// V3 format has upsList OR has upsDevices with flat structure (host at top level)
if (config.upsList && !config.upsDevices) {
return true; // Classic v3 with upsList
}
// Check if upsDevices exists but has flat structure (v3 format)
if (config.upsDevices && config.upsDevices.length > 0) {
const firstDevice = config.upsDevices[0];
const upsDevices = config.upsDevices as Array<Record<string, unknown>> | undefined;
if (upsDevices && upsDevices.length > 0) {
const firstDevice = upsDevices[0];
// V3 has host at top level, v4 has it nested in snmp object
return !!firstDevice.host && !firstDevice.snmp;
}
@@ -58,17 +59,17 @@ export class MigrationV3ToV4 extends BaseMigration {
return false;
}
async migrate(config: any): Promise<any> {
migrate(config: Record<string, unknown>): Record<string, unknown> {
logger.info(`${this.getName()}: Migrating v3 config to v4 format...`);
logger.dim(` - Restructuring UPS devices (flat → nested snmp config)`);
// Get devices from either upsList or upsDevices (for partially migrated configs)
const sourceDevices = config.upsList || config.upsDevices;
const sourceDevices = (config.upsList || config.upsDevices) as Array<Record<string, unknown>>;
// Transform each UPS device from v3 flat structure to v4 nested structure
const transformedDevices = sourceDevices.map((device: any) => {
const transformedDevices = sourceDevices.map((device: Record<string, unknown>) => {
// Build SNMP config object
const snmpConfig: any = {
const snmpConfig: Record<string, unknown> = {
host: device.host,
port: device.port || 161,
version: typeof device.version === 'string' ? parseInt(device.version, 10) : device.version,
@@ -112,7 +113,9 @@ export class MigrationV3ToV4 extends BaseMigration {
checkInterval: config.checkInterval || 30000,
};
logger.success(`${this.getName()}: Migration complete (${transformedDevices.length} devices transformed)`);
logger.success(
`${this.getName()}: Migration complete (${transformedDevices.length} devices transformed)`,
);
return migrated;
}
}

View File

@@ -49,7 +49,7 @@ export class MigrationV4_0ToV4_1 extends BaseMigration {
readonly fromVersion = '4.0';
readonly toVersion = '4.1';
async shouldRun(config: Record<string, unknown>): Promise<boolean> {
shouldRun(config: Record<string, unknown>): boolean {
// Run if config is version 4.0
if (config.version === '4.0') {
return true;
@@ -65,7 +65,7 @@ export class MigrationV4_0ToV4_1 extends BaseMigration {
return false;
}
async migrate(config: Record<string, unknown>): Promise<Record<string, unknown>> {
migrate(config: Record<string, unknown>): Record<string, unknown> {
logger.info(`${this.getName()}: Migrating v4.0 config to v4.1 format...`);
logger.dim(` - Moving thresholds from UPS level to action level`);
logger.dim(` - Creating default shutdown actions from existing thresholds`);
@@ -81,7 +81,9 @@ export class MigrationV4_0ToV4_1 extends BaseMigration {
};
// If device has thresholds at UPS level, convert to shutdown action
const deviceThresholds = device.thresholds as { battery: number; runtime: number } | undefined;
const deviceThresholds = device.thresholds as
| { battery: number; runtime: number }
| undefined;
if (deviceThresholds) {
migrated.actions = [
{

View File

@@ -171,8 +171,8 @@ export class Nupst implements INupstAccessor {
const response = JSON.parse(data);
if (response.tag_name) {
// Strip 'v' prefix from tag name (e.g., "v5.1.7" -> "5.1.7")
const version = response.tag_name.startsWith('v')
? response.tag_name.substring(1)
const version = response.tag_name.startsWith('v')
? response.tag_name.substring(1)
: response.tag_name;
resolve(version);
} else {

View File

@@ -197,7 +197,11 @@ export class NupstSnmp {
const levelName = Object.keys(snmp.SecurityLevel).find((key) =>
snmp.SecurityLevel[key] === user.level
);
logger.dim(`SNMPv3 user configuration: name=${user.name}, level=${levelName}, authProtocol=${user.authProtocol ? 'Set' : 'Not Set'}, privProtocol=${user.privProtocol ? 'Set' : 'Not Set'}`);
logger.dim(
`SNMPv3 user configuration: name=${user.name}, level=${levelName}, authProtocol=${
user.authProtocol ? 'Set' : 'Not Set'
}, privProtocol=${user.privProtocol ? 'Set' : 'Not Set'}`,
);
}
session = snmp.createV3Session(config.host, user, options);
@@ -259,7 +263,9 @@ export class NupstSnmp {
}
if (this.debug) {
logger.dim(`SNMP response: oid=${varbinds[0].oid}, type=${varbinds[0].type}, value=${value}`);
logger.dim(
`SNMP response: oid=${varbinds[0].oid}, type=${varbinds[0].type}, value=${value}`,
);
}
resolve(value);
@@ -496,7 +502,9 @@ export class NupstSnmp {
} catch (retryError) {
if (this.debug) {
logger.error(
`Retry failed for ${description}: ${retryError instanceof Error ? retryError.message : String(retryError)}`,
`Retry failed for ${description}: ${
retryError instanceof Error ? retryError.message : String(retryError)
}`,
);
}
}
@@ -517,7 +525,9 @@ export class NupstSnmp {
} catch (retryError) {
if (this.debug) {
logger.error(
`Retry failed for ${description}: ${retryError instanceof Error ? retryError.message : String(retryError)}`,
`Retry failed for ${description}: ${
retryError instanceof Error ? retryError.message : String(retryError)
}`,
);
}
}
@@ -556,7 +566,9 @@ export class NupstSnmp {
} catch (stdError) {
if (this.debug) {
logger.error(
`Standard OID retry failed for ${description}: ${stdError instanceof Error ? stdError.message : String(stdError)}`,
`Standard OID retry failed for ${description}: ${
stdError instanceof Error ? stdError.message : String(stdError)
}`,
);
}
}

View File

@@ -23,7 +23,7 @@ export interface IUpsStatus {
/** Output current in amps */
outputCurrent: number;
/** Raw values from SNMP responses */
raw: Record<string, any>;
raw: Record<string, unknown>;
}
/**

View File

@@ -1,10 +1,10 @@
import process from 'node:process';
import { promises as fs } from 'node:fs';
import { execSync } from 'node:child_process';
import { NupstDaemon, type IUpsConfig } from './daemon.ts';
import { type IUpsConfig, NupstDaemon } from './daemon.ts';
import { NupstSnmp } from './snmp/manager.ts';
import { logger } from './logger.ts';
import { theme, symbols, getBatteryColor, getRuntimeColor, formatPowerStatus } from './colors.ts';
import { formatPowerStatus, getBatteryColor, getRuntimeColor, symbols, theme } from './colors.ts';
/**
* Class for managing systemd service
@@ -54,7 +54,11 @@ WantedBy=multi-user.target
logger.log('');
logger.error('No configuration found');
logger.log(` ${theme.dim('Config file:')} ${configPath}`);
logger.log(` ${theme.dim('Run')} ${theme.command('nupst ups add')} ${theme.dim('to create a configuration')}`);
logger.log(
` ${theme.dim('Run')} ${theme.command('nupst ups add')} ${
theme.dim('to create a configuration')
}`,
);
logger.log('');
throw new Error('Configuration not found');
}
@@ -155,13 +159,19 @@ WantedBy=multi-user.target
const updateStatus = nupst.getUpdateStatus();
logger.log('');
logger.log(
`${theme.dim('NUPST')} ${theme.dim('v' + version)} ${symbols.warning} ${theme.statusWarning(`Update available: v${updateStatus.latestVersion}`)}`,
`${theme.dim('NUPST')} ${theme.dim('v' + version)} ${symbols.warning} ${
theme.statusWarning(`Update available: v${updateStatus.latestVersion}`)
}`,
);
logger.log(
` ${theme.dim('Run')} ${theme.command('sudo nupst update')} ${theme.dim('to upgrade')}`,
);
logger.log(` ${theme.dim('Run')} ${theme.command('sudo nupst update')} ${theme.dim('to upgrade')}`);
} else {
logger.log('');
logger.log(
`${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) {
@@ -242,9 +252,15 @@ WantedBy=multi-user.target
// Display beautiful status
logger.log('');
if (isActive) {
logger.log(`${symbols.running} ${theme.success('Service:')} ${theme.statusActive('active (running)')}`);
logger.log(
`${symbols.running} ${theme.success('Service:')} ${
theme.statusActive('active (running)')
}`,
);
} else {
logger.log(`${symbols.stopped} ${theme.dim('Service:')} ${theme.statusInactive('inactive')}`);
logger.log(
`${symbols.stopped} ${theme.dim('Service:')} ${theme.statusInactive('inactive')}`,
);
}
if (pid || memory || cpu) {
@@ -255,10 +271,11 @@ WantedBy=multi-user.target
logger.log(` ${details.join(' ')}`);
}
logger.log('');
} catch (error) {
logger.log('');
logger.log(`${symbols.stopped} ${theme.dim('Service:')} ${theme.statusInactive('not installed')}`);
logger.log(
`${symbols.stopped} ${theme.dim('Service:')} ${theme.statusInactive('not installed')}`,
);
logger.log('');
}
}
@@ -295,13 +312,13 @@ WantedBy=multi-user.target
groups: [],
actions: config.thresholds
? [
{
type: 'shutdown',
thresholds: config.thresholds,
triggerMode: 'onlyThresholds',
shutdownDelay: 5,
},
]
{
type: 'shutdown',
thresholds: config.thresholds,
triggerMode: 'onlyThresholds',
shutdownDelay: 5,
},
]
: [],
};
@@ -309,7 +326,9 @@ WantedBy=multi-user.target
} else {
logger.log('');
logger.warn('No UPS devices configured');
logger.log(` ${theme.dim('Run')} ${theme.command('nupst ups add')} ${theme.dim('to add a device')}`);
logger.log(
` ${theme.dim('Run')} ${theme.command('nupst ups add')} ${theme.dim('to add a device')}`,
);
logger.log('');
}
} catch (error) {
@@ -344,7 +363,9 @@ WantedBy=multi-user.target
}
// Display UPS name and power status
logger.log(` ${statusSymbol} ${theme.highlight(ups.name)} - ${formatPowerStatus(status.powerStatus)}`);
logger.log(
` ${statusSymbol} ${theme.highlight(ups.name)} - ${formatPowerStatus(status.powerStatus)}`,
);
// Display battery with color coding
const batteryColor = getBatteryColor(status.batteryCapacity);
@@ -352,16 +373,27 @@ WantedBy=multi-user.target
// Get threshold from actions (if any action has thresholds defined)
const actionWithThresholds = ups.actions?.find((action) => action.thresholds);
const batteryThreshold = actionWithThresholds?.thresholds?.battery;
const batterySymbol = batteryThreshold !== undefined && status.batteryCapacity >= batteryThreshold
? symbols.success
: batteryThreshold !== undefined
? symbols.warning
: '';
const batterySymbol =
batteryThreshold !== undefined && status.batteryCapacity >= batteryThreshold
? symbols.success
: batteryThreshold !== undefined
? symbols.warning
: '';
logger.log(` Battery: ${batteryColor(status.batteryCapacity + '%')} ${batterySymbol} Runtime: ${getRuntimeColor(status.batteryRuntime)(status.batteryRuntime + ' min')}`);
logger.log(
` Battery: ${batteryColor(status.batteryCapacity + '%')} ${batterySymbol} Runtime: ${
getRuntimeColor(status.batteryRuntime)(status.batteryRuntime + ' min')
}`,
);
// Display power metrics
logger.log(` Load: ${theme.highlight(status.outputLoad + '%')} Power: ${theme.highlight(status.outputPower + 'W')} Voltage: ${theme.highlight(status.outputVoltage + 'V')} Current: ${theme.highlight(status.outputCurrent + 'A')}`);
logger.log(
` Load: ${theme.highlight(status.outputLoad + '%')} Power: ${
theme.highlight(status.outputPower + 'W')
} Voltage: ${theme.highlight(status.outputVoltage + 'V')} Current: ${
theme.highlight(status.outputCurrent + 'A')
}`,
);
// Display host info
logger.log(` ${theme.dim(`Host: ${ups.snmp.host}:${ups.snmp.port}`)}`);
@@ -381,7 +413,9 @@ WantedBy=multi-user.target
for (const action of ups.actions) {
let actionDesc = `${action.type}`;
if (action.thresholds) {
actionDesc += ` (${action.triggerMode || 'onlyThresholds'}: battery<${action.thresholds.battery}%, runtime<${action.thresholds.runtime}min`;
actionDesc += ` (${
action.triggerMode || 'onlyThresholds'
}: battery<${action.thresholds.battery}%, runtime<${action.thresholds.runtime}min`;
if (action.shutdownDelay) {
actionDesc += `, delay=${action.shutdownDelay}s`;
}
@@ -398,10 +432,11 @@ WantedBy=multi-user.target
}
logger.log('');
} catch (error) {
// Display error for this UPS
logger.log(` ${symbols.error} ${theme.highlight(ups.name)} - ${theme.error('Connection failed')}`);
logger.log(
` ${symbols.error} ${theme.highlight(ups.name)} - ${theme.error('Connection failed')}`,
);
logger.log(` ${theme.dim(error instanceof Error ? error.message : String(error))}`);
logger.log(` ${theme.dim(`Host: ${ups.snmp.host}:${ups.snmp.port}`)}`);
logger.log('');
@@ -426,7 +461,9 @@ WantedBy=multi-user.target
// Display group name and mode
const modeColor = group.mode === 'redundant' ? theme.success : theme.warning;
logger.log(
` ${symbols.info} ${theme.highlight(group.name)} ${theme.dim(`(${modeColor(group.mode)})`)}`,
` ${symbols.info} ${theme.highlight(group.name)} ${
theme.dim(`(${modeColor(group.mode)})`)
}`,
);
// Display description if present
@@ -451,7 +488,9 @@ WantedBy=multi-user.target
for (const action of group.actions) {
let actionDesc = `${action.type}`;
if (action.thresholds) {
actionDesc += ` (${action.triggerMode || 'onlyThresholds'}: battery<${action.thresholds.battery}%, runtime<${action.thresholds.runtime}min`;
actionDesc += ` (${
action.triggerMode || 'onlyThresholds'
}: battery<${action.thresholds.battery}%, runtime<${action.thresholds.runtime}min`;
if (action.shutdownDelay) {
actionDesc += `, delay=${action.shutdownDelay}s`;
}