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 { try { 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 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 { 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, ): Promise { if (!targetId || !actionIndexStr) { logger.error('Target ID and action index are required'); logger.log( ` ${theme.dim('Usage:')} ${ theme.command('nupst action edit ') }`, ); 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 { try { if (!targetId || !actionIndexStr) { logger.error('Target ID and action index are required'); logger.log( ` ${theme.dim('Usage:')} ${ theme.command('nupst action remove ') }`, ); 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 { 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 ') }`, ); 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, existingAction?: IActionConfig, ): Promise { 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 = {}; 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> = { 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(''); } }