From 1ca05e879bbf2160c79d887c90065f0604d9895b Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Mon, 20 Oct 2025 12:34:47 +0000 Subject: [PATCH] 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 * nupst action remove * 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. --- ts/cli.ts | 23 +++--- ts/cli/action-handler.ts | 154 ++++++++++++++++++++++++++------------- 2 files changed, 115 insertions(+), 62 deletions(-) diff --git a/ts/cli.ts b/ts/cli.ts index a5be164..49225ac 100644 --- a/ts/cli.ts +++ b/ts/cli.ts @@ -571,9 +571,9 @@ export class NupstCli { // Action subcommands logger.log(theme.info('Action Subcommands:')); - this.printCommand('nupst action add ', 'Add a new action to a UPS'); - this.printCommand('nupst action remove ', 'Remove an action by index'); - this.printCommand('nupst action list [ups-id]', 'List all actions (optionally for specific UPS)'); + this.printCommand('nupst action add ', 'Add a new action to a UPS or group'); + this.printCommand('nupst action remove ', 'Remove an action by index'); + this.printCommand('nupst action list [target-id]', 'List all actions (optionally for specific target)'); console.log(''); // Options @@ -691,18 +691,19 @@ Usage: nupst action [arguments] Subcommands: - add - Add a new action to a UPS interactively - remove - Remove an action by index (alias: rm, delete) - list [ups-id] - List all actions (optionally for specific UPS) (alias: ls) + add - Add a new action to a UPS or group interactively + remove - Remove an action by index (alias: rm, delete) + list [ups-id|group-id] - List all actions (optionally for specific target) (alias: ls) Options: - --debug, -d - Enable debug mode for detailed logging + --debug, -d - Enable debug mode for detailed logging Examples: - nupst action list - List actions for all UPS devices - nupst action list default - List actions for UPS with ID 'default' - nupst action add default - Add a new action to UPS 'default' - nupst action remove default 0 - Remove action at index 0 from UPS 'default' + nupst action list - List actions for all UPS devices and groups + nupst action list default - List actions for UPS or group with ID 'default' + nupst action add default - Add a new action to UPS or group 'default' + nupst action remove default 0 - Remove action at index 0 from UPS or group 'default' + nupst action add dc-rack-1 - Add a new action to group 'dc-rack-1' `); } } diff --git a/ts/cli/action-handler.ts b/ts/cli/action-handler.ts index b130063..b06e80c 100644 --- a/ts/cli/action-handler.ts +++ b/ts/cli/action-handler.ts @@ -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 { + public async add(targetId?: string): Promise { try { - if (!upsId) { - logger.error('UPS ID is required'); - logger.log(` ${theme.dim('Usage:')} ${theme.command('nupst action add ')}`); + if (!targetId) { + logger.error('Target ID is required'); + logger.log( + ` ${theme.dim('Usage:')} ${theme.command('nupst action add ')}`, + ); 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 { + public async remove(targetId?: string, actionIndexStr?: string): Promise { 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 ')}`, + ` ${theme.dim('Usage:')} ${theme.command('nupst action remove ')}`, ); 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 { + public async list(targetId?: string): Promise { 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 ')}`); + logger.log( + ` ${theme.dim('Add an action:')} ${theme.command('nupst action add ')}`, + ); 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'),