feat(core): Centralize timeouts/constants, add CLI prompt helpers, and introduce webhook/script actions with safety and SNMP refactors
This commit is contained in:
@@ -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';
|
||||
|
||||
@@ -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}%`);
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user