Compare commits

...

5 Commits

Author SHA1 Message Date
a7113d0387 fix(daemon): replace require() with ES6 imports for Deno compatibility
All checks were successful
CI / Type Check & Lint (push) Successful in 4s
CI / Build Test (Current Platform) (push) Successful in 4s
Release / build-and-release (push) Successful in 47s
CI / Build All Platforms (push) Successful in 49s
- Add proper ES6 imports at top of file for theme, symbols, colors
- Remove all require() calls that were causing 'require is not defined' errors
- Daemon now starts properly with modernized logging intact
2025-10-20 01:43:09 +00:00
61d4e9037a chore(release): bump version to 4.1.6
Some checks failed
CI / Type Check & Lint (push) Failing after 5s
CI / Build Test (Current Platform) (push) Successful in 4s
Release / build-and-release (push) Successful in 44s
CI / Build All Platforms (push) Successful in 49s
2025-10-20 01:38:52 +00:00
caced2718f feat(daemon): modernize daemon logging with tables and color-coded output
- Modernize periodic status update with logger.logTable() and color-coded battery/runtime
- Modernize configuration loaded display with tables for UPS devices and groups
- Enhance power status change notifications with better colors and timestamps
- Modernize shutdown monitoring with real-time table display of UPS status
- Add color-coded CRITICAL indicators for emergency conditions
- Improve visual hierarchy with appropriate box styles (info, warning, error, success)
- Ensure consistent theming across all daemon log output
2025-10-20 01:38:44 +00:00
8516056f84 chore(release): bump version to 4.1.5
All checks were successful
CI / Type Check & Lint (push) Successful in 5s
CI / Build Test (Current Platform) (push) Successful in 4s
Release / build-and-release (push) Successful in 44s
CI / Build All Platforms (push) Successful in 48s
2025-10-20 01:31:43 +00:00
07ec9d7595 feat(cli): modernize all CLI output to use logger tables
- Modernize ups list command with logger.logTable()
- Modernize group list command with logger.logTable()
- Completely rewrite config show with tables and proper box styling
- Add professional column definitions with themed colors
- Replace all manual table formatting (padEnd, pipe separators)
- Improve visual hierarchy with appropriate box styles (info, warning, success)
- Ensure consistent theming across all CLI commands
2025-10-20 01:30:57 +00:00
5 changed files with 379 additions and 264 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "@serve.zone/nupst", "name": "@serve.zone/nupst",
"version": "4.1.4", "version": "4.1.6",
"exports": "./mod.ts", "exports": "./mod.ts",
"tasks": { "tasks": {
"dev": "deno run --allow-all mod.ts", "dev": "deno run --allow-all mod.ts",

252
ts/cli.ts
View File

@@ -1,6 +1,6 @@
import { execSync } from 'node:child_process'; import { execSync } from 'node:child_process';
import { Nupst } from './nupst.ts'; import { Nupst } from './nupst.ts';
import { logger } from './logger.ts'; import { logger, type ITableColumn } from './logger.ts';
import { theme, symbols } from './colors.ts'; import { theme, symbols } from './colors.ts';
/** /**
@@ -303,154 +303,164 @@ export class NupstCli {
try { try {
await this.nupst.getDaemon().loadConfig(); await this.nupst.getDaemon().loadConfig();
} catch (_error) { } catch (_error) {
const errorBoxWidth = 45; logger.logBox('Configuration Error', [
logger.logBoxTitle('Configuration Error', errorBoxWidth); 'No configuration found.',
logger.logBoxLine('No configuration found.'); "Please run 'nupst ups add' first to create a configuration.",
logger.logBoxLine("Please run 'nupst setup' first to create a configuration."); ], 50, 'error');
logger.logBoxEnd();
return; return;
} }
// Get current configuration // Get current configuration
const config = this.nupst.getDaemon().getConfig(); const config = this.nupst.getDaemon().getConfig();
const boxWidth = 50;
logger.logBoxTitle('NUPST Configuration', boxWidth);
// Check if multi-UPS config // Check if multi-UPS config
if (config.upsDevices && Array.isArray(config.upsDevices)) { if (config.upsDevices && Array.isArray(config.upsDevices)) {
// Multi-UPS configuration // === Multi-UPS Configuration ===
logger.logBoxLine(`UPS Devices: ${config.upsDevices.length}`);
logger.logBoxLine(`Groups: ${config.groups ? config.groups.length : 0}`); // Overview Box
logger.logBoxLine(`Check Interval: ${config.checkInterval / 1000} seconds`); logger.log('');
logger.logBoxLine(''); logger.logBox('NUPST Configuration', [
logger.logBoxLine('Configuration File Location:'); `UPS Devices: ${theme.highlight(String(config.upsDevices.length))}`,
logger.logBoxLine(' /etc/nupst/config.json'); `Groups: ${theme.highlight(String(config.groups ? config.groups.length : 0))}`,
logger.logBoxEnd(); `Check Interval: ${theme.info(String(config.checkInterval / 1000))} seconds`,
'',
theme.dim('Configuration File:'),
` ${theme.path('/etc/nupst/config.json')}`,
], 60, 'info');
// Show UPS devices // UPS Devices Table
if (config.upsDevices.length > 0) { if (config.upsDevices.length > 0) {
logger.logBoxTitle('UPS Devices', boxWidth); const upsRows = config.upsDevices.map((ups) => ({
for (const ups of config.upsDevices) { name: ups.name,
logger.logBoxLine(`${ups.name} (${ups.id}):`); id: theme.dim(ups.id),
logger.logBoxLine(` Host: ${ups.snmp.host}:${ups.snmp.port}`); host: `${ups.snmp.host}:${ups.snmp.port}`,
logger.logBoxLine(` Model: ${ups.snmp.upsModel}`); model: ups.snmp.upsModel || 'cyberpower',
logger.logBoxLine( thresholds: `${ups.thresholds.battery}% / ${ups.thresholds.runtime}min`,
` Thresholds: ${ups.thresholds.battery}% battery, ${ups.thresholds.runtime} min runtime`, groups: ups.groups.length > 0 ? ups.groups.join(', ') : theme.dim('None'),
); }));
logger.logBoxLine(
` Groups: ${ups.groups.length > 0 ? ups.groups.join(', ') : 'None'}`, const upsColumns: ITableColumn[] = [
); { header: 'Name', key: 'name', align: 'left', color: theme.highlight },
logger.logBoxLine(''); { header: 'ID', key: 'id', align: 'left' },
} { header: 'Host:Port', key: 'host', align: 'left', color: theme.info },
logger.logBoxEnd(); { header: 'Model', key: 'model', align: 'left' },
{ header: 'Battery/Runtime', key: 'thresholds', align: 'left' },
{ header: 'Groups', key: 'groups', align: 'left' },
];
logger.log('');
logger.info(`UPS Devices (${config.upsDevices.length}):`);
logger.log('');
logger.logTable(upsColumns, upsRows);
} }
// Show groups // Groups Table
if (config.groups && config.groups.length > 0) { if (config.groups && config.groups.length > 0) {
logger.logBoxTitle('UPS Groups', boxWidth); const groupRows = config.groups.map((group) => {
for (const group of config.groups) {
logger.logBoxLine(`${group.name} (${group.id}):`);
logger.logBoxLine(` Mode: ${group.mode}`);
if (group.description) {
logger.logBoxLine(` Description: ${group.description}`);
}
// List UPS devices in this group
const upsInGroup = config.upsDevices.filter((ups) => const upsInGroup = config.upsDevices.filter((ups) =>
ups.groups && ups.groups.includes(group.id) ups.groups && ups.groups.includes(group.id)
); );
logger.logBoxLine( return {
` UPS Devices: ${ name: group.name,
upsInGroup.length > 0 ? upsInGroup.map((ups) => ups.name).join(', ') : 'None' id: theme.dim(group.id),
}`, mode: group.mode,
); upsCount: String(upsInGroup.length),
logger.logBoxLine(''); ups: upsInGroup.length > 0
} ? upsInGroup.map((ups) => ups.name).join(', ')
logger.logBoxEnd(); : theme.dim('None'),
description: group.description || theme.dim('—'),
};
});
const groupColumns: ITableColumn[] = [
{ header: 'Name', key: 'name', align: 'left', color: theme.highlight },
{ header: 'ID', key: 'id', align: 'left' },
{ header: 'Mode', key: 'mode', align: 'left', color: theme.info },
{ header: 'UPS', key: 'upsCount', align: 'right' },
{ header: 'UPS Devices', key: 'ups', align: 'left' },
{ header: 'Description', key: 'description', align: 'left' },
];
logger.log('');
logger.info(`UPS Groups (${config.groups.length}):`);
logger.log('');
logger.logTable(groupColumns, groupRows);
} }
} else { } else {
// Legacy single UPS configuration // === Legacy Single UPS Configuration ===
if (!config.snmp) {
logger.logBoxLine('Error: Legacy configuration missing SNMP settings'); if (!config.snmp || !config.thresholds) {
} else { logger.logBox('Configuration Error', [
// SNMP Settings 'Error: Legacy configuration missing SNMP or threshold settings',
logger.logBoxLine('SNMP Settings:'); ], 60, 'error');
logger.logBoxLine(` Host: ${config.snmp.host}`); return;
logger.logBoxLine(` Port: ${config.snmp.port}`);
logger.logBoxLine(` Version: ${config.snmp.version}`);
logger.logBoxLine(` UPS Model: ${config.snmp.upsModel || 'cyberpower'}`);
if (config.snmp.version === 1 || config.snmp.version === 2) {
logger.logBoxLine(` Community: ${config.snmp.community}`);
} else if (config.snmp.version === 3) {
logger.logBoxLine(` Security Level: ${config.snmp.securityLevel}`);
logger.logBoxLine(` Username: ${config.snmp.username}`);
// Show auth and privacy details based on security level
if (
config.snmp.securityLevel === 'authNoPriv' ||
config.snmp.securityLevel === 'authPriv'
) {
logger.logBoxLine(` Auth Protocol: ${config.snmp.authProtocol || 'None'}`);
}
if (config.snmp.securityLevel === 'authPriv') {
logger.logBoxLine(` Privacy Protocol: ${config.snmp.privProtocol || 'None'}`);
}
// Show timeout value
logger.logBoxLine(` Timeout: ${config.snmp.timeout / 1000} seconds`);
}
// Show OIDs if custom model is selected
if (config.snmp.upsModel === 'custom' && config.snmp.customOIDs) {
logger.logBoxLine('Custom OIDs:');
logger.logBoxLine(
` Power Status: ${config.snmp.customOIDs.POWER_STATUS || 'Not set'}`,
);
logger.logBoxLine(
` Battery Capacity: ${config.snmp.customOIDs.BATTERY_CAPACITY || 'Not set'}`,
);
logger.logBoxLine(
` Battery Runtime: ${config.snmp.customOIDs.BATTERY_RUNTIME || 'Not set'}`,
);
}
} }
// Thresholds logger.log('');
if (!config.thresholds) { logger.logBox('NUPST Configuration (Legacy)', [
logger.logBoxLine('Error: Legacy configuration missing threshold settings'); theme.warning('Legacy single-UPS configuration format'),
} else { '',
logger.logBoxLine('Thresholds:'); theme.dim('SNMP Settings:'),
logger.logBoxLine(` Battery: ${config.thresholds.battery}%`); ` Host: ${theme.info(config.snmp.host)}`,
logger.logBoxLine(` Runtime: ${config.thresholds.runtime} minutes`); ` Port: ${theme.info(String(config.snmp.port))}`,
} ` Version: ${config.snmp.version}`,
logger.logBoxLine(`Check Interval: ${config.checkInterval / 1000} seconds`); ` UPS Model: ${config.snmp.upsModel || 'cyberpower'}`,
...(config.snmp.version === 1 || config.snmp.version === 2
// Configuration file location ? [` Community: ${config.snmp.community}`]
logger.logBoxLine(''); : []
logger.logBoxLine('Configuration File Location:'); ),
logger.logBoxLine(' /etc/nupst/config.json'); ...(config.snmp.version === 3
logger.logBoxLine(''); ? [
logger.logBoxLine('Note: Using legacy single-UPS configuration format.'); ` Security Level: ${config.snmp.securityLevel}`,
logger.logBoxLine('Consider using "nupst add" to migrate to multi-UPS format.'); ` Username: ${config.snmp.username}`,
...(config.snmp.securityLevel === 'authNoPriv' || config.snmp.securityLevel === 'authPriv'
logger.logBoxEnd(); ? [` Auth Protocol: ${config.snmp.authProtocol || 'None'}`]
: []
),
...(config.snmp.securityLevel === 'authPriv'
? [` Privacy Protocol: ${config.snmp.privProtocol || 'None'}`]
: []
),
` Timeout: ${config.snmp.timeout / 1000} seconds`,
]
: []
),
...(config.snmp.upsModel === 'custom' && config.snmp.customOIDs
? [
theme.dim('Custom OIDs:'),
` Power Status: ${config.snmp.customOIDs.POWER_STATUS || 'Not set'}`,
` Battery Capacity: ${config.snmp.customOIDs.BATTERY_CAPACITY || 'Not set'}`,
` Battery Runtime: ${config.snmp.customOIDs.BATTERY_RUNTIME || 'Not set'}`,
]
: []
),
'',
theme.dim('Thresholds:'),
` Battery: ${theme.highlight(String(config.thresholds.battery))}%`,
` Runtime: ${theme.highlight(String(config.thresholds.runtime))} minutes`,
` Check Interval: ${config.checkInterval / 1000} seconds`,
'',
theme.dim('Configuration File:'),
` ${theme.path('/etc/nupst/config.json')}`,
'',
theme.warning('Note: Using legacy single-UPS configuration format.'),
`Consider using ${theme.command('nupst ups add')} to migrate to multi-UPS format.`,
], 70, 'warning');
} }
// Show service status // Service Status
try { try {
const isActive = const isActive =
execSync('systemctl is-active nupst.service || true').toString().trim() === 'active'; execSync('systemctl is-active nupst.service || true').toString().trim() === 'active';
const isEnabled = const isEnabled =
execSync('systemctl is-enabled nupst.service || true').toString().trim() === 'enabled'; execSync('systemctl is-enabled nupst.service || true').toString().trim() === 'enabled';
const statusBoxWidth = 45; logger.log('');
logger.logBoxTitle('Service Status', statusBoxWidth); logger.logBox('Service Status', [
logger.logBoxLine(`Service Active: ${isActive ? 'Yes' : 'No'}`); `Active: ${isActive ? theme.success('Yes') : theme.dim('No')}`,
logger.logBoxLine(`Service Enabled: ${isEnabled ? 'Yes' : 'No'}`); `Enabled: ${isEnabled ? theme.success('Yes') : theme.dim('No')}`,
logger.logBoxEnd(); ], 50, isActive ? 'success' : 'default');
logger.log('');
} catch (_error) { } catch (_error) {
// Ignore errors checking service status // Ignore errors checking service status
} }

View File

@@ -1,6 +1,7 @@
import process from 'node:process'; import process from 'node:process';
import { Nupst } from '../nupst.ts'; import { Nupst } from '../nupst.ts';
import { logger } from '../logger.ts'; import { logger, type ITableColumn } from '../logger.ts';
import { theme } from '../colors.ts';
import * as helpers from '../helpers/index.ts'; import * as helpers from '../helpers/index.ts';
import { type IGroupConfig } from '../daemon.ts'; import { type IGroupConfig } from '../daemon.ts';
@@ -28,11 +29,10 @@ export class GroupHandler {
try { try {
await this.nupst.getDaemon().loadConfig(); await this.nupst.getDaemon().loadConfig();
} catch (error) { } catch (error) {
const errorBoxWidth = 45; logger.logBox('Configuration Error', [
logger.logBoxTitle('Configuration Error', errorBoxWidth); 'No configuration found.',
logger.logBoxLine('No configuration found.'); "Please run 'nupst ups add' first to create a configuration.",
logger.logBoxLine("Please run 'nupst setup' first to create a configuration."); ], 50, 'error');
logger.logBoxEnd();
return; return;
} }
@@ -41,43 +41,53 @@ export class GroupHandler {
// Check if multi-UPS config // Check if multi-UPS config
if (!config.groups || !Array.isArray(config.groups)) { if (!config.groups || !Array.isArray(config.groups)) {
// Legacy or missing groups configuration logger.logBox('UPS Groups', [
const boxWidth = 45; 'No groups configured.',
logger.logBoxTitle('UPS Groups', boxWidth); '',
logger.logBoxLine('No groups configured.'); `${theme.dim('Run')} ${theme.command('nupst group add')} ${theme.dim('to add a group')}`,
logger.logBoxLine('Use "nupst group add" to add a UPS group.'); ], 50, 'info');
logger.logBoxEnd();
return; return;
} }
// Display group list // Display group list with modern table
const boxWidth = 60;
logger.logBoxTitle('UPS Groups', boxWidth);
if (config.groups.length === 0) { if (config.groups.length === 0) {
logger.logBoxLine('No UPS groups configured.'); logger.logBox('UPS Groups', [
logger.logBoxLine('Use "nupst group add" to add a UPS group.'); 'No UPS groups configured.',
} else { '',
logger.logBoxLine(`Found ${config.groups.length} group(s)`); `${theme.dim('Run')} ${theme.command('nupst group add')} ${theme.dim('to add a group')}`,
logger.logBoxLine(''); ], 60, 'info');
logger.logBoxLine('ID | Name | Mode | UPS Devices'); return;
logger.logBoxLine('-----------+----------------------+--------------+----------------');
for (const group of config.groups) {
const id = group.id.padEnd(10, ' ').substring(0, 10);
const name = (group.name || '').padEnd(20, ' ').substring(0, 20);
const mode = (group.mode || 'unknown').padEnd(12, ' ').substring(0, 12);
// Count UPS devices in this group
const upsInGroup = config.upsDevices.filter((ups) => ups.groups.includes(group.id));
const upsCount = upsInGroup.length;
const upsNames = upsInGroup.map((ups) => ups.name).join(', ');
logger.logBoxLine(`${id} | ${name} | ${mode} | ${upsCount > 0 ? upsNames : 'None'}`);
}
} }
logger.logBoxEnd(); // Prepare table data
const rows = config.groups.map((group) => {
// Count UPS devices in this group
const upsInGroup = config.upsDevices.filter((ups) => ups.groups.includes(group.id));
const upsCount = upsInGroup.length;
const upsNames = upsInGroup.map((ups) => ups.name).join(', ');
return {
id: group.id,
name: group.name || '',
mode: group.mode || 'unknown',
count: String(upsCount),
devices: upsCount > 0 ? upsNames : theme.dim('None'),
};
});
const columns: ITableColumn[] = [
{ header: 'ID', key: 'id', align: 'left', color: theme.highlight },
{ header: 'Name', key: 'name', align: 'left' },
{ header: 'Mode', key: 'mode', align: 'left', color: theme.info },
{ header: 'UPS Count', key: 'count', align: 'right' },
{ header: 'UPS Devices', key: 'devices', align: 'left' },
];
logger.log('');
logger.info(`UPS Groups (${config.groups.length}):`);
logger.log('');
logger.logTable(columns, rows);
logger.log('');
} catch (error) { } catch (error) {
logger.error( logger.error(
`Failed to list UPS groups: ${error instanceof Error ? error.message : String(error)}`, `Failed to list UPS groups: ${error instanceof Error ? error.message : String(error)}`,

View File

@@ -1,7 +1,8 @@
import process from 'node:process'; import process from 'node:process';
import { execSync } from 'node:child_process'; import { execSync } from 'node:child_process';
import { Nupst } from '../nupst.ts'; import { Nupst } from '../nupst.ts';
import { logger } from '../logger.ts'; import { logger, type ITableColumn } from '../logger.ts';
import { theme } from '../colors.ts';
import * as helpers from '../helpers/index.ts'; import * as helpers from '../helpers/index.ts';
import type { TUpsModel } from '../snmp/types.ts'; import type { TUpsModel } from '../snmp/types.ts';
import type { INupstConfig } from '../daemon.ts'; import type { INupstConfig } from '../daemon.ts';
@@ -379,11 +380,10 @@ export class UpsHandler {
try { try {
await this.nupst.getDaemon().loadConfig(); await this.nupst.getDaemon().loadConfig();
} catch (error) { } catch (error) {
const errorBoxWidth = 45; logger.logBox('Configuration Error', [
logger.logBoxTitle('Configuration Error', errorBoxWidth); 'No configuration found.',
logger.logBoxLine('No configuration found.'); "Please run 'nupst ups add' first to create a configuration.",
logger.logBoxLine("Please run 'nupst setup' first to create a configuration."); ], 50, 'error');
logger.logBoxEnd();
return; return;
} }
@@ -393,58 +393,57 @@ export class UpsHandler {
// Check if multi-UPS config // Check if multi-UPS config
if (!config.upsDevices || !Array.isArray(config.upsDevices)) { if (!config.upsDevices || !Array.isArray(config.upsDevices)) {
// Legacy single UPS configuration // Legacy single UPS configuration
const boxWidth = 45; logger.logBox('UPS Devices', [
logger.logBoxTitle('UPS Devices', boxWidth); 'Legacy single-UPS configuration detected.',
logger.logBoxLine('Legacy single-UPS configuration detected.'); '',
if (!config.snmp || !config.thresholds) { ...((!config.snmp || !config.thresholds)
logger.logBoxLine(''); ? ['Error: Configuration missing SNMP or threshold settings']
logger.logBoxLine('Error: Configuration missing SNMP or threshold settings'); : [
logger.logBoxEnd(); 'Default UPS:',
return; ` Host: ${config.snmp.host}:${config.snmp.port}`,
} ` Model: ${config.snmp.upsModel || 'cyberpower'}`,
logger.logBoxLine(''); ` Thresholds: ${config.thresholds.battery}% battery, ${config.thresholds.runtime} min runtime`,
logger.logBoxLine('Default UPS:'); '',
logger.logBoxLine(` Host: ${config.snmp.host}:${config.snmp.port}`); 'Use "nupst ups add" to add more UPS devices and migrate',
logger.logBoxLine(` Model: ${config.snmp.upsModel || 'cyberpower'}`); 'to the multi-UPS configuration format.',
logger.logBoxLine( ]
` Thresholds: ${config.thresholds.battery}% battery, ${config.thresholds.runtime} min runtime`, ),
); ], 60, 'warning');
logger.logBoxLine('');
logger.logBoxLine('Use "nupst add" to add more UPS devices and migrate');
logger.logBoxLine('to the multi-UPS configuration format.');
logger.logBoxEnd();
return; return;
} }
// Display UPS list // Display UPS list with modern table
const boxWidth = 60;
logger.logBoxTitle('UPS Devices', boxWidth);
if (config.upsDevices.length === 0) { if (config.upsDevices.length === 0) {
logger.logBoxLine('No UPS devices configured.'); logger.logBox('UPS Devices', [
logger.logBoxLine('Use "nupst add" to add a UPS device.'); 'No UPS devices configured.',
} else { '',
logger.logBoxLine(`Found ${config.upsDevices.length} UPS device(s)`); `${theme.dim('Run')} ${theme.command('nupst ups add')} ${theme.dim('to add a device')}`,
logger.logBoxLine(''); ], 60, 'info');
logger.logBoxLine( return;
'ID | Name | Host | Mode | Groups',
);
logger.logBoxLine(
'-----------+----------------------+-----------------+--------------+----------------',
);
for (const ups of config.upsDevices) {
const id = ups.id.padEnd(10, ' ').substring(0, 10);
const name = (ups.name || '').padEnd(20, ' ').substring(0, 20);
const host = `${ups.snmp.host}:${ups.snmp.port}`.padEnd(15, ' ').substring(0, 15);
const model = (ups.snmp.upsModel || 'cyberpower').padEnd(12, ' ').substring(0, 12);
const groups = ups.groups.length > 0 ? ups.groups.join(', ') : 'None';
logger.logBoxLine(`${id} | ${name} | ${host} | ${model} | ${groups}`);
}
} }
logger.logBoxEnd(); // Prepare table data
const rows = config.upsDevices.map((ups) => ({
id: ups.id,
name: ups.name || '',
host: `${ups.snmp.host}:${ups.snmp.port}`,
model: ups.snmp.upsModel || 'cyberpower',
groups: ups.groups.length > 0 ? ups.groups.join(', ') : theme.dim('None'),
}));
const columns: ITableColumn[] = [
{ header: 'ID', key: 'id', align: 'left', color: theme.highlight },
{ header: 'Name', key: 'name', align: 'left' },
{ header: 'Host:Port', key: 'host', align: 'left', color: theme.info },
{ header: 'Model', key: 'model', align: 'left' },
{ header: 'Groups', key: 'groups', align: 'left' },
];
logger.log('');
logger.info(`UPS Devices (${config.upsDevices.length}):`);
logger.log('');
logger.logTable(columns, rows);
logger.log('');
} catch (error) { } catch (error) {
logger.error( logger.error(
`Failed to list UPS devices: ${error instanceof Error ? error.message : String(error)}`, `Failed to list UPS devices: ${error instanceof Error ? error.message : String(error)}`,

View File

@@ -5,8 +5,9 @@ import { exec, execFile } from 'node:child_process';
import { promisify } from 'node:util'; import { promisify } from 'node:util';
import { NupstSnmp } from './snmp/manager.ts'; import { NupstSnmp } from './snmp/manager.ts';
import type { ISnmpConfig } from './snmp/types.ts'; import type { ISnmpConfig } from './snmp/types.ts';
import { logger } from './logger.ts'; import { logger, type ITableColumn } from './logger.ts';
import { MigrationRunner } from './migrations/index.ts'; import { MigrationRunner } from './migrations/index.ts';
import { theme, symbols, getBatteryColor, getRuntimeColor, formatPowerStatus } from './colors.ts';
const execAsync = promisify(exec); const execAsync = promisify(exec);
const execFileAsync = promisify(execFile); const execFileAsync = promisify(execFile);
@@ -310,29 +311,57 @@ export class NupstDaemon {
* Log the loaded configuration settings * Log the loaded configuration settings
*/ */
private logConfigLoaded(): void { private logConfigLoaded(): void {
const boxWidth = 50;
logger.logBoxTitle('Configuration Loaded', boxWidth); logger.log('');
logger.logBoxTitle('Configuration Loaded', 70, 'success');
if (this.config.upsDevices && this.config.upsDevices.length > 0) {
logger.logBoxLine(`UPS Devices: ${this.config.upsDevices.length}`);
for (const ups of this.config.upsDevices) {
logger.logBoxLine(` - ${ups.name} (${ups.id}): ${ups.snmp.host}:${ups.snmp.port}`);
}
} else {
logger.logBoxLine('No UPS devices configured');
}
if (this.config.groups && this.config.groups.length > 0) {
logger.logBoxLine(`Groups: ${this.config.groups.length}`);
for (const group of this.config.groups) {
logger.logBoxLine(` - ${group.name} (${group.id}): ${group.mode} mode`);
}
} else {
logger.logBoxLine('No Groups configured');
}
logger.logBoxLine(`Check Interval: ${this.config.checkInterval / 1000} seconds`); logger.logBoxLine(`Check Interval: ${this.config.checkInterval / 1000} seconds`);
logger.logBoxEnd(); logger.logBoxEnd();
logger.log('');
// Display UPS devices in a table
if (this.config.upsDevices && this.config.upsDevices.length > 0) {
logger.info(`UPS Devices (${this.config.upsDevices.length}):`);
const upsColumns: Array<{ header: string; key: string; align?: 'left' | 'right'; color?: (val: string) => string }> = [
{ header: 'Name', key: 'name', align: 'left', color: theme.highlight },
{ header: 'ID', key: 'id', align: 'left', color: theme.dim },
{ header: 'Host:Port', key: 'host', align: 'left', color: theme.info },
{ header: 'Battery/Runtime', key: 'thresholds', align: 'left' },
];
const upsRows: Array<Record<string, string>> = this.config.upsDevices.map((ups) => ({
name: ups.name,
id: ups.id,
host: `${ups.snmp.host}:${ups.snmp.port}`,
thresholds: `${ups.thresholds.battery}% / ${ups.thresholds.runtime} min`,
}));
logger.logTable(upsColumns, upsRows);
logger.log('');
} else {
logger.warn('No UPS devices configured');
logger.log('');
}
// Display groups in a table
if (this.config.groups && this.config.groups.length > 0) {
logger.info(`Groups (${this.config.groups.length}):`);
const groupColumns: Array<{ header: string; key: string; align?: 'left' | 'right'; color?: (val: string) => string }> = [
{ header: 'Name', key: 'name', align: 'left', color: theme.highlight },
{ header: 'ID', key: 'id', align: 'left', color: theme.dim },
{ header: 'Mode', key: 'mode', align: 'left', color: theme.info },
];
const groupRows: Array<Record<string, string>> = this.config.groups.map((group) => ({
name: group.name,
id: group.id,
mode: group.mode,
}));
logger.logTable(groupColumns, groupRows);
logger.log('');
}
} }
/** /**
@@ -428,9 +457,13 @@ export class NupstDaemon {
// Check if power status changed // Check if power status changed
if (currentStatus && currentStatus.powerStatus !== status.powerStatus) { if (currentStatus && currentStatus.powerStatus !== status.powerStatus) {
logger.logBoxTitle(`Power Status Change: ${ups.name}`, 50); logger.log('');
logger.logBoxLine(`Status changed: ${currentStatus.powerStatus}${status.powerStatus}`); logger.logBoxTitle(`Power Status Change: ${ups.name}`, 60, 'warning');
logger.logBoxLine(`Previous: ${formatPowerStatus(currentStatus.powerStatus)}`);
logger.logBoxLine(`Current: ${formatPowerStatus(status.powerStatus)}`);
logger.logBoxLine(`Time: ${new Date().toISOString()}`);
logger.logBoxEnd(); logger.logBoxEnd();
logger.log('');
updatedStatus.lastStatusChange = currentTime; updatedStatus.lastStatusChange = currentTime;
} }
@@ -452,21 +485,38 @@ export class NupstDaemon {
*/ */
private logAllUpsStatus(): void { private logAllUpsStatus(): void {
const timestamp = new Date().toISOString(); const timestamp = new Date().toISOString();
const boxWidth = 60;
logger.logBoxTitle('Periodic Status Update', boxWidth); logger.log('');
logger.logBoxTitle('Periodic Status Update', 70, 'info');
logger.logBoxLine(`Timestamp: ${timestamp}`); logger.logBoxLine(`Timestamp: ${timestamp}`);
logger.logBoxLine(''); logger.logBoxEnd();
logger.log('');
// Build table data
const columns: Array<{ header: string; key: string; align?: 'left' | 'right'; color?: (val: string) => string }> = [
{ header: 'UPS Name', key: 'name', align: 'left', color: theme.highlight },
{ header: 'ID', key: 'id', align: 'left', color: theme.dim },
{ header: 'Power Status', key: 'powerStatus', align: 'left' },
{ header: 'Battery', key: 'battery', align: 'right' },
{ header: 'Runtime', key: 'runtime', align: 'right' },
];
const rows: Array<Record<string, string>> = [];
for (const [id, status] of this.upsStatus.entries()) { for (const [id, status] of this.upsStatus.entries()) {
logger.logBoxLine(`UPS: ${status.name} (${id})`); const batteryColor = getBatteryColor(status.batteryCapacity);
logger.logBoxLine(` Power Status: ${status.powerStatus}`); const runtimeColor = getRuntimeColor(status.batteryRuntime);
logger.logBoxLine(
` Battery: ${status.batteryCapacity}% | Runtime: ${status.batteryRuntime} min`, rows.push({
); name: status.name,
logger.logBoxLine(''); id: id,
powerStatus: formatPowerStatus(status.powerStatus),
battery: batteryColor(status.batteryCapacity + '%'),
runtime: runtimeColor(status.batteryRuntime + ' min'),
});
} }
logger.logBoxEnd(); logger.logTable(columns, rows);
logger.log('');
} }
/** /**
@@ -745,38 +795,61 @@ export class NupstDaemon {
const MAX_MONITORING_TIME = 5 * 60 * 1000; // Max 5 minutes of monitoring const MAX_MONITORING_TIME = 5 * 60 * 1000; // Max 5 minutes of monitoring
const startTime = Date.now(); const startTime = Date.now();
logger.log( logger.log('');
`Emergency shutdown threshold: ${EMERGENCY_RUNTIME_THRESHOLD} minutes remaining battery runtime`, logger.logBoxTitle('Shutdown Monitoring Active', 60, 'warning');
); logger.logBoxLine(`Emergency threshold: ${EMERGENCY_RUNTIME_THRESHOLD} minutes runtime`);
logger.logBoxLine(`Check interval: ${CHECK_INTERVAL / 1000} seconds`);
logger.logBoxLine(`Max monitoring time: ${MAX_MONITORING_TIME / 1000} seconds`);
logger.logBoxEnd();
logger.log('');
// Continue monitoring until max monitoring time is reached // Continue monitoring until max monitoring time is reached
while (Date.now() - startTime < MAX_MONITORING_TIME) { while (Date.now() - startTime < MAX_MONITORING_TIME) {
try { try {
logger.log('Checking UPS status during shutdown...'); logger.info('Checking UPS status during shutdown...');
// Build table for UPS status during shutdown
const columns: Array<{ header: string; key: string; align?: 'left' | 'right'; color?: (val: string) => string }> = [
{ header: 'UPS Name', key: 'name', align: 'left', color: theme.highlight },
{ header: 'Battery', key: 'battery', align: 'right' },
{ header: 'Runtime', key: 'runtime', align: 'right' },
{ header: 'Status', key: 'status', align: 'left' },
];
const rows: Array<Record<string, string>> = [];
let emergencyDetected = false;
let emergencyUps: any = null;
// Check all UPS devices // Check all UPS devices
for (const ups of this.config.upsDevices) { for (const ups of this.config.upsDevices) {
try { try {
const status = await this.snmp.getUpsStatus(ups.snmp); const status = await this.snmp.getUpsStatus(ups.snmp);
logger.log( const batteryColor = getBatteryColor(status.batteryCapacity);
`UPS ${ups.name}: Battery ${status.batteryCapacity}%, Runtime: ${status.batteryRuntime} minutes`, const runtimeColor = getRuntimeColor(status.batteryRuntime);
);
// If any UPS battery runtime gets critically low, force immediate shutdown const isCritical = status.batteryRuntime < EMERGENCY_RUNTIME_THRESHOLD;
if (status.batteryRuntime < EMERGENCY_RUNTIME_THRESHOLD) {
logger.logBoxTitle('EMERGENCY SHUTDOWN', 50); rows.push({
logger.logBoxLine( name: ups.name,
`UPS ${ups.name} runtime critically low: ${status.batteryRuntime} minutes`, battery: batteryColor(status.batteryCapacity + '%'),
); runtime: runtimeColor(status.batteryRuntime + ' min'),
logger.logBoxLine('Forcing immediate shutdown!'); status: isCritical ? theme.error('CRITICAL!') : theme.success('OK'),
logger.logBoxEnd(); });
// Force immediate shutdown // If any UPS battery runtime gets critically low, flag for immediate shutdown
await this.forceImmediateShutdown(); if (isCritical && !emergencyDetected) {
return; emergencyDetected = true;
emergencyUps = { ups, status };
} }
} catch (upsError) { } catch (upsError) {
rows.push({
name: ups.name,
battery: theme.error('N/A'),
runtime: theme.error('N/A'),
status: theme.error('ERROR'),
});
logger.error( logger.error(
`Error checking UPS ${ups.name} during shutdown: ${ `Error checking UPS ${ups.name} during shutdown: ${
upsError instanceof Error ? upsError.message : String(upsError) upsError instanceof Error ? upsError.message : String(upsError)
@@ -785,6 +858,27 @@ export class NupstDaemon {
} }
} }
// Display the table
logger.logTable(columns, rows);
logger.log('');
// If emergency detected, trigger immediate shutdown
if (emergencyDetected && emergencyUps) {
logger.log('');
logger.logBoxTitle('EMERGENCY SHUTDOWN', 60, 'error');
logger.logBoxLine(
`UPS ${emergencyUps.ups.name} runtime critically low: ${emergencyUps.status.batteryRuntime} minutes`,
);
logger.logBoxLine(`Emergency threshold: ${EMERGENCY_RUNTIME_THRESHOLD} minutes`);
logger.logBoxLine('Forcing immediate shutdown!');
logger.logBoxEnd();
logger.log('');
// Force immediate shutdown
await this.forceImmediateShutdown();
return;
}
// Wait before checking again // Wait before checking again
await this.sleep(CHECK_INTERVAL); await this.sleep(CHECK_INTERVAL);
} catch (error) { } catch (error) {
@@ -797,7 +891,9 @@ export class NupstDaemon {
} }
} }
logger.log('UPS monitoring during shutdown completed'); logger.log('');
logger.success('UPS monitoring during shutdown completed');
logger.log('');
} }
/** /**