Compare commits

...

2 Commits

Author SHA1 Message Date
jkunz bf4d519428 v5.6.0
Release / build-and-release (push) Successful in 54s
2026-04-14 18:47:37 +00:00
jkunz 579667b3cd feat(config): add configurable default shutdown delay for shutdown actions 2026-04-14 18:47:37 +00:00
15 changed files with 142 additions and 43 deletions
+7
View File
@@ -1,5 +1,12 @@
# Changelog # Changelog
## 2026-04-14 - 5.6.0 - feat(config)
add configurable default shutdown delay for shutdown actions
- introduces a top-level defaultShutdownDelay config value used by shutdown actions that do not define their own delay
- applies the configured default during action execution, daemon-initiated shutdowns, CLI prompts, and status display output
- preserves explicit shutdownDelay values including 0 minutes and normalizes invalid config values back to the built-in default
## 2026-04-14 - 5.5.1 - fix(cli,daemon,snmp) ## 2026-04-14 - 5.5.1 - fix(cli,daemon,snmp)
normalize CLI argument parsing and extract daemon monitoring helpers with stronger SNMP typing normalize CLI argument parsing and extract daemon monitoring helpers with stronger SNMP typing
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@serve.zone/nupst", "name": "@serve.zone/nupst",
"version": "5.5.1", "version": "5.6.0",
"exports": "./mod.ts", "exports": "./mod.ts",
"nodeModulesDir": "auto", "nodeModulesDir": "auto",
"tasks": { "tasks": {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@serve.zone/nupst", "name": "@serve.zone/nupst",
"version": "5.5.1", "version": "5.6.0",
"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",
"keywords": [ "keywords": [
"ups", "ups",
+2
View File
@@ -75,6 +75,8 @@
shutdowns shutdowns
- `ts/daemon.ts` now delegates OS shutdown execution instead of embedding command lookup logic - `ts/daemon.ts` now delegates OS shutdown execution instead of embedding command lookup logic
inline inline
- `defaultShutdownDelay` in config provides the inherited delay for shutdown actions without an
explicit `shutdownDelay` override
### Config Watch Handling ### Config Watch Handling
+8 -4
View File
@@ -219,12 +219,16 @@ nupst uninstall # Completely remove NUPST (requires root)
NUPST stores configuration at `/etc/nupst/config.json`. The easiest way to configure is through the interactive CLI commands, but you can also edit the JSON directly. NUPST stores configuration at `/etc/nupst/config.json`. The easiest way to configure is through the interactive CLI commands, but you can also edit the JSON directly.
`defaultShutdownDelay` sets the inherited delay in minutes for shutdown actions that do not define
their own `shutdownDelay`.
### Example Configuration ### Example Configuration
```json ```json
{ {
"version": "4.3", "version": "4.3",
"checkInterval": 30000, "checkInterval": 30000,
"defaultShutdownDelay": 5,
"httpServer": { "httpServer": {
"enabled": true, "enabled": true,
"port": 8080, "port": 8080,
@@ -395,7 +399,7 @@ Actions define automated responses to UPS conditions. They run **sequentially in
| Field | Description | Default | | Field | Description | Default |
| --------------- | ---------------------------------- | ------- | | --------------- | ---------------------------------- | ------- |
| `shutdownDelay` | Minutes to wait before shutdown | `5` | | `shutdownDelay` | Minutes to wait before shutdown | Inherits `defaultShutdownDelay` (`5`) |
#### Webhook Action #### Webhook Action
@@ -610,16 +614,16 @@ UPS Devices (2):
Host: 192.168.1.100:161 (SNMP) Host: 192.168.1.100:161 (SNMP)
Groups: Data Center Groups: Data Center
Action: proxmox (onlyThresholds: battery<30%, runtime<15min) Action: proxmox (onlyThresholds: battery<30%, runtime<15min)
Action: shutdown (onlyThresholds: battery<20%, runtime<10min, delay=10s) Action: shutdown (onlyThresholds: battery<20%, runtime<10min, delay=10min)
✓ Local USB UPS (online - 95%, 2400min) ✓ Local USB UPS (online - 95%, 2400min)
Host: 127.0.0.1:3493 (UPSD) Host: 127.0.0.1:3493 (UPSD)
Action: shutdown (onlyThresholds: battery<15%, runtime<5min, delay=5s) Action: shutdown (onlyThresholds: battery<15%, runtime<5min, delay=5min)
Groups (1): Groups (1):
Data Center (redundant) Data Center (redundant)
UPS Devices (1): Main Server UPS UPS Devices (1): Main Server UPS
Action: shutdown (onlyThresholds: battery<10%, runtime<5min, delay=15s) Action: shutdown (onlyThresholds: battery<10%, runtime<5min, delay=15min)
``` ```
### Live Logs ### Live Logs
+21 -1
View File
@@ -11,7 +11,11 @@ import { type IPauseState, loadPauseSnapshot } from '../ts/pause-state.ts';
import { shortId } from '../ts/helpers/shortid.ts'; import { shortId } from '../ts/helpers/shortid.ts';
import { HTTP_SERVER, SNMP, THRESHOLDS, TIMING, UI } from '../ts/constants.ts'; import { HTTP_SERVER, SNMP, THRESHOLDS, TIMING, UI } from '../ts/constants.ts';
import { Action, type IActionContext } from '../ts/actions/base-action.ts'; import { Action, type IActionContext } from '../ts/actions/base-action.ts';
import { buildUpsActionContext, decideUpsActionExecution } from '../ts/action-orchestration.ts'; import {
applyDefaultShutdownDelay,
buildUpsActionContext,
decideUpsActionExecution,
} from '../ts/action-orchestration.ts';
import { import {
buildShutdownErrorRow, buildShutdownErrorRow,
buildShutdownStatusRow, buildShutdownStatusRow,
@@ -353,6 +357,22 @@ Deno.test('decideUpsActionExecution: returns executable action plan when actions
}); });
}); });
Deno.test('applyDefaultShutdownDelay: applies only to shutdown actions without explicit delay', () => {
const actions = [
{ type: 'shutdown' as const },
{ type: 'shutdown' as const, shutdownDelay: 0 },
{ type: 'shutdown' as const, shutdownDelay: 9 },
{ type: 'webhook' as const },
];
assertEquals(applyDefaultShutdownDelay(actions, 7), [
{ type: 'shutdown', shutdownDelay: 7 },
{ type: 'shutdown', shutdownDelay: 0 },
{ type: 'shutdown', shutdownDelay: 9 },
{ type: 'webhook' },
]);
});
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Shutdown Monitoring Tests // Shutdown Monitoring Tests
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
+1 -1
View File
@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@serve.zone/nupst', name: '@serve.zone/nupst',
version: '5.5.1', version: '5.6.0',
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'
} }
+16
View File
@@ -34,6 +34,22 @@ export function buildUpsActionContext(
}; };
} }
export function applyDefaultShutdownDelay(
actions: IActionConfig[],
defaultDelayMinutes: number,
): IActionConfig[] {
return actions.map((action) => {
if (action.type !== 'shutdown' || action.shutdownDelay !== undefined) {
return action;
}
return {
...action,
shutdownDelay: defaultDelayMinutes,
};
});
}
export function decideUpsActionExecution( export function decideUpsActionExecution(
isPaused: boolean, isPaused: boolean,
ups: IUpsActionSource, ups: IUpsActionSource,
+1 -1
View File
@@ -74,7 +74,7 @@ export interface IActionConfig {
}; };
// Shutdown action configuration // Shutdown action configuration
/** Delay before shutdown in minutes (default: 5) */ /** Delay before shutdown in minutes (defaults to the config-level shutdown delay, or 5) */
shutdownDelay?: number; shutdownDelay?: number;
/** Only execute shutdown on threshold violation, not power status changes */ /** Only execute shutdown on threshold violation, not power status changes */
onlyOnThresholdViolation?: boolean; onlyOnThresholdViolation?: boolean;
+1 -1
View File
@@ -124,7 +124,7 @@ export class ShutdownAction extends Action {
return; return;
} }
const shutdownDelay = this.config.shutdownDelay || SHUTDOWN.DEFAULT_DELAY_MINUTES; const shutdownDelay = this.config.shutdownDelay ?? SHUTDOWN.DEFAULT_DELAY_MINUTES;
logger.log(''); logger.log('');
logger.logBoxTitle('Initiating System Shutdown', UI.WIDE_BOX_WIDTH, 'error'); logger.logBoxTitle('Initiating System Shutdown', UI.WIDE_BOX_WIDTH, 'error');
+14 -7
View File
@@ -4,6 +4,7 @@ import { type ITableColumn, logger } from '../logger.ts';
import { symbols, theme } from '../colors.ts'; import { symbols, theme } from '../colors.ts';
import type { IActionConfig } from '../actions/base-action.ts'; import type { IActionConfig } from '../actions/base-action.ts';
import { ProxmoxAction } from '../actions/proxmox-action.ts'; import { ProxmoxAction } from '../actions/proxmox-action.ts';
import { SHUTDOWN } from '../constants.ts';
import type { IGroupConfig, IUpsConfig } from '../daemon.ts'; import type { IGroupConfig, IUpsConfig } from '../daemon.ts';
import * as helpers from '../helpers/index.ts'; import * as helpers from '../helpers/index.ts';
@@ -81,16 +82,20 @@ export class ActionHandler {
if (typeValue === 1) { if (typeValue === 1) {
// Shutdown action // Shutdown action
newAction.type = 'shutdown'; newAction.type = 'shutdown';
const defaultShutdownDelay =
this.nupst.getDaemon().getConfig().defaultShutdownDelay ?? SHUTDOWN.DEFAULT_DELAY_MINUTES;
const delayStr = await prompt( const delayStr = await prompt(
` ${theme.dim('Shutdown delay')} ${theme.dim('(minutes) [5]:')} `, ` ${theme.dim('Shutdown delay')} ${theme.dim(`(minutes, leave empty for default ${defaultShutdownDelay}):`)} `,
); );
const shutdownDelay = delayStr ? parseInt(delayStr, 10) : 5; if (delayStr.trim()) {
if (isNaN(shutdownDelay) || shutdownDelay < 0) { const shutdownDelay = parseInt(delayStr, 10);
logger.error('Invalid shutdown delay. Must be >= 0.'); if (isNaN(shutdownDelay) || shutdownDelay < 0) {
process.exit(1); logger.error('Invalid shutdown delay. Must be >= 0.');
process.exit(1);
}
newAction.shutdownDelay = shutdownDelay;
} }
newAction.shutdownDelay = shutdownDelay;
} else if (typeValue === 2) { } else if (typeValue === 2) {
// Webhook action // Webhook action
newAction.type = 'webhook'; newAction.type = 'webhook';
@@ -468,7 +473,9 @@ export class ActionHandler {
]; ];
const rows = target.actions.map((action, index) => { const rows = target.actions.map((action, index) => {
let details = `${action.shutdownDelay || 5}min delay`; const defaultShutdownDelay =
this.nupst.getDaemon().getConfig().defaultShutdownDelay ?? SHUTDOWN.DEFAULT_DELAY_MINUTES;
let details = `${action.shutdownDelay ?? defaultShutdownDelay}min delay`;
if (action.type === 'proxmox') { if (action.type === 'proxmox') {
const mode = action.proxmoxMode || 'auto'; const mode = action.proxmoxMode || 'auto';
if (mode === 'cli' || (mode === 'auto' && !action.proxmoxTokenId)) { if (mode === 'cli' || (mode === 'auto' && !action.proxmoxTokenId)) {
+13 -5
View File
@@ -10,7 +10,7 @@ import type { TProtocol } from '../protocol/types.ts';
import type { INupstConfig, IUpsConfig } from '../daemon.ts'; import type { INupstConfig, IUpsConfig } from '../daemon.ts';
import type { IActionConfig } from '../actions/base-action.ts'; import type { IActionConfig } from '../actions/base-action.ts';
import { ProxmoxAction } from '../actions/proxmox-action.ts'; import { ProxmoxAction } from '../actions/proxmox-action.ts';
import { UPSD } from '../constants.ts'; import { SHUTDOWN, UPSD } from '../constants.ts';
/** /**
* Thresholds configuration for CLI display * Thresholds configuration for CLI display
@@ -1152,11 +1152,19 @@ export class UpsHandler {
if (typeValue === 1) { if (typeValue === 1) {
// Shutdown action // Shutdown action
action.type = 'shutdown'; action.type = 'shutdown';
const defaultShutdownDelay =
this.nupst.getDaemon().getConfig().defaultShutdownDelay ?? SHUTDOWN.DEFAULT_DELAY_MINUTES;
const delayInput = await prompt('Shutdown delay in minutes [5]: '); const delayInput = await prompt(
const delay = parseInt(delayInput, 10); `Shutdown delay in minutes (leave empty for default ${defaultShutdownDelay}): `,
if (delayInput.trim() && !isNaN(delay)) { );
action.shutdownDelay = delay; if (delayInput.trim()) {
const delay = parseInt(delayInput, 10);
if (isNaN(delay) || delay < 0) {
logger.warn('Invalid shutdown delay, using configured default');
} else {
action.shutdownDelay = delay;
}
} }
} else if (typeValue === 2) { } else if (typeValue === 2) {
// Webhook action // Webhook action
+39 -9
View File
@@ -12,9 +12,13 @@ import { MigrationRunner } from './migrations/index.ts';
import { formatPowerStatus, getBatteryColor, getRuntimeColor, theme } from './colors.ts'; import { formatPowerStatus, getBatteryColor, getRuntimeColor, theme } from './colors.ts';
import type { IActionConfig } from './actions/base-action.ts'; import type { IActionConfig } from './actions/base-action.ts';
import { ActionManager } from './actions/index.ts'; import { ActionManager } from './actions/index.ts';
import { decideUpsActionExecution, type TUpsTriggerReason } from './action-orchestration.ts'; import {
applyDefaultShutdownDelay,
decideUpsActionExecution,
type TUpsTriggerReason,
} from './action-orchestration.ts';
import { NupstHttpServer } from './http-server.ts'; import { NupstHttpServer } from './http-server.ts';
import { NETWORK, PAUSE, THRESHOLDS, TIMING, UI } from './constants.ts'; import { NETWORK, PAUSE, SHUTDOWN, THRESHOLDS, TIMING, UI } from './constants.ts';
import { import {
analyzeConfigReload, analyzeConfigReload,
shouldRefreshPauseState, shouldRefreshPauseState,
@@ -97,6 +101,8 @@ export interface INupstConfig {
groups: IGroupConfig[]; groups: IGroupConfig[];
/** Check interval in milliseconds */ /** Check interval in milliseconds */
checkInterval: number; checkInterval: number;
/** Default delay in minutes for shutdown actions without an override */
defaultShutdownDelay?: number;
/** HTTP Server configuration */ /** HTTP Server configuration */
httpServer?: IHttpServerConfig; httpServer?: IHttpServerConfig;
@@ -125,6 +131,7 @@ export class NupstDaemon {
/** Default configuration */ /** Default configuration */
private readonly DEFAULT_CONFIG: INupstConfig = { private readonly DEFAULT_CONFIG: INupstConfig = {
version: '4.3', version: '4.3',
defaultShutdownDelay: SHUTDOWN.DEFAULT_DELAY_MINUTES,
upsDevices: [ upsDevices: [
{ {
id: 'default', id: 'default',
@@ -155,7 +162,6 @@ export class NupstDaemon {
battery: THRESHOLDS.DEFAULT_BATTERY_PERCENT, // Shutdown when battery below 60% battery: THRESHOLDS.DEFAULT_BATTERY_PERCENT, // Shutdown when battery below 60%
runtime: THRESHOLDS.DEFAULT_RUNTIME_MINUTES, // Shutdown when runtime below 20 minutes runtime: THRESHOLDS.DEFAULT_RUNTIME_MINUTES, // Shutdown when runtime below 20 minutes
}, },
shutdownDelay: 5,
}, },
], ],
}, },
@@ -208,10 +214,13 @@ export class NupstDaemon {
const migrationRunner = new MigrationRunner(); const migrationRunner = new MigrationRunner();
const { config: migratedConfig, migrated } = await migrationRunner.run(parsedConfig); const { config: migratedConfig, migrated } = await migrationRunner.run(parsedConfig);
// Save migrated config back to disk if any migrations ran // Save migrated or normalized config back to disk when needed.
// Cast to INupstConfig since migrations ensure the output is valid // Cast to INupstConfig since migrations ensure the output is valid.
const validConfig = migratedConfig as unknown as INupstConfig; const validConfig = migratedConfig as unknown as INupstConfig;
if (migrated) { const normalizedShutdownDelay = this.normalizeShutdownDelay(validConfig.defaultShutdownDelay);
const shouldPersistNormalizedConfig = validConfig.defaultShutdownDelay !== normalizedShutdownDelay;
validConfig.defaultShutdownDelay = normalizedShutdownDelay;
if (migrated || shouldPersistNormalizedConfig) {
this.config = validConfig; this.config = validConfig;
await this.saveConfig(this.config); await this.saveConfig(this.config);
} else { } else {
@@ -249,6 +258,7 @@ export class NupstDaemon {
upsDevices: config.upsDevices, upsDevices: config.upsDevices,
groups: config.groups, groups: config.groups,
checkInterval: config.checkInterval, checkInterval: config.checkInterval,
defaultShutdownDelay: this.normalizeShutdownDelay(config.defaultShutdownDelay),
...(config.httpServer ? { httpServer: config.httpServer } : {}), ...(config.httpServer ? { httpServer: config.httpServer } : {}),
}; };
@@ -280,6 +290,22 @@ export class NupstDaemon {
return this.config; return this.config;
} }
private normalizeShutdownDelay(delayMinutes: number | undefined): number {
if (
typeof delayMinutes !== 'number' ||
!Number.isFinite(delayMinutes) ||
delayMinutes < 0
) {
return SHUTDOWN.DEFAULT_DELAY_MINUTES;
}
return delayMinutes;
}
private getDefaultShutdownDelayMinutes(): number {
return this.normalizeShutdownDelay(this.config.defaultShutdownDelay);
}
/** /**
* Get the SNMP instance * Get the SNMP instance
*/ */
@@ -758,7 +784,12 @@ export class NupstDaemon {
return; return;
} }
await ActionManager.executeActions(decision.actions, decision.context); const actions = applyDefaultShutdownDelay(
decision.actions,
this.getDefaultShutdownDelayMinutes(),
);
await ActionManager.executeActions(actions, decision.context);
} }
/** /**
@@ -768,8 +799,7 @@ export class NupstDaemon {
public async initiateShutdown(reason: string): Promise<void> { public async initiateShutdown(reason: string): Promise<void> {
logger.log(`Initiating system shutdown due to: ${reason}`); logger.log(`Initiating system shutdown due to: ${reason}`);
// Set a longer delay for shutdown to allow VMs and services to close const shutdownDelayMinutes = this.getDefaultShutdownDelayMinutes();
const shutdownDelayMinutes = 5;
try { try {
await this.shutdownExecutor.scheduleShutdown(shutdownDelayMinutes); await this.shutdownExecutor.scheduleShutdown(shutdownDelayMinutes);
+1 -3
View File
@@ -37,8 +37,7 @@ import { logger } from '../logger.ts';
* { * {
* type: "shutdown", * type: "shutdown",
* thresholds: { battery: 60, runtime: 20 }, * thresholds: { battery: 60, runtime: 20 },
* triggerMode: "onlyThresholds", * triggerMode: "onlyThresholds"
* shutdownDelay: 5
* } * }
* ] * ]
* } * }
@@ -93,7 +92,6 @@ export class MigrationV4_0ToV4_1 extends BaseMigration {
runtime: deviceThresholds.runtime, runtime: deviceThresholds.runtime,
}, },
triggerMode: 'onlyThresholds', // Preserve old behavior (only on threshold violation) triggerMode: 'onlyThresholds', // Preserve old behavior (only on threshold violation)
shutdownDelay: 5, // Default delay
}, },
]; ];
logger.dim( logger.dim(
+16 -9
View File
@@ -5,6 +5,7 @@ import { type IUpsConfig, NupstDaemon } from './daemon.ts';
import { NupstSnmp } from './snmp/manager.ts'; import { NupstSnmp } from './snmp/manager.ts';
import { logger } from './logger.ts'; import { logger } from './logger.ts';
import { formatPowerStatus, getBatteryColor, getRuntimeColor, symbols, theme } from './colors.ts'; import { formatPowerStatus, getBatteryColor, getRuntimeColor, symbols, theme } from './colors.ts';
import { SHUTDOWN } from './constants.ts';
/** /**
* Class for managing systemd service * Class for managing systemd service
@@ -316,7 +317,6 @@ WantedBy=multi-user.target
type: 'shutdown', type: 'shutdown',
thresholds: config.thresholds, thresholds: config.thresholds,
triggerMode: 'onlyThresholds', triggerMode: 'onlyThresholds',
shutdownDelay: 5,
}, },
] ]
: [], : [],
@@ -346,6 +346,8 @@ WantedBy=multi-user.target
*/ */
private async displaySingleUpsStatus(ups: IUpsConfig, snmp: NupstSnmp): Promise<void> { private async displaySingleUpsStatus(ups: IUpsConfig, snmp: NupstSnmp): Promise<void> {
try { try {
const defaultShutdownDelay =
this.daemon.getConfig().defaultShutdownDelay ?? SHUTDOWN.DEFAULT_DELAY_MINUTES;
const protocol = ups.protocol || 'snmp'; const protocol = ups.protocol || 'snmp';
let status; let status;
@@ -432,14 +434,16 @@ WantedBy=multi-user.target
actionDesc += ` (${ actionDesc += ` (${
action.triggerMode || 'onlyThresholds' action.triggerMode || 'onlyThresholds'
}: battery<${action.thresholds.battery}%, runtime<${action.thresholds.runtime}min`; }: battery<${action.thresholds.battery}%, runtime<${action.thresholds.runtime}min`;
if (action.shutdownDelay) { if (action.type === 'shutdown') {
actionDesc += `, delay=${action.shutdownDelay}s`; const shutdownDelay = action.shutdownDelay ?? defaultShutdownDelay;
actionDesc += `, delay=${shutdownDelay}min`;
} }
actionDesc += ')'; actionDesc += ')';
} else { } else {
actionDesc += ` (${action.triggerMode || 'onlyPowerChanges'}`; actionDesc += ` (${action.triggerMode || 'onlyPowerChanges'}`;
if (action.shutdownDelay) { if (action.type === 'shutdown') {
actionDesc += `, delay=${action.shutdownDelay}s`; const shutdownDelay = action.shutdownDelay ?? defaultShutdownDelay;
actionDesc += `, delay=${shutdownDelay}min`;
} }
actionDesc += ')'; actionDesc += ')';
} }
@@ -506,20 +510,23 @@ WantedBy=multi-user.target
// Display actions if any // Display actions if any
if (group.actions && group.actions.length > 0) { if (group.actions && group.actions.length > 0) {
const defaultShutdownDelay = config.defaultShutdownDelay ?? SHUTDOWN.DEFAULT_DELAY_MINUTES;
for (const action of group.actions) { for (const action of group.actions) {
let actionDesc = `${action.type}`; let actionDesc = `${action.type}`;
if (action.thresholds) { if (action.thresholds) {
actionDesc += ` (${ actionDesc += ` (${
action.triggerMode || 'onlyThresholds' action.triggerMode || 'onlyThresholds'
}: battery<${action.thresholds.battery}%, runtime<${action.thresholds.runtime}min`; }: battery<${action.thresholds.battery}%, runtime<${action.thresholds.runtime}min`;
if (action.shutdownDelay) { if (action.type === 'shutdown') {
actionDesc += `, delay=${action.shutdownDelay}s`; const shutdownDelay = action.shutdownDelay ?? defaultShutdownDelay;
actionDesc += `, delay=${shutdownDelay}min`;
} }
actionDesc += ')'; actionDesc += ')';
} else { } else {
actionDesc += ` (${action.triggerMode || 'onlyPowerChanges'}`; actionDesc += ` (${action.triggerMode || 'onlyPowerChanges'}`;
if (action.shutdownDelay) { if (action.type === 'shutdown') {
actionDesc += `, delay=${action.shutdownDelay}s`; const shutdownDelay = action.shutdownDelay ?? defaultShutdownDelay;
actionDesc += `, delay=${shutdownDelay}min`;
} }
actionDesc += ')'; actionDesc += ')';
} }