feat(action): add group action support

Extended action management to support groups in addition to UPS devices:

Changes:
- Auto-detects whether target ID is a UPS or group
- All action commands now work with both UPS and groups:
  * nupst action add <ups-id|group-id>
  * nupst action remove <ups-id|group-id> <index>
  * nupst action list [ups-id|group-id]

- Updated ActionHandler methods to handle both target types
- Updated help text and usage examples
- List command shows both UPS and group actions when no target specified
- Clear labeling in output distinguishes UPS actions from group actions

Example usage:
  nupst action list                 # Shows all UPS and group actions
  nupst action add dc-rack-1        # Adds action to group 'dc-rack-1'
  nupst action remove default 0     # Removes action from UPS 'default'

Groups can now have their own shutdown actions, allowing fine-grained
control over group behavior during power events.
This commit is contained in:
2025-10-20 12:34:47 +00:00
parent 9c6fa37eb8
commit 1ca05e879b
2 changed files with 115 additions and 62 deletions

View File

@@ -3,7 +3,7 @@ import { Nupst } from '../nupst.ts';
import { logger, type ITableColumn } from '../logger.ts';
import { theme, symbols } from '../colors.ts';
import type { IActionConfig } from '../actions/base-action.ts';
import type { IUpsConfig } from '../daemon.ts';
import type { IUpsConfig, IGroupConfig } from '../daemon.ts';
/**
* Class for handling action-related CLI commands
@@ -21,30 +21,42 @@ export class ActionHandler {
}
/**
* Add a new action to a UPS
* Add a new action to a UPS or group
*/
public async add(upsId?: string): Promise<void> {
public async add(targetId?: string): Promise<void> {
try {
if (!upsId) {
logger.error('UPS ID is required');
logger.log(` ${theme.dim('Usage:')} ${theme.command('nupst action add <ups-id>')}`);
if (!targetId) {
logger.error('Target ID is required');
logger.log(
` ${theme.dim('Usage:')} ${theme.command('nupst action add <ups-id|group-id>')}`,
);
logger.log('');
logger.log(` ${theme.dim('List UPS devices:')} ${theme.command('nupst ups list')}`);
logger.log(` ${theme.dim('List groups:')} ${theme.command('nupst group list')}`);
logger.log('');
process.exit(1);
}
const config = await this.nupst.getDaemon().loadConfig();
const ups = config.upsDevices.find((u) => u.id === upsId);
if (!ups) {
logger.error(`UPS with ID '${upsId}' not found`);
// Check if it's a UPS
const ups = config.upsDevices.find((u) => u.id === targetId);
// Check if it's a group
const group = config.groups?.find((g) => g.id === targetId);
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('');
process.exit(1);
}
const target = ups || group;
const targetType = ups ? 'UPS' : 'Group';
const targetName = ups ? ups.name : group!.name;
const readline = await import('node:readline');
const rl = readline.createInterface({
input: process.stdin,
@@ -61,7 +73,7 @@ export class ActionHandler {
try {
logger.log('');
logger.info(`Add Action to ${theme.highlight(ups.name)}`);
logger.info(`Add Action to ${targetType} ${theme.highlight(targetName)}`);
logger.log('');
// Action type (currently only shutdown is supported)
@@ -130,37 +142,41 @@ export class ActionHandler {
shutdownDelay,
};
// Add to UPS
if (!ups.actions) {
ups.actions = [];
// Add to target (UPS or group)
if (!target!.actions) {
target!.actions = [];
}
ups.actions.push(newAction);
target!.actions.push(newAction);
await this.nupst.getDaemon().saveConfig(config);
logger.log('');
logger.success(`Action added to ${ups.name}`);
logger.success(`Action added to ${targetType} ${targetName}`);
logger.log('');
logger.log(` ${theme.dim('Restart service to apply changes:')} ${theme.command('nupst service restart')}`);
logger.log(
` ${theme.dim('Restart service to apply changes:')} ${theme.command('nupst service restart')}`,
);
logger.log('');
} finally {
rl.close();
}
} catch (error) {
logger.error(`Failed to add action: ${error instanceof Error ? error.message : String(error)}`);
logger.error(
`Failed to add action: ${error instanceof Error ? error.message : String(error)}`,
);
process.exit(1);
}
}
/**
* Remove an action from a UPS
* Remove an action from a UPS or group
*/
public async remove(upsId?: string, actionIndexStr?: string): Promise<void> {
public async remove(targetId?: string, actionIndexStr?: string): Promise<void> {
try {
if (!upsId || !actionIndexStr) {
logger.error('UPS ID and action index are required');
if (!targetId || !actionIndexStr) {
logger.error('Target ID and action index are required');
logger.log(
` ${theme.dim('Usage:')} ${theme.command('nupst action remove <ups-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')}`);
@@ -175,39 +191,50 @@ export class ActionHandler {
}
const config = await this.nupst.getDaemon().loadConfig();
const ups = config.upsDevices.find((u) => u.id === upsId);
if (!ups) {
logger.error(`UPS with ID '${upsId}' not found`);
// Check if it's a UPS
const ups = config.upsDevices.find((u) => u.id === targetId);
// Check if it's a group
const group = config.groups?.find((g) => g.id === targetId);
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('');
process.exit(1);
}
if (!ups.actions || ups.actions.length === 0) {
logger.error(`No actions configured for UPS '${ups.name}'`);
const target = ups || group;
const targetType = ups ? 'UPS' : 'Group';
const targetName = ups ? ups.name : group!.name;
if (!target!.actions || target!.actions.length === 0) {
logger.error(`No actions configured for ${targetType} '${targetName}'`);
logger.log('');
process.exit(1);
}
if (actionIndex >= ups.actions.length) {
if (actionIndex >= target!.actions.length) {
logger.error(
`Invalid action index. UPS '${ups.name}' has ${ups.actions.length} action(s) (index 0-${ups.actions.length - 1})`,
`Invalid action index. ${targetType} '${targetName}' has ${target!.actions.length} action(s) (index 0-${target!.actions.length - 1})`,
);
logger.log('');
logger.log(` ${theme.dim('List actions:')} ${theme.command(`nupst action list ${upsId}`)}`);
logger.log(
` ${theme.dim('List actions:')} ${theme.command(`nupst action list ${targetId}`)}`,
);
logger.log('');
process.exit(1);
}
const removedAction = ups.actions[actionIndex];
ups.actions.splice(actionIndex, 1);
const removedAction = target!.actions[actionIndex];
target!.actions.splice(actionIndex, 1);
await this.nupst.getDaemon().saveConfig(config);
logger.log('');
logger.success(`Action removed from ${ups.name}`);
logger.success(`Action removed from ${targetType} ${targetName}`);
logger.log(` ${theme.dim('Type:')} ${removedAction.type}`);
if (removedAction.thresholds) {
logger.log(
@@ -215,7 +242,9 @@ export class ActionHandler {
);
}
logger.log('');
logger.log(` ${theme.dim('Restart service to apply changes:')} ${theme.command('nupst service restart')}`);
logger.log(
` ${theme.dim('Restart service to apply changes:')} ${theme.command('nupst service restart')}`,
);
logger.log('');
} catch (error) {
logger.error(
@@ -226,43 +255,61 @@ export class ActionHandler {
}
/**
* List all actions for a specific UPS or all UPS devices
* List all actions for a specific UPS/group or all devices
*/
public async list(upsId?: string): Promise<void> {
public async list(targetId?: string): Promise<void> {
try {
const config = await this.nupst.getDaemon().loadConfig();
if (upsId) {
// List actions for specific UPS
const ups = config.upsDevices.find((u) => u.id === upsId);
if (targetId) {
// List actions for specific UPS or group
const ups = config.upsDevices.find((u) => u.id === targetId);
const group = config.groups?.find((g) => g.id === targetId);
if (!ups) {
logger.error(`UPS with ID '${upsId}' not found`);
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('');
process.exit(1);
}
this.displayUpsActions(ups);
if (ups) {
this.displayTargetActions(ups, 'UPS');
} else {
this.displayTargetActions(group!, 'Group');
}
} else {
// List actions for all UPS devices
// List actions for all UPS devices and groups
logger.log('');
logger.info('Actions for All UPS Devices');
logger.info('Actions for All UPS Devices and Groups');
logger.log('');
let hasAnyActions = false;
// Display UPS actions
for (const ups of config.upsDevices) {
if (ups.actions && ups.actions.length > 0) {
hasAnyActions = true;
this.displayUpsActions(ups);
this.displayTargetActions(ups, 'UPS');
}
}
// Display Group actions
for (const group of config.groups || []) {
if (group.actions && group.actions.length > 0) {
hasAnyActions = true;
this.displayTargetActions(group, 'Group');
}
}
if (!hasAnyActions) {
logger.log(` ${theme.dim('No actions configured')}`);
logger.log('');
logger.log(` ${theme.dim('Add an action:')} ${theme.command('nupst action add <ups-id>')}`);
logger.log(
` ${theme.dim('Add an action:')} ${theme.command('nupst action add <ups-id|group-id>')}`,
);
logger.log('');
}
}
@@ -275,13 +322,18 @@ export class ActionHandler {
}
/**
* Display actions for a single UPS
* Display actions for a single UPS or Group
*/
private displayUpsActions(ups: IUpsConfig): void {
logger.log(`${symbols.info} ${theme.highlight(ups.name)} ${theme.dim(`(${ups.id})`)}`);
private displayTargetActions(
target: IUpsConfig | IGroupConfig,
targetType: 'UPS' | 'Group',
): void {
logger.log(
`${symbols.info} ${targetType} ${theme.highlight(target.name)} ${theme.dim(`(${target.id})`)}`,
);
logger.log('');
if (!ups.actions || ups.actions.length === 0) {
if (!target.actions || target.actions.length === 0) {
logger.log(` ${theme.dim('No actions configured')}`);
logger.log('');
return;
@@ -296,7 +348,7 @@ export class ActionHandler {
{ header: 'Delay', key: 'delay', align: 'right' },
];
const rows = ups.actions.map((action, index) => ({
const rows = target.actions.map((action, index) => ({
index: theme.dim(index.toString()),
type: theme.highlight(action.type),
battery: action.thresholds ? `${action.thresholds.battery}%` : theme.dim('N/A'),