feat(core): Centralize timeouts/constants, add CLI prompt helpers, and introduce webhook/script actions with safety and SNMP refactors

This commit is contained in:
2026-01-29 17:04:12 +00:00
parent d0e3a4ae74
commit 07648b4880
24 changed files with 1019 additions and 590 deletions

View File

@@ -13,6 +13,7 @@ import { ScriptAction } from './script-action.ts';
// Re-export types for convenience
export type { IActionConfig, IActionContext, TPowerStatus } from './base-action.ts';
export type { IWebhookPayload } from './webhook-action.ts';
export { Action } from './base-action.ts';
export { ShutdownAction } from './shutdown-action.ts';
export { WebhookAction } from './webhook-action.ts';

View File

@@ -1,8 +1,9 @@
import * as fs from 'node:fs';
import { execFile } from 'node:child_process';
import { promisify } from 'node:util';
import { Action, type IActionConfig, type IActionContext } from './base-action.ts';
import { Action, type IActionContext } from './base-action.ts';
import { logger } from '../logger.ts';
import { SHUTDOWN, UI } from '../constants.ts';
const execFileAsync = promisify(execFile);
@@ -15,6 +16,81 @@ const execFileAsync = promisify(execFile);
export class ShutdownAction extends Action {
readonly type = 'shutdown';
/**
* Override shouldExecute to add shutdown-specific safety checks
*
* Key safety rules:
* 1. Shutdown should NEVER trigger unless UPS is actually on battery
* (low battery while on grid power is not an emergency - it's charging)
* 2. For power status changes, only trigger on transitions TO onBattery from online
* (ignore unknown → online at startup, and power restoration events)
* 3. For threshold violations, verify UPS is on battery before acting
*
* @param context Action context with UPS state
* @returns True if shutdown should execute
*/
protected override shouldExecute(context: IActionContext): boolean {
const mode = this.config.triggerMode || 'powerChangesAndThresholds';
// CRITICAL SAFETY CHECK: Shutdown should NEVER trigger unless UPS is on battery
// A low battery while on grid power is not an emergency (the battery is charging)
if (context.powerStatus !== 'onBattery') {
logger.info(`Shutdown action skipped: UPS is not on battery (status: ${context.powerStatus})`);
return false;
}
// Handle threshold violations (UPS is confirmed on battery at this point)
if (context.triggerReason === 'thresholdViolation') {
// 'onlyPowerChanges' mode ignores thresholds
if (mode === 'onlyPowerChanges') {
logger.info('Shutdown action skipped: triggerMode is onlyPowerChanges, ignoring threshold');
return false;
}
// Check if thresholds are actually exceeded
return this.areThresholdsExceeded(context.batteryCapacity, context.batteryRuntime);
}
// Handle power status changes
if (context.triggerReason === 'powerStatusChange') {
// 'onlyThresholds' mode ignores power status changes
if (mode === 'onlyThresholds') {
logger.info('Shutdown action skipped: triggerMode is onlyThresholds, ignoring power change');
return false;
}
const prev = context.previousPowerStatus;
// Only trigger on transitions TO onBattery from online (real power loss)
if (prev === 'online') {
logger.info('Shutdown action triggered: power loss detected (online → onBattery)');
return true;
}
// For unknown → onBattery (daemon started while on battery):
// This is a startup scenario - be cautious. The user may have just started
// the daemon for testing, or the UPS may have been on battery for a while.
// Only trigger if mode explicitly includes power changes.
if (prev === 'unknown') {
if (mode === 'onlyPowerChanges' || mode === 'powerChangesAndThresholds' || mode === 'anyChange') {
logger.info('Shutdown action triggered: UPS on battery at daemon startup (unknown → onBattery)');
return true;
}
return false;
}
// Other transitions (e.g., onBattery → onBattery) should not trigger
logger.info(`Shutdown action skipped: non-emergency transition (${prev}${context.powerStatus})`);
return false;
}
// For 'anyChange' mode, always execute (UPS is already confirmed on battery)
if (mode === 'anyChange') {
return true;
}
return false;
}
/**
* Execute the shutdown action
* @param context Action context with UPS state
@@ -26,10 +102,10 @@ export class ShutdownAction extends Action {
return;
}
const shutdownDelay = this.config.shutdownDelay || 5; // Default 5 minutes
const shutdownDelay = this.config.shutdownDelay || SHUTDOWN.DEFAULT_DELAY_MINUTES;
logger.log('');
logger.logBoxTitle('Initiating System Shutdown', 60, 'error');
logger.logBoxTitle('Initiating System Shutdown', UI.WIDE_BOX_WIDTH, 'error');
logger.logBoxLine(`UPS: ${context.upsName} (${context.upsId})`);
logger.logBoxLine(`Power Status: ${context.powerStatus}`);
logger.logBoxLine(`Battery: ${context.batteryCapacity}%`);

View File

@@ -3,6 +3,32 @@ import * as https from 'node:https';
import { URL } from 'node:url';
import { Action, type IActionConfig, type IActionContext } from './base-action.ts';
import { logger } from '../logger.ts';
import { WEBHOOK } from '../constants.ts';
/**
* Payload sent to webhook endpoints
*/
export interface IWebhookPayload {
/** UPS ID */
upsId: string;
/** UPS name */
upsName: string;
/** Current power status */
powerStatus: 'online' | 'onBattery' | 'unknown';
/** Current battery capacity percentage */
batteryCapacity: number;
/** Current battery runtime in minutes */
batteryRuntime: number;
/** Reason this webhook was triggered */
triggerReason: 'powerStatusChange' | 'thresholdViolation';
/** Timestamp when webhook was triggered */
timestamp: number;
/** Thresholds configured for this action (if any) */
thresholds?: {
battery: number;
runtime: number;
};
}
/**
* WebhookAction - Calls an HTTP webhook with UPS state information
@@ -30,7 +56,7 @@ export class WebhookAction extends Action {
}
const method = this.config.webhookMethod || 'POST';
const timeout = this.config.webhookTimeout || 10000;
const timeout = this.config.webhookTimeout || WEBHOOK.DEFAULT_TIMEOUT_MS;
logger.info(`Calling webhook: ${method} ${this.config.webhookUrl}`);
@@ -56,7 +82,7 @@ export class WebhookAction extends Action {
method: 'GET' | 'POST',
timeout: number,
): Promise<void> {
const payload: any = {
const payload: IWebhookPayload = {
upsId: context.upsId,
upsName: context.upsName,
powerStatus: context.powerStatus,