Files
nupst/ts/cli/action-handler.ts
T

804 lines
29 KiB
TypeScript

import process from 'node:process';
import { Nupst } from '../nupst.ts';
import { type ITableColumn, logger } from '../logger.ts';
import { symbols, theme } from '../colors.ts';
import type { IActionConfig } from '../actions/base-action.ts';
import { ProxmoxAction } from '../actions/proxmox-action.ts';
import { SHUTDOWN } from '../constants.ts';
import type { IGroupConfig, IUpsConfig } from '../daemon.ts';
import * as helpers from '../helpers/index.ts';
/**
* Class for handling action-related CLI commands
* Provides interface for managing UPS actions
*/
export class ActionHandler {
private readonly nupst: Nupst;
/**
* Create a new action handler
* @param nupst Reference to the main Nupst instance
*/
constructor(nupst: Nupst) {
this.nupst = nupst;
}
/**
* Add a new action to a UPS or group
*/
public async add(targetId?: string): Promise<void> {
try {
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 targetSnapshot = this.resolveActionTarget(config, targetId);
await helpers.withPrompt(async (prompt) => {
logger.log('');
logger.info(
`Add Action to ${targetSnapshot.targetType} ${
theme.highlight(targetSnapshot.targetName)
}`,
);
logger.log('');
const newAction = await this.promptForActionConfig(prompt);
// Add to target (UPS or group)
if (!targetSnapshot.target.actions) {
targetSnapshot.target.actions = [];
}
targetSnapshot.target.actions.push(newAction);
await this.nupst.getDaemon().saveConfig(config);
logger.log('');
logger.success(`Action added to ${targetSnapshot.targetType} ${targetSnapshot.targetName}`);
logger.log(` ${theme.dim('Changes saved and will be applied automatically')}`);
logger.log('');
});
} catch (error) {
logger.error(
`Failed to add action: ${error instanceof Error ? error.message : String(error)}`,
);
process.exit(1);
}
}
/**
* Edit an existing action on a UPS or group
*/
public async edit(targetId?: string, actionIndexStr?: string): Promise<void> {
try {
await helpers.withPrompt(async (prompt) => {
await this.runEditProcess(targetId, actionIndexStr, prompt);
});
} catch (error) {
logger.error(
`Failed to edit action: ${error instanceof Error ? error.message : String(error)}`,
);
process.exit(1);
}
}
/**
* Run the interactive process to edit an action
*/
public async runEditProcess(
targetId: string | undefined,
actionIndexStr: string | undefined,
prompt: (question: string) => Promise<string>,
): Promise<void> {
if (!targetId || !actionIndexStr) {
logger.error('Target ID and action index are required');
logger.log(
` ${theme.dim('Usage:')} ${
theme.command('nupst action edit <ups-id|group-id> <action-index>')
}`,
);
logger.log('');
logger.log(` ${theme.dim('List actions:')} ${theme.command('nupst action list')}`);
logger.log('');
process.exit(1);
}
const actionIndex = parseInt(actionIndexStr, 10);
if (isNaN(actionIndex) || actionIndex < 0) {
logger.error('Invalid action index. Must be >= 0.');
process.exit(1);
}
const config = await this.nupst.getDaemon().loadConfig();
const targetSnapshot = this.resolveActionTarget(config, targetId);
if (!targetSnapshot.target.actions || targetSnapshot.target.actions.length === 0) {
logger.error(
`No actions configured for ${targetSnapshot.targetType} '${targetSnapshot.targetName}'`,
);
logger.log('');
process.exit(1);
}
if (actionIndex >= targetSnapshot.target.actions.length) {
logger.error(
`Invalid action index. ${targetSnapshot.targetType} '${targetSnapshot.targetName}' has ${targetSnapshot.target.actions.length} action(s) (index 0-${
targetSnapshot.target.actions.length - 1
})`,
);
logger.log('');
logger.log(
` ${theme.dim('List actions:')} ${theme.command(`nupst action list ${targetId}`)}`,
);
logger.log('');
process.exit(1);
}
const currentAction = targetSnapshot.target.actions[actionIndex];
logger.log('');
logger.info(
`Edit Action ${theme.highlight(String(actionIndex))} on ${targetSnapshot.targetType} ${
theme.highlight(targetSnapshot.targetName)
}`,
);
logger.log(` ${theme.dim('Current type:')} ${theme.highlight(currentAction.type)}`);
logger.log('');
const updatedAction = await this.promptForActionConfig(prompt, currentAction);
targetSnapshot.target.actions[actionIndex] = updatedAction;
await this.nupst.getDaemon().saveConfig(config);
logger.log('');
logger.success(`Action updated on ${targetSnapshot.targetType} ${targetSnapshot.targetName}`);
logger.log(` ${theme.dim('Index:')} ${actionIndex}`);
logger.log(` ${theme.dim('Type:')} ${updatedAction.type}`);
logger.log(` ${theme.dim('Changes saved and will be applied automatically')}`);
logger.log('');
}
/**
* Remove an action from a UPS or group
*/
public async remove(targetId?: string, actionIndexStr?: string): Promise<void> {
try {
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>')
}`,
);
logger.log('');
logger.log(` ${theme.dim('List actions:')} ${theme.command('nupst action list')}`);
logger.log('');
process.exit(1);
}
const actionIndex = parseInt(actionIndexStr, 10);
if (isNaN(actionIndex) || actionIndex < 0) {
logger.error('Invalid action index. Must be >= 0.');
process.exit(1);
}
const config = await this.nupst.getDaemon().loadConfig();
// 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;
if (!target!.actions || target!.actions.length === 0) {
logger.error(`No actions configured for ${targetType} '${targetName}'`);
logger.log('');
process.exit(1);
}
if (actionIndex >= target!.actions.length) {
logger.error(
`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 ${targetId}`)}`,
);
logger.log('');
process.exit(1);
}
const removedAction = target!.actions[actionIndex];
target!.actions.splice(actionIndex, 1);
await this.nupst.getDaemon().saveConfig(config);
logger.log('');
logger.success(`Action removed from ${targetType} ${targetName}`);
logger.log(` ${theme.dim('Type:')} ${removedAction.type}`);
if (removedAction.thresholds) {
logger.log(
` ${
theme.dim('Thresholds:')
} Battery: ${removedAction.thresholds.battery}%, Runtime: ${removedAction.thresholds.runtime}min`,
);
}
logger.log(` ${theme.dim('Changes saved and will be applied automatically')}`);
logger.log('');
} catch (error) {
logger.error(
`Failed to remove action: ${error instanceof Error ? error.message : String(error)}`,
);
process.exit(1);
}
}
/**
* List all actions for a specific UPS/group or all devices
*/
public async list(targetId?: string): Promise<void> {
try {
const config = await this.nupst.getDaemon().loadConfig();
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 && !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) {
this.displayTargetActions(ups, 'UPS');
} else {
this.displayTargetActions(group!, 'Group');
}
} else {
// List actions for all UPS devices and groups
logger.log('');
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.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|group-id>')
}`,
);
logger.log('');
}
}
} catch (error) {
logger.error(
`Failed to list actions: ${error instanceof Error ? error.message : String(error)}`,
);
process.exit(1);
}
}
private resolveActionTarget(
config: { upsDevices: IUpsConfig[]; groups?: IGroupConfig[] },
targetId: string,
): { target: IUpsConfig | IGroupConfig; targetType: 'UPS' | 'Group'; targetName: string } {
const ups = config.upsDevices.find((u) => u.id === targetId);
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);
}
return {
target: (ups || group)!,
targetType: ups ? 'UPS' : 'Group',
targetName: ups ? ups.name : group!.name,
};
}
private isClearInput(input: string): boolean {
return input.trim().toLowerCase() === 'clear';
}
private getActionTypeValue(action?: IActionConfig): number {
switch (action?.type) {
case 'webhook':
return 2;
case 'script':
return 3;
case 'proxmox':
return 4;
case 'shutdown':
default:
return 1;
}
}
private getTriggerModeValue(action?: IActionConfig): number {
switch (action?.triggerMode) {
case 'onlyPowerChanges':
return 1;
case 'powerChangesAndThresholds':
return 3;
case 'anyChange':
return 4;
case 'onlyThresholds':
default:
return 2;
}
}
private async promptForActionConfig(
prompt: (question: string) => Promise<string>,
existingAction?: IActionConfig,
): Promise<IActionConfig> {
logger.log(` ${theme.dim('Action Type:')}`);
logger.log(` ${theme.dim('1)')} Shutdown (system shutdown)`);
logger.log(` ${theme.dim('2)')} Webhook (HTTP notification)`);
logger.log(` ${theme.dim('3)')} Custom Script (run .sh file from /etc/nupst)`);
logger.log(
` ${theme.dim('4)')} Proxmox (gracefully shut down VMs/LXCs before host shutdown)`,
);
const defaultTypeValue = this.getActionTypeValue(existingAction);
const typeInput = await prompt(
` ${theme.dim('Select action type')} ${theme.dim(`[${defaultTypeValue}]:`)} `,
);
const typeValue = parseInt(typeInput, 10) || defaultTypeValue;
const newAction: Partial<IActionConfig> = {};
if (typeValue === 1) {
const shutdownAction = existingAction?.type === 'shutdown' ? existingAction : undefined;
const defaultShutdownDelay = this.nupst.getDaemon().getConfig().defaultShutdownDelay ??
SHUTDOWN.DEFAULT_DELAY_MINUTES;
newAction.type = 'shutdown';
const delayPrompt = shutdownAction?.shutdownDelay !== undefined
? ` ${theme.dim('Shutdown delay')} ${
theme.dim(
`(minutes, 'clear' = default ${defaultShutdownDelay}) [${shutdownAction.shutdownDelay}]:`,
)
} `
: ` ${theme.dim('Shutdown delay')} ${
theme.dim(`(minutes, leave empty for default ${defaultShutdownDelay}):`)
} `;
const delayInput = await prompt(delayPrompt);
if (this.isClearInput(delayInput)) {
// Leave unset so the config-level default is used.
} else if (delayInput.trim()) {
const shutdownDelay = parseInt(delayInput, 10);
if (isNaN(shutdownDelay) || shutdownDelay < 0) {
logger.error('Invalid shutdown delay. Must be >= 0.');
process.exit(1);
}
newAction.shutdownDelay = shutdownDelay;
} else if (shutdownAction?.shutdownDelay !== undefined) {
newAction.shutdownDelay = shutdownAction.shutdownDelay;
}
} else if (typeValue === 2) {
const webhookAction = existingAction?.type === 'webhook' ? existingAction : undefined;
newAction.type = 'webhook';
const webhookUrlInput = await prompt(
` ${theme.dim('Webhook URL')} ${
theme.dim(webhookAction?.webhookUrl ? `[${webhookAction.webhookUrl}]:` : ':')
} `,
);
const webhookUrl = webhookUrlInput.trim() || webhookAction?.webhookUrl || '';
if (!webhookUrl) {
logger.error('Webhook URL is required.');
process.exit(1);
}
newAction.webhookUrl = webhookUrl;
logger.log('');
logger.log(` ${theme.dim('HTTP Method:')}`);
logger.log(` ${theme.dim('1)')} POST (JSON body)`);
logger.log(` ${theme.dim('2)')} GET (query parameters)`);
const defaultMethodValue = webhookAction?.webhookMethod === 'GET' ? 2 : 1;
const methodInput = await prompt(
` ${theme.dim('Select method')} ${theme.dim(`[${defaultMethodValue}]:`)} `,
);
const methodValue = parseInt(methodInput, 10) || defaultMethodValue;
newAction.webhookMethod = methodValue === 2 ? 'GET' : 'POST';
const currentWebhookTimeout = webhookAction?.webhookTimeout;
const timeoutPrompt = currentWebhookTimeout !== undefined
? ` ${theme.dim('Timeout in seconds')} ${
theme.dim(`('clear' to unset) [${Math.floor(currentWebhookTimeout / 1000)}]:`)
} `
: ` ${theme.dim('Timeout in seconds')} ${theme.dim('[10]:')} `;
const timeoutInput = await prompt(timeoutPrompt);
if (this.isClearInput(timeoutInput)) {
// Leave unset.
} else if (timeoutInput.trim()) {
const timeout = parseInt(timeoutInput, 10);
if (isNaN(timeout) || timeout < 0) {
logger.error('Invalid webhook timeout. Must be >= 0.');
process.exit(1);
}
newAction.webhookTimeout = timeout * 1000;
} else if (currentWebhookTimeout !== undefined) {
newAction.webhookTimeout = currentWebhookTimeout;
}
} else if (typeValue === 3) {
const scriptAction = existingAction?.type === 'script' ? existingAction : undefined;
newAction.type = 'script';
const scriptPathInput = await prompt(
` ${theme.dim('Script filename (in /etc/nupst/, must end with .sh)')} ${
theme.dim(scriptAction?.scriptPath ? `[${scriptAction.scriptPath}]:` : ':')
} `,
);
const scriptPath = scriptPathInput.trim() || scriptAction?.scriptPath || '';
if (!scriptPath || !scriptPath.endsWith('.sh')) {
logger.error('Script path must end with .sh.');
process.exit(1);
}
newAction.scriptPath = scriptPath;
const currentScriptTimeout = scriptAction?.scriptTimeout;
const timeoutPrompt = currentScriptTimeout !== undefined
? ` ${theme.dim('Script timeout in seconds')} ${
theme.dim(`('clear' to unset) [${Math.floor(currentScriptTimeout / 1000)}]:`)
} `
: ` ${theme.dim('Script timeout in seconds')} ${theme.dim('[60]:')} `;
const timeoutInput = await prompt(timeoutPrompt);
if (this.isClearInput(timeoutInput)) {
// Leave unset.
} else if (timeoutInput.trim()) {
const timeout = parseInt(timeoutInput, 10);
if (isNaN(timeout) || timeout < 0) {
logger.error('Invalid script timeout. Must be >= 0.');
process.exit(1);
}
newAction.scriptTimeout = timeout * 1000;
} else if (currentScriptTimeout !== undefined) {
newAction.scriptTimeout = currentScriptTimeout;
}
} else if (typeValue === 4) {
const proxmoxAction = existingAction?.type === 'proxmox' ? existingAction : undefined;
const detection = ProxmoxAction.detectCliAvailability();
let useApiMode = false;
newAction.type = 'proxmox';
if (detection.available) {
logger.log('');
logger.success('Proxmox CLI tools detected (qm/pct).');
logger.dim(` qm: ${detection.qmPath}`);
logger.dim(` pct: ${detection.pctPath}`);
if (proxmoxAction) {
logger.log('');
logger.log(` ${theme.dim('Proxmox mode:')}`);
logger.log(` ${theme.dim('1)')} CLI (local qm/pct tools)`);
logger.log(` ${theme.dim('2)')} API (REST token authentication)`);
const defaultModeValue = proxmoxAction.proxmoxMode === 'api' ? 2 : 1;
const modeInput = await prompt(
` ${theme.dim('Select Proxmox mode')} ${theme.dim(`[${defaultModeValue}]:`)} `,
);
const modeValue = parseInt(modeInput, 10) || defaultModeValue;
useApiMode = modeValue === 2;
}
} else {
logger.log('');
if (!detection.isRoot) {
logger.warn('Not running as root - CLI mode unavailable, using API mode.');
} else {
logger.warn('Proxmox CLI tools (qm/pct) not found - using API mode.');
}
useApiMode = true;
}
if (useApiMode) {
logger.log('');
logger.info('Proxmox API Settings:');
logger.dim('Create a token with: pveum user token add root@pam nupst --privsep=0');
const currentHost = proxmoxAction?.proxmoxHost || 'localhost';
const pxHost = await prompt(
` ${theme.dim('Proxmox Host')} ${theme.dim(`[${currentHost}]:`)} `,
);
newAction.proxmoxHost = pxHost.trim() || currentHost;
const currentPort = proxmoxAction?.proxmoxPort || 8006;
const pxPortInput = await prompt(
` ${theme.dim('Proxmox API Port')} ${theme.dim(`[${currentPort}]:`)} `,
);
const pxPort = parseInt(pxPortInput, 10);
newAction.proxmoxPort = pxPortInput.trim() && !isNaN(pxPort) ? pxPort : currentPort;
const pxNodePrompt = proxmoxAction?.proxmoxNode
? ` ${theme.dim('Proxmox Node Name')} ${
theme.dim(`('clear' = auto-detect) [${proxmoxAction.proxmoxNode}]:`)
} `
: ` ${theme.dim('Proxmox Node Name')} ${theme.dim('(empty = auto-detect):')} `;
const pxNode = await prompt(pxNodePrompt);
if (this.isClearInput(pxNode)) {
// Leave unset so hostname auto-detection is used.
} else if (pxNode.trim()) {
newAction.proxmoxNode = pxNode.trim();
} else if (proxmoxAction?.proxmoxNode) {
newAction.proxmoxNode = proxmoxAction.proxmoxNode;
}
const currentTokenId = proxmoxAction?.proxmoxTokenId || '';
const tokenIdInput = await prompt(
` ${theme.dim('API Token ID (e.g., root@pam!nupst)')} ${
theme.dim(currentTokenId ? `[${currentTokenId}]:` : ':')
} `,
);
const tokenId = tokenIdInput.trim() || currentTokenId;
if (!tokenId) {
logger.error('Token ID is required for API mode.');
process.exit(1);
}
newAction.proxmoxTokenId = tokenId;
const currentTokenSecret = proxmoxAction?.proxmoxTokenSecret || '';
const tokenSecretInput = await prompt(
` ${theme.dim('API Token Secret')} ${theme.dim(currentTokenSecret ? '[*****]:' : ':')} `,
);
const tokenSecret = tokenSecretInput.trim() || currentTokenSecret;
if (!tokenSecret) {
logger.error('Token Secret is required for API mode.');
process.exit(1);
}
newAction.proxmoxTokenSecret = tokenSecret;
const defaultInsecure = proxmoxAction?.proxmoxInsecure !== false;
const insecureInput = await prompt(
` ${theme.dim('Skip TLS verification (self-signed cert)?')} ${
theme.dim(defaultInsecure ? '(Y/n):' : '(y/N):')
} `,
);
newAction.proxmoxInsecure = insecureInput.trim()
? insecureInput.toLowerCase() !== 'n'
: defaultInsecure;
newAction.proxmoxMode = 'api';
} else {
newAction.proxmoxMode = 'cli';
}
const currentExcludeIds = proxmoxAction?.proxmoxExcludeIds || [];
const excludePrompt = currentExcludeIds.length > 0
? ` ${theme.dim('VM/CT IDs to exclude')} ${
theme.dim(`(comma-separated, 'clear' = none) [${currentExcludeIds.join(',')}]:`)
} `
: ` ${theme.dim('VM/CT IDs to exclude (comma-separated, or empty):')} `;
const excludeInput = await prompt(excludePrompt);
if (this.isClearInput(excludeInput)) {
newAction.proxmoxExcludeIds = [];
} else if (excludeInput.trim()) {
newAction.proxmoxExcludeIds = excludeInput.split(',').map((s) => parseInt(s.trim(), 10))
.filter((n) => !isNaN(n));
} else if (currentExcludeIds.length > 0) {
newAction.proxmoxExcludeIds = [...currentExcludeIds];
}
const currentStopTimeout = proxmoxAction?.proxmoxStopTimeout;
const stopTimeoutPrompt = currentStopTimeout !== undefined
? ` ${theme.dim('VM shutdown timeout in seconds')} ${
theme.dim(`('clear' to unset) [${currentStopTimeout}]:`)
} `
: ` ${theme.dim('VM shutdown timeout in seconds')} ${theme.dim('[120]:')} `;
const timeoutInput = await prompt(stopTimeoutPrompt);
if (this.isClearInput(timeoutInput)) {
// Leave unset.
} else if (timeoutInput.trim()) {
const stopTimeout = parseInt(timeoutInput, 10);
if (isNaN(stopTimeout) || stopTimeout < 0) {
logger.error('Invalid VM shutdown timeout. Must be >= 0.');
process.exit(1);
}
newAction.proxmoxStopTimeout = stopTimeout;
} else if (currentStopTimeout !== undefined) {
newAction.proxmoxStopTimeout = currentStopTimeout;
}
const defaultForceStop = proxmoxAction?.proxmoxForceStop !== false;
const forceInput = await prompt(
` ${theme.dim("Force-stop VMs that don't shut down in time?")} ${
theme.dim(defaultForceStop ? '(Y/n):' : '(y/N):')
} `,
);
newAction.proxmoxForceStop = forceInput.trim()
? forceInput.toLowerCase() !== 'n'
: defaultForceStop;
const defaultHaPolicyValue = proxmoxAction?.proxmoxHaPolicy === 'haStop' ? 2 : 1;
const haPolicyInput = await prompt(
` ${theme.dim('HA-managed guest handling')} ${
theme.dim(`([1] none, 2 haStop) [${defaultHaPolicyValue}]:`)
} `,
);
const haPolicyValue = parseInt(haPolicyInput, 10) || defaultHaPolicyValue;
newAction.proxmoxHaPolicy = haPolicyValue === 2 ? 'haStop' : 'none';
} else {
logger.error('Invalid action type.');
process.exit(1);
}
logger.log('');
const defaultBatteryThreshold = existingAction?.thresholds?.battery ?? 60;
const batteryInput = await prompt(
` ${theme.dim('Battery threshold')} ${theme.dim(`(%) [${defaultBatteryThreshold}]:`)} `,
);
const battery = batteryInput.trim() ? parseInt(batteryInput, 10) : defaultBatteryThreshold;
if (isNaN(battery) || battery < 0 || battery > 100) {
logger.error('Invalid battery threshold. Must be 0-100.');
process.exit(1);
}
const defaultRuntimeThreshold = existingAction?.thresholds?.runtime ?? 20;
const runtimeInput = await prompt(
` ${theme.dim('Runtime threshold')} ${
theme.dim(`(minutes) [${defaultRuntimeThreshold}]:`)
} `,
);
const runtime = runtimeInput.trim() ? parseInt(runtimeInput, 10) : defaultRuntimeThreshold;
if (isNaN(runtime) || runtime < 0) {
logger.error('Invalid runtime threshold. Must be >= 0.');
process.exit(1);
}
newAction.thresholds = { battery, runtime };
logger.log('');
logger.log(` ${theme.dim('Trigger mode:')}`);
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`,
);
logger.log(` ${theme.dim('4)')} anyChange - Trigger on any status change`);
const defaultTriggerValue = this.getTriggerModeValue(existingAction);
const triggerChoice = await prompt(
` ${theme.dim('Choice')} ${theme.dim(`[${defaultTriggerValue}]:`)} `,
);
const triggerValue = parseInt(triggerChoice, 10) || defaultTriggerValue;
const triggerModeMap: Record<number, NonNullable<IActionConfig['triggerMode']>> = {
1: 'onlyPowerChanges',
2: 'onlyThresholds',
3: 'powerChangesAndThresholds',
4: 'anyChange',
};
newAction.triggerMode = triggerModeMap[triggerValue] || 'onlyThresholds';
return newAction as IActionConfig;
}
/**
* Display actions for a single UPS or Group
*/
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 (!target.actions || target.actions.length === 0) {
logger.log(` ${theme.dim('No actions configured')}`);
logger.log('');
return;
}
const columns: ITableColumn[] = [
{ header: 'Index', key: 'index', align: 'right' },
{ header: 'Type', key: 'type', align: 'left' },
{ header: 'Battery', key: 'battery', align: 'right' },
{ header: 'Runtime', key: 'runtime', align: 'right' },
{ header: 'Trigger Mode', key: 'triggerMode', align: 'left' },
{ header: 'Details', key: 'details', align: 'left' },
];
const rows = target.actions.map((action, index) => {
const defaultShutdownDelay = this.nupst.getDaemon().getConfig().defaultShutdownDelay ??
SHUTDOWN.DEFAULT_DELAY_MINUTES;
let details = `${action.shutdownDelay ?? defaultShutdownDelay}min delay`;
if (action.type === 'proxmox') {
const mode = action.proxmoxMode || 'auto';
if (mode === 'cli' || (mode === 'auto' && !action.proxmoxTokenId)) {
details = 'CLI mode';
} else {
const host = action.proxmoxHost || 'localhost';
const port = action.proxmoxPort || 8006;
details = `API ${host}:${port}`;
}
if (action.proxmoxExcludeIds?.length) {
details += `, excl: ${action.proxmoxExcludeIds.join(',')}`;
}
if (action.proxmoxHaPolicy === 'haStop') {
details += ', haStop';
}
} else if (action.type === 'webhook') {
details = action.webhookUrl || theme.dim('N/A');
} else if (action.type === 'script') {
details = action.scriptPath || theme.dim('N/A');
}
return {
index: theme.dim(index.toString()),
type: theme.highlight(action.type),
battery: action.thresholds ? `${action.thresholds.battery}%` : theme.dim('N/A'),
runtime: action.thresholds ? `${action.thresholds.runtime}min` : theme.dim('N/A'),
triggerMode: theme.dim(action.triggerMode || 'onlyThresholds'),
details,
};
});
logger.logTable(columns, rows);
logger.log('');
}
}