Compare commits
5 Commits
Author | SHA1 | Date | |
---|---|---|---|
a7113d0387 | |||
61d4e9037a | |||
caced2718f | |||
8516056f84 | |||
07ec9d7595 |
@@ -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",
|
||||||
|
244
ts/cli.ts
244
ts/cli.ts
@@ -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}`);
|
|
||||||
logger.logBoxLine(`Check Interval: ${config.checkInterval / 1000} seconds`);
|
|
||||||
logger.logBoxLine('');
|
|
||||||
logger.logBoxLine('Configuration File Location:');
|
|
||||||
logger.logBoxLine(' /etc/nupst/config.json');
|
|
||||||
logger.logBoxEnd();
|
|
||||||
|
|
||||||
// Show UPS devices
|
// Overview Box
|
||||||
|
logger.log('');
|
||||||
|
logger.logBox('NUPST Configuration', [
|
||||||
|
`UPS Devices: ${theme.highlight(String(config.upsDevices.length))}`,
|
||||||
|
`Groups: ${theme.highlight(String(config.groups ? config.groups.length : 0))}`,
|
||||||
|
`Check Interval: ${theme.info(String(config.checkInterval / 1000))} seconds`,
|
||||||
|
'',
|
||||||
|
theme.dim('Configuration File:'),
|
||||||
|
` ${theme.path('/etc/nupst/config.json')}`,
|
||||||
|
], 60, 'info');
|
||||||
|
|
||||||
|
// 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');
|
|
||||||
} else {
|
|
||||||
// SNMP Settings
|
|
||||||
logger.logBoxLine('SNMP Settings:');
|
|
||||||
logger.logBoxLine(` Host: ${config.snmp.host}`);
|
|
||||||
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) {
|
if (!config.snmp || !config.thresholds) {
|
||||||
logger.logBoxLine(` Community: ${config.snmp.community}`);
|
logger.logBox('Configuration Error', [
|
||||||
} else if (config.snmp.version === 3) {
|
'Error: Legacy configuration missing SNMP or threshold settings',
|
||||||
logger.logBoxLine(` Security Level: ${config.snmp.securityLevel}`);
|
], 60, 'error');
|
||||||
logger.logBoxLine(` Username: ${config.snmp.username}`);
|
return;
|
||||||
|
|
||||||
// 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.log('');
|
||||||
logger.logBoxLine(` Privacy Protocol: ${config.snmp.privProtocol || 'None'}`);
|
logger.logBox('NUPST Configuration (Legacy)', [
|
||||||
}
|
theme.warning('Legacy single-UPS configuration format'),
|
||||||
|
'',
|
||||||
// Show timeout value
|
theme.dim('SNMP Settings:'),
|
||||||
logger.logBoxLine(` Timeout: ${config.snmp.timeout / 1000} seconds`);
|
` Host: ${theme.info(config.snmp.host)}`,
|
||||||
}
|
` Port: ${theme.info(String(config.snmp.port))}`,
|
||||||
|
` Version: ${config.snmp.version}`,
|
||||||
// Show OIDs if custom model is selected
|
` UPS Model: ${config.snmp.upsModel || 'cyberpower'}`,
|
||||||
if (config.snmp.upsModel === 'custom' && config.snmp.customOIDs) {
|
...(config.snmp.version === 1 || config.snmp.version === 2
|
||||||
logger.logBoxLine('Custom OIDs:');
|
? [` Community: ${config.snmp.community}`]
|
||||||
logger.logBoxLine(
|
: []
|
||||||
|
),
|
||||||
|
...(config.snmp.version === 3
|
||||||
|
? [
|
||||||
|
` Security Level: ${config.snmp.securityLevel}`,
|
||||||
|
` Username: ${config.snmp.username}`,
|
||||||
|
...(config.snmp.securityLevel === 'authNoPriv' || config.snmp.securityLevel === 'authPriv'
|
||||||
|
? [` 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'}`,
|
` Power Status: ${config.snmp.customOIDs.POWER_STATUS || 'Not set'}`,
|
||||||
);
|
|
||||||
logger.logBoxLine(
|
|
||||||
` Battery Capacity: ${config.snmp.customOIDs.BATTERY_CAPACITY || 'Not set'}`,
|
` Battery Capacity: ${config.snmp.customOIDs.BATTERY_CAPACITY || 'Not set'}`,
|
||||||
);
|
|
||||||
logger.logBoxLine(
|
|
||||||
` Battery Runtime: ${config.snmp.customOIDs.BATTERY_RUNTIME || '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');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Thresholds
|
// Service Status
|
||||||
if (!config.thresholds) {
|
|
||||||
logger.logBoxLine('Error: Legacy configuration missing threshold settings');
|
|
||||||
} else {
|
|
||||||
logger.logBoxLine('Thresholds:');
|
|
||||||
logger.logBoxLine(` Battery: ${config.thresholds.battery}%`);
|
|
||||||
logger.logBoxLine(` Runtime: ${config.thresholds.runtime} minutes`);
|
|
||||||
}
|
|
||||||
logger.logBoxLine(`Check Interval: ${config.checkInterval / 1000} seconds`);
|
|
||||||
|
|
||||||
// Configuration file location
|
|
||||||
logger.logBoxLine('');
|
|
||||||
logger.logBoxLine('Configuration File Location:');
|
|
||||||
logger.logBoxLine(' /etc/nupst/config.json');
|
|
||||||
logger.logBoxLine('');
|
|
||||||
logger.logBoxLine('Note: Using legacy single-UPS configuration format.');
|
|
||||||
logger.logBoxLine('Consider using "nupst add" to migrate to multi-UPS format.');
|
|
||||||
|
|
||||||
logger.logBoxEnd();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show 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
|
||||||
}
|
}
|
||||||
|
@@ -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);
|
|
||||||
|
|
||||||
|
// Prepare table data
|
||||||
|
const rows = config.groups.map((group) => {
|
||||||
// Count UPS devices in this group
|
// Count UPS devices in this group
|
||||||
const upsInGroup = config.upsDevices.filter((ups) => ups.groups.includes(group.id));
|
const upsInGroup = config.upsDevices.filter((ups) => ups.groups.includes(group.id));
|
||||||
const upsCount = upsInGroup.length;
|
const upsCount = upsInGroup.length;
|
||||||
const upsNames = upsInGroup.map((ups) => ups.name).join(', ');
|
const upsNames = upsInGroup.map((ups) => ups.name).join(', ');
|
||||||
|
|
||||||
logger.logBoxLine(`${id} | ${name} | ${mode} | ${upsCount > 0 ? upsNames : 'None'}`);
|
return {
|
||||||
}
|
id: group.id,
|
||||||
}
|
name: group.name || '',
|
||||||
|
mode: group.mode || 'unknown',
|
||||||
|
count: String(upsCount),
|
||||||
|
devices: upsCount > 0 ? upsNames : theme.dim('None'),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
logger.logBoxEnd();
|
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)}`,
|
||||||
|
@@ -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('');
|
|
||||||
logger.logBoxLine('Default UPS:');
|
|
||||||
logger.logBoxLine(` Host: ${config.snmp.host}:${config.snmp.port}`);
|
|
||||||
logger.logBoxLine(` Model: ${config.snmp.upsModel || 'cyberpower'}`);
|
|
||||||
logger.logBoxLine(
|
|
||||||
` Thresholds: ${config.thresholds.battery}% battery, ${config.thresholds.runtime} min runtime`,
|
` Thresholds: ${config.thresholds.battery}% battery, ${config.thresholds.runtime} min runtime`,
|
||||||
);
|
'',
|
||||||
logger.logBoxLine('');
|
'Use "nupst ups add" to add more UPS devices and migrate',
|
||||||
logger.logBoxLine('Use "nupst add" to add more UPS devices and migrate');
|
'to the multi-UPS configuration format.',
|
||||||
logger.logBoxLine('to the multi-UPS configuration format.');
|
]
|
||||||
logger.logBoxEnd();
|
),
|
||||||
|
], 60, 'warning');
|
||||||
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)}`,
|
||||||
|
202
ts/daemon.ts
202
ts/daemon.ts
@@ -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);
|
|
||||||
|
|
||||||
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.log('');
|
||||||
|
logger.logBoxTitle('Configuration Loaded', 70, 'success');
|
||||||
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.logBoxLine(`Timestamp: ${timestamp}`);
|
|
||||||
logger.logBoxLine('');
|
|
||||||
|
|
||||||
|
logger.log('');
|
||||||
|
logger.logBoxTitle('Periodic Status Update', 70, 'info');
|
||||||
|
logger.logBoxLine(`Timestamp: ${timestamp}`);
|
||||||
|
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);
|
|
||||||
logger.logBoxLine(
|
|
||||||
`UPS ${ups.name} runtime critically low: ${status.batteryRuntime} minutes`,
|
|
||||||
);
|
|
||||||
logger.logBoxLine('Forcing immediate shutdown!');
|
|
||||||
logger.logBoxEnd();
|
|
||||||
|
|
||||||
// Force immediate shutdown
|
rows.push({
|
||||||
await this.forceImmediateShutdown();
|
name: ups.name,
|
||||||
return;
|
battery: batteryColor(status.batteryCapacity + '%'),
|
||||||
|
runtime: runtimeColor(status.batteryRuntime + ' min'),
|
||||||
|
status: isCritical ? theme.error('CRITICAL!') : theme.success('OK'),
|
||||||
|
});
|
||||||
|
|
||||||
|
// If any UPS battery runtime gets critically low, flag for immediate shutdown
|
||||||
|
if (isCritical && !emergencyDetected) {
|
||||||
|
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('');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Reference in New Issue
Block a user