Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 782c8c9555 | |||
| 463c32ebba | |||
| 51aa68ff8d | |||
| cb34ae5041 | |||
| 165c7d29bb | |||
| ff2dc00f31 | |||
| fda072d15e | |||
| c7786e9626 |
@@ -5,19 +5,23 @@ Pre-compiled binaries for multiple platforms.
|
|||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
#### Option 1: Via npm (recommended)
|
#### Option 1: Via npm (recommended)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install -g @serve.zone/nupst
|
npm install -g @serve.zone/nupst
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Option 2: Via installer script
|
#### Option 2: Via installer script
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash
|
curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Option 3: Direct binary download
|
#### Option 3: Direct binary download
|
||||||
|
|
||||||
Download the appropriate binary for your platform from the assets below and make it executable.
|
Download the appropriate binary for your platform from the assets below and make it executable.
|
||||||
|
|
||||||
### Supported Platforms
|
### Supported Platforms
|
||||||
|
|
||||||
- Linux x86_64 (x64)
|
- Linux x86_64 (x64)
|
||||||
- Linux ARM64 (aarch64)
|
- Linux ARM64 (aarch64)
|
||||||
- macOS x86_64 (Intel)
|
- macOS x86_64 (Intel)
|
||||||
@@ -25,7 +29,9 @@ Download the appropriate binary for your platform from the assets below and make
|
|||||||
- Windows x86_64
|
- Windows x86_64
|
||||||
|
|
||||||
### Checksums
|
### Checksums
|
||||||
|
|
||||||
SHA256 checksums are provided in `SHA256SUMS.txt` for binary verification.
|
SHA256 checksums are provided in `SHA256SUMS.txt` for binary verification.
|
||||||
|
|
||||||
### npm Package
|
### npm Package
|
||||||
|
|
||||||
The npm package includes automatic binary detection and installation for your platform.
|
The npm package includes automatic binary detection and installation for your platform.
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import { spawn } from 'child_process';
|
|||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { dirname, join } from 'path';
|
import { dirname, join } from 'path';
|
||||||
import { existsSync } from 'fs';
|
import { existsSync } from 'fs';
|
||||||
import { platform, arch } from 'os';
|
import { arch, platform } from 'os';
|
||||||
|
import process from "node:process";
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = dirname(__filename);
|
const __dirname = dirname(__filename);
|
||||||
@@ -25,12 +26,12 @@ function getBinaryName() {
|
|||||||
const platformMap = {
|
const platformMap = {
|
||||||
'darwin': 'macos',
|
'darwin': 'macos',
|
||||||
'linux': 'linux',
|
'linux': 'linux',
|
||||||
'win32': 'windows'
|
'win32': 'windows',
|
||||||
};
|
};
|
||||||
|
|
||||||
const archMap = {
|
const archMap = {
|
||||||
'x64': 'x64',
|
'x64': 'x64',
|
||||||
'arm64': 'arm64'
|
'arm64': 'arm64',
|
||||||
};
|
};
|
||||||
|
|
||||||
const mappedPlatform = platformMap[plat];
|
const mappedPlatform = platformMap[plat];
|
||||||
@@ -76,7 +77,7 @@ function executeBinary() {
|
|||||||
// Spawn the binary with all arguments passed through
|
// Spawn the binary with all arguments passed through
|
||||||
const child = spawn(binaryPath, process.argv.slice(2), {
|
const child = spawn(binaryPath, process.argv.slice(2), {
|
||||||
stdio: 'inherit',
|
stdio: 'inherit',
|
||||||
shell: false
|
shell: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle child process events
|
// Handle child process events
|
||||||
@@ -95,7 +96,7 @@ function executeBinary() {
|
|||||||
|
|
||||||
// Forward signals to child process
|
// Forward signals to child process
|
||||||
const signals = ['SIGINT', 'SIGTERM', 'SIGHUP'];
|
const signals = ['SIGINT', 'SIGTERM', 'SIGHUP'];
|
||||||
signals.forEach(signal => {
|
signals.forEach((signal) => {
|
||||||
process.on(signal, () => {
|
process.on(signal, () => {
|
||||||
if (!child.killed) {
|
if (!child.killed) {
|
||||||
child.kill(signal);
|
child.kill(signal);
|
||||||
@@ -105,4 +106,4 @@ function executeBinary() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Execute
|
// Execute
|
||||||
executeBinary();
|
executeBinary();
|
||||||
|
|||||||
116
changelog.md
116
changelog.md
@@ -1,59 +1,131 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## 2026-01-29 - 5.2.0 - feat(core)
|
## 2026-01-29 - 5.2.4 - fix()
|
||||||
Centralize timeouts/constants, add CLI prompt helpers, and introduce webhook/script actions with safety and SNMP refactors
|
no changes
|
||||||
|
|
||||||
- Add ts/constants.ts to centralize timing, SNMP, webhook, script, shutdown and UI constants and replace magic numbers across the codebase
|
- No files changed in the provided git diff; no commit or version bump required.
|
||||||
- Introduce helpers/prompt.ts with createPrompt() and withPrompt() and refactor CLI handlers to use these helpers (cleaner prompt lifecycle)
|
|
||||||
- Add webhook action support: ts/actions/webhook-action.ts, IWebhookPayload type, and export from ts/actions/index.ts
|
## 2026-01-29 - 5.2.3 - fix(core)
|
||||||
- Enhance ShutdownAction safety checks (only trigger onBattery, stricter transition rules) and use constants/UI widths for displays
|
fix lint/type issues and small refactors
|
||||||
- Refactor SNMP manager to use logger instead of console, pull SNMP defaults from constants, improved debug output, and add INupstAccessor interface to break circular dependency (Nupst now implements the interface)
|
|
||||||
- Update many CLI and core types (stronger typing for configs/actions), expand tests and update README and npmextra.json to document new features
|
- Add missing node:process imports in bin and scripts to ensure process is available
|
||||||
|
- Remove unused imports and unused type imports (e.g. writeFileSync, IActionConfig) to reduce noise
|
||||||
|
- Make some methods synchronous (service update, webhook call) to match actual usage
|
||||||
|
- Tighten SNMP typings and linting: added deno-lint-ignore comments, renamed unused params with leading underscore, and use `as const` for securityLevel fallbacks
|
||||||
|
- Improve error handling variable naming in systemd (use error instead of _error)
|
||||||
|
- Annotate ANSI regex with deno-lint-ignore no-control-regex and remove unused color/symbol imports across CLI/daemon/logger
|
||||||
|
|
||||||
|
## 2026-01-29 - 5.2.2 - fix(core)
|
||||||
|
tidy formatting and minor fixes across CLI, SNMP, HTTP server, migrations and packaging
|
||||||
|
|
||||||
|
- Normalize import ordering and improve logger/string formatting across many CLI handlers, daemon, systemd, actions and tests
|
||||||
|
- Apply formatting tidies: trailing commas, newline fixes, and more consistent multiline strings
|
||||||
|
- Allow BaseMigration methods to return either sync or async results (shouldRun/migrate signatures updated)
|
||||||
|
- Improve SNMP manager and HTTP server logging/error messages and tighten some typings (raw SNMP types, server error typing)
|
||||||
|
- Small robustness and messaging improvements in npm installer and wrapper (platform/arch mapping, error outputs)
|
||||||
|
- Update tests and documentation layout/formatting for readability
|
||||||
|
|
||||||
|
## 2026-01-29 - 5.2.1 - fix(cli(ups-handler), systemd)
|
||||||
|
|
||||||
|
add type guards and null checks for UPS configs; improve SNMP handling and prompts; guard version
|
||||||
|
display
|
||||||
|
|
||||||
|
- Introduce a type guard ('id' in config && 'name' in config) to distinguish IUpsConfig from legacy
|
||||||
|
INupstConfig and route fields (snmp, checkInterval, name, id) accordingly.
|
||||||
|
- displayTestConfig now handles missing SNMP by logging 'Not configured' and returning, computes
|
||||||
|
checkInterval/upsName/upsId correctly, and uses groups only for true UPS configs.
|
||||||
|
- testConnection now safely derives snmpConfig for both config types, throws if SNMP is missing, and
|
||||||
|
caps test timeout to 10s for probes.
|
||||||
|
- Clear auth/priv credentials by setting undefined (instead of empty strings) when disabling
|
||||||
|
security levels to avoid invalid/empty string values.
|
||||||
|
- Expanded customOIDs to include OUTPUT_LOAD, OUTPUT_POWER, OUTPUT_VOLTAGE, OUTPUT_CURRENT with
|
||||||
|
defaults; trim prompt input and document RFC 1628 fallbacks.
|
||||||
|
- systemd.displayVersionInfo: guard against missing nupst (silent return) and avoid errors when
|
||||||
|
printing version info; use ignored catch variables for clarity.
|
||||||
|
|
||||||
|
## 2026-01-29 - 5.2.0 - feat(core)
|
||||||
|
|
||||||
|
Centralize timeouts/constants, add CLI prompt helpers, and introduce webhook/script actions with
|
||||||
|
safety and SNMP refactors
|
||||||
|
|
||||||
|
- Add ts/constants.ts to centralize timing, SNMP, webhook, script, shutdown and UI constants and
|
||||||
|
replace magic numbers across the codebase
|
||||||
|
- Introduce helpers/prompt.ts with createPrompt() and withPrompt() and refactor CLI handlers to use
|
||||||
|
these helpers (cleaner prompt lifecycle)
|
||||||
|
- Add webhook action support: ts/actions/webhook-action.ts, IWebhookPayload type, and export from
|
||||||
|
ts/actions/index.ts
|
||||||
|
- Enhance ShutdownAction safety checks (only trigger onBattery, stricter transition rules) and use
|
||||||
|
constants/UI widths for displays
|
||||||
|
- Refactor SNMP manager to use logger instead of console, pull SNMP defaults from constants,
|
||||||
|
improved debug output, and add INupstAccessor interface to break circular dependency (Nupst now
|
||||||
|
implements the interface)
|
||||||
|
- Update many CLI and core types (stronger typing for configs/actions), expand tests and update
|
||||||
|
README and npmextra.json to document new features
|
||||||
|
|
||||||
## 2025-11-09 - 5.1.11 - fix(readme)
|
## 2025-11-09 - 5.1.11 - fix(readme)
|
||||||
Update README installation instructions to recommend automated installer script and clarify npm installation
|
|
||||||
|
|
||||||
- Replace the previous 'Via npm (NEW! - Recommended)' section with a clear 'Automated Installer Script (Recommended)' section and example curl installer.
|
Update README installation instructions to recommend automated installer script and clarify npm
|
||||||
- Move npm installation instructions into an 'Alternative: Via npm' subsection and clarify that the npm package downloads the appropriate pre-compiled binary for the platform during installation.
|
installation
|
||||||
|
|
||||||
|
- Replace the previous 'Via npm (NEW! - Recommended)' section with a clear 'Automated Installer
|
||||||
|
Script (Recommended)' section and example curl installer.
|
||||||
|
- Move npm installation instructions into an 'Alternative: Via npm' subsection and clarify that the
|
||||||
|
npm package downloads the appropriate pre-compiled binary for the platform during installation.
|
||||||
- Remove the 'NEW!' badge and streamline notes about binary downloads and installation methods.
|
- Remove the 'NEW!' badge and streamline notes about binary downloads and installation methods.
|
||||||
|
|
||||||
## 2025-10-23 - 5.1.10 - fix(config)
|
## 2025-10-23 - 5.1.10 - fix(config)
|
||||||
|
|
||||||
Synchronize deno.json version with package.json, tidy formatting, and add local tooling settings
|
Synchronize deno.json version with package.json, tidy formatting, and add local tooling settings
|
||||||
|
|
||||||
- Bumped deno.json version to 5.1.9 to match package.json/commitinfo
|
- Bumped deno.json version to 5.1.9 to match package.json/commitinfo
|
||||||
- Reformatted deno.json arrays (lint, fmt, compilerOptions) for readability
|
- Reformatted deno.json arrays (lint, fmt, compilerOptions) for readability
|
||||||
- Added .claude/settings.local.json for local development/tooling permissions (no runtime behaviour changes)
|
- Added .claude/settings.local.json for local development/tooling permissions (no runtime behaviour
|
||||||
|
changes)
|
||||||
|
|
||||||
## 2025-10-23 - 5.1.9 - fix(dev)
|
## 2025-10-23 - 5.1.9 - fix(dev)
|
||||||
|
|
||||||
Add local assistant permissions/settings file (.claude/settings.local.json)
|
Add local assistant permissions/settings file (.claude/settings.local.json)
|
||||||
|
|
||||||
- Added .claude/settings.local.json containing local assistant permission configuration used for development tasks (deno check, deno lint/format, npm/pack, running packaged binaries, etc.)
|
- Added .claude/settings.local.json containing local assistant permission configuration used for
|
||||||
- This is a development/local configuration file and does not change runtime behavior or product code paths
|
development tasks (deno check, deno lint/format, npm/pack, running packaged binaries, etc.)
|
||||||
|
- This is a development/local configuration file and does not change runtime behavior or product
|
||||||
|
code paths
|
||||||
- Patch version bump recommended
|
- Patch version bump recommended
|
||||||
|
|
||||||
## 2025-10-23 - 5.1.2 - fix(scripts)
|
## 2025-10-23 - 5.1.2 - fix(scripts)
|
||||||
|
|
||||||
Add build script to package.json and include local dev tool settings
|
Add build script to package.json and include local dev tool settings
|
||||||
|
|
||||||
- Add a 'build' script to package.json (no-op placeholder) to provide an explicit build step
|
- Add a 'build' script to package.json (no-op placeholder) to provide an explicit build step
|
||||||
- Minor scripts section formatting tidy in package.json
|
- Minor scripts section formatting tidy in package.json
|
||||||
- Add a hidden local settings file for development tooling permissions to the repository (local-only configuration)
|
- Add a hidden local settings file for development tooling permissions to the repository (local-only
|
||||||
|
configuration)
|
||||||
|
|
||||||
## 2025-10-23 - 5.1.1 - fix(tooling)
|
## 2025-10-23 - 5.1.1 - fix(tooling)
|
||||||
|
|
||||||
Add .claude/settings.local.json with local automation permissions
|
Add .claude/settings.local.json with local automation permissions
|
||||||
|
|
||||||
- Add .claude/settings.local.json to specify allowed permissions for local automated tasks
|
- Add .claude/settings.local.json to specify allowed permissions for local automated tasks
|
||||||
- Grants permissions for various developer/CI actions (deno check/lint/fmt, npm/npm pack, selective Bash commands, WebFetch to docs.deno.com and code.foss.global, and file/read/replace helpers)
|
- Grants permissions for various developer/CI actions (deno check/lint/fmt, npm/npm pack, selective
|
||||||
|
Bash commands, WebFetch to docs.deno.com and code.foss.global, and file/read/replace helpers)
|
||||||
- This is a developer/local tooling config only and does not change runtime code or package behavior
|
- This is a developer/local tooling config only and does not change runtime code or package behavior
|
||||||
|
|
||||||
## 2025-10-22 - 5.1.0 - feat(packaging)
|
## 2025-10-22 - 5.1.0 - feat(packaging)
|
||||||
Add npm packaging and installer: wrapper, postinstall downloader, publish workflow, and packaging files
|
|
||||||
|
Add npm packaging and installer: wrapper, postinstall downloader, publish workflow, and packaging
|
||||||
|
files
|
||||||
|
|
||||||
- Add package.json (v5.0.5) and npm packaging metadata to publish @serve.zone/nupst
|
- Add package.json (v5.0.5) and npm packaging metadata to publish @serve.zone/nupst
|
||||||
- Include a small Node.js wrapper (bin/nupst-wrapper.js) to execute platform-specific precompiled binaries
|
- Include a small Node.js wrapper (bin/nupst-wrapper.js) to execute platform-specific precompiled
|
||||||
- Add postinstall script (scripts/install-binary.js) that downloads the correct binary for the current platform and sets executable permissions
|
binaries
|
||||||
- Add GitHub Actions workflow (.github/workflows/npm-publish.yml) to build binaries, pack and publish to npm, and create releases
|
- Add postinstall script (scripts/install-binary.js) that downloads the correct binary for the
|
||||||
- Add .npmignore to keep source, tests and dev files out of npm package; keep only runtime installer and wrapper
|
current platform and sets executable permissions
|
||||||
- Move example action script into docs (docs/example-action.sh) and remove the top-level example-action.sh
|
- Add GitHub Actions workflow (.github/workflows/npm-publish.yml) to build binaries, pack and
|
||||||
|
publish to npm, and create releases
|
||||||
|
- Add .npmignore to keep source, tests and dev files out of npm package; keep only runtime installer
|
||||||
|
and wrapper
|
||||||
|
- Move example action script into docs (docs/example-action.sh) and remove the top-level
|
||||||
|
example-action.sh
|
||||||
- Include generated npm package artifact (serve.zone-nupst-5.0.5.tgz) and npmextra.json
|
- Include generated npm package artifact (serve.zone-nupst-5.0.5.tgz) and npmextra.json
|
||||||
|
|
||||||
## 2025-10-18 - 4.0.0 - BREAKING CHANGE(core): Complete migration to Deno runtime
|
## 2025-10-18 - 4.0.0 - BREAKING CHANGE(core): Complete migration to Deno runtime
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@serve.zone/nupst",
|
"name": "@serve.zone/nupst",
|
||||||
"version": "5.2.0",
|
"version": "5.2.4",
|
||||||
"exports": "./mod.ts",
|
"exports": "./mod.ts",
|
||||||
"nodeModulesDir": "auto",
|
"nodeModulesDir": "auto",
|
||||||
"tasks": {
|
"tasks": {
|
||||||
|
|||||||
@@ -17,4 +17,4 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@ship.zone/szci": {}
|
"@ship.zone/szci": {}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@serve.zone/nupst",
|
"name": "@serve.zone/nupst",
|
||||||
"version": "5.2.0",
|
"version": "5.2.4",
|
||||||
"description": "Network UPS Shutdown Tool - Monitor SNMP-enabled UPS devices and orchestrate graceful system shutdowns during power emergencies",
|
"description": "Network UPS Shutdown Tool - Monitor SNMP-enabled UPS devices and orchestrate graceful system shutdowns during power emergencies",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"ups",
|
"ups",
|
||||||
|
|||||||
@@ -7,7 +7,8 @@
|
|||||||
1. **Prompt Utility (`ts/helpers/prompt.ts`)**
|
1. **Prompt Utility (`ts/helpers/prompt.ts`)**
|
||||||
- Extracted readline/prompt pattern from all CLI handlers
|
- Extracted readline/prompt pattern from all CLI handlers
|
||||||
- Provides `createPrompt()` and `withPrompt()` helper functions
|
- Provides `createPrompt()` and `withPrompt()` helper functions
|
||||||
- Used in: `ups-handler.ts`, `group-handler.ts`, `service-handler.ts`, `action-handler.ts`, `feature-handler.ts`
|
- Used in: `ups-handler.ts`, `group-handler.ts`, `service-handler.ts`, `action-handler.ts`,
|
||||||
|
`feature-handler.ts`
|
||||||
|
|
||||||
2. **Constants File (`ts/constants.ts`)**
|
2. **Constants File (`ts/constants.ts`)**
|
||||||
- Centralized all magic numbers (timeouts, intervals, thresholds)
|
- Centralized all magic numbers (timeouts, intervals, thresholds)
|
||||||
@@ -21,7 +22,8 @@
|
|||||||
### Phase 2 - Type Safety
|
### Phase 2 - Type Safety
|
||||||
|
|
||||||
4. **Circular Dependency Fix (`ts/interfaces/nupst-accessor.ts`)**
|
4. **Circular Dependency Fix (`ts/interfaces/nupst-accessor.ts`)**
|
||||||
- Created `INupstAccessor` interface to break the circular dependency between `Nupst` and `NupstSnmp`
|
- Created `INupstAccessor` interface to break the circular dependency between `Nupst` and
|
||||||
|
`NupstSnmp`
|
||||||
- `NupstSnmp.nupst` property now uses the interface instead of `any`
|
- `NupstSnmp.nupst` property now uses the interface instead of `any`
|
||||||
|
|
||||||
5. **Webhook Payload Interface (`ts/actions/webhook-action.ts`)**
|
5. **Webhook Payload Interface (`ts/actions/webhook-action.ts`)**
|
||||||
@@ -30,11 +32,13 @@
|
|||||||
|
|
||||||
6. **CLI Handler Type Safety**
|
6. **CLI Handler Type Safety**
|
||||||
- Replaced `any` types in `ups-handler.ts` and `group-handler.ts` with proper interfaces
|
- Replaced `any` types in `ups-handler.ts` and `group-handler.ts` with proper interfaces
|
||||||
- Uses: `IUpsConfig`, `INupstConfig`, `ISnmpConfig`, `IActionConfig`, `IThresholds`, `ISnmpUpsStatus`
|
- Uses: `IUpsConfig`, `INupstConfig`, `ISnmpConfig`, `IActionConfig`, `IThresholds`,
|
||||||
|
`ISnmpUpsStatus`
|
||||||
|
|
||||||
## Architecture Notes
|
## Architecture Notes
|
||||||
|
|
||||||
- **SNMP Manager**: Uses `INupstAccessor` interface (not direct `Nupst` reference) to avoid circular imports
|
- **SNMP Manager**: Uses `INupstAccessor` interface (not direct `Nupst` reference) to avoid circular
|
||||||
|
imports
|
||||||
- **CLI Handlers**: All use the `helpers.withPrompt()` utility for interactive input
|
- **CLI Handlers**: All use the `helpers.withPrompt()` utility for interactive input
|
||||||
- **Constants**: All timing values should be referenced from `ts/constants.ts`
|
- **Constants**: All timing values should be referenced from `ts/constants.ts`
|
||||||
- **Actions**: Use `IActionConfig` from `ts/actions/base-action.ts` for action configuration
|
- **Actions**: Use `IActionConfig` from `ts/actions/base-action.ts` for action configuration
|
||||||
|
|||||||
54
readme.md
54
readme.md
@@ -10,7 +10,11 @@ no setup, just run.
|
|||||||
|
|
||||||
## Issue Reporting and Security
|
## Issue Reporting and Security
|
||||||
|
|
||||||
For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.
|
For reporting bugs, issues, or security vulnerabilities, please visit
|
||||||
|
[community.foss.global/](https://community.foss.global/). This is the central community hub for all
|
||||||
|
issue reporting. Developers who sign and comply with our contribution agreement and go through
|
||||||
|
identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull
|
||||||
|
Requests directly.
|
||||||
|
|
||||||
## ✨ Features
|
## ✨ Features
|
||||||
|
|
||||||
@@ -131,7 +135,8 @@ Alternatively, NUPST can be installed via npm:
|
|||||||
npm install -g @serve.zone/nupst
|
npm install -g @serve.zone/nupst
|
||||||
```
|
```
|
||||||
|
|
||||||
**Note:** This method downloads the appropriate pre-compiled binary for your platform during installation.
|
**Note:** This method downloads the appropriate pre-compiled binary for your platform during
|
||||||
|
installation.
|
||||||
|
|
||||||
### Verify Installation
|
### Verify Installation
|
||||||
|
|
||||||
@@ -474,18 +479,18 @@ Actions define automated responses to UPS conditions:
|
|||||||
|
|
||||||
**Webhook-Specific Fields:**
|
**Webhook-Specific Fields:**
|
||||||
|
|
||||||
| Field | Description | Values |
|
| Field | Description | Values |
|
||||||
| ---------------- | -------------------------- | ---------------- |
|
| ---------------- | --------------------- | --------------- |
|
||||||
| `webhookUrl` | URL to call | HTTP/HTTPS URL |
|
| `webhookUrl` | URL to call | HTTP/HTTPS URL |
|
||||||
| `webhookMethod` | HTTP method | 'POST' or 'GET' |
|
| `webhookMethod` | HTTP method | 'POST' or 'GET' |
|
||||||
| `webhookTimeout` | Request timeout in ms | Default: 10000 |
|
| `webhookTimeout` | Request timeout in ms | Default: 10000 |
|
||||||
|
|
||||||
**Script-Specific Fields:**
|
**Script-Specific Fields:**
|
||||||
|
|
||||||
| Field | Description | Values |
|
| Field | Description | Values |
|
||||||
| --------------- | ---------------------------------- | ----------------------- |
|
| --------------- | -------------------------------- | ------------------- |
|
||||||
| `scriptPath` | Script filename in `/etc/nupst/` | Must end with `.sh` |
|
| `scriptPath` | Script filename in `/etc/nupst/` | Must end with `.sh` |
|
||||||
| `scriptTimeout` | Execution timeout in ms | Default: 60000 |
|
| `scriptTimeout` | Execution timeout in ms | Default: 60000 |
|
||||||
|
|
||||||
**Trigger Modes:**
|
**Trigger Modes:**
|
||||||
|
|
||||||
@@ -874,7 +879,8 @@ curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh |
|
|||||||
|
|
||||||
### Configuration Compatibility
|
### Configuration Compatibility
|
||||||
|
|
||||||
Your v3.x configuration is **fully compatible**. The migration system automatically converts older formats to the current version.
|
Your v3.x configuration is **fully compatible**. The migration system automatically converts older
|
||||||
|
formats to the current version.
|
||||||
|
|
||||||
## 💻 Development
|
## 💻 Development
|
||||||
|
|
||||||
@@ -927,21 +933,31 @@ nupst/
|
|||||||
|
|
||||||
## License and Legal Information
|
## License and Legal Information
|
||||||
|
|
||||||
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./LICENSE) file.
|
This repository contains open-source code licensed under the MIT License. A copy of the license can
|
||||||
|
be found in the [LICENSE](./LICENSE) file.
|
||||||
|
|
||||||
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
**Please note:** The MIT License does not grant permission to use the trade names, trademarks,
|
||||||
|
service marks, or product names of the project, except as required for reasonable and customary use
|
||||||
|
in describing the origin of the work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
### Trademarks
|
### Trademarks
|
||||||
|
|
||||||
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.
|
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated
|
||||||
|
with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture
|
||||||
|
Capital GmbH or third parties, and are not included within the scope of the MIT license granted
|
||||||
|
herein.
|
||||||
|
|
||||||
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.
|
Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the
|
||||||
|
guidelines of the respective third-party owners, and any usage must be approved in writing.
|
||||||
|
Third-party trademarks used herein are the property of their respective owners and used only in a
|
||||||
|
descriptive manner, e.g. for an implementation of an API or similar.
|
||||||
|
|
||||||
### Company Information
|
### Company Information
|
||||||
|
|
||||||
Task Venture Capital GmbH
|
Task Venture Capital GmbH Registered at District Court Bremen HRB 35230 HB, Germany
|
||||||
Registered at District Court Bremen HRB 35230 HB, Germany
|
|
||||||
|
|
||||||
For any legal inquiries or further information, please contact us via email at hello@task.vc.
|
For any legal inquiries or further information, please contact us via email at hello@task.vc.
|
||||||
|
|
||||||
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
By using this repository, you acknowledge that you have read this section, agree to comply with its
|
||||||
|
terms, and understand that the licensing of the code does not imply endorsement by Task Venture
|
||||||
|
Capital GmbH of any derivative works.
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
// deno-lint-ignore-file no-unused-vars
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* NUPST npm postinstall script
|
* NUPST npm postinstall script
|
||||||
* Downloads the appropriate binary for the current platform from GitHub releases
|
* Downloads the appropriate binary for the current platform from GitHub releases
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { platform, arch } from 'os';
|
import { arch, platform } from 'os';
|
||||||
import { existsSync, mkdirSync, writeFileSync, chmodSync, unlinkSync } from 'fs';
|
import { chmodSync, existsSync, mkdirSync, unlinkSync } from 'fs';
|
||||||
import { join, dirname } from 'path';
|
import { dirname, join } from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import https from 'https';
|
import https from 'https';
|
||||||
import { pipeline } from 'stream';
|
import { pipeline } from 'stream';
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
import { createWriteStream } from 'fs';
|
import { createWriteStream } from 'fs';
|
||||||
|
import process from "node:process";
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = dirname(__filename);
|
const __dirname = dirname(__filename);
|
||||||
@@ -29,12 +31,12 @@ function getBinaryInfo() {
|
|||||||
const platformMap = {
|
const platformMap = {
|
||||||
'darwin': 'macos',
|
'darwin': 'macos',
|
||||||
'linux': 'linux',
|
'linux': 'linux',
|
||||||
'win32': 'windows'
|
'win32': 'windows',
|
||||||
};
|
};
|
||||||
|
|
||||||
const archMap = {
|
const archMap = {
|
||||||
'x64': 'x64',
|
'x64': 'x64',
|
||||||
'arm64': 'arm64'
|
'arm64': 'arm64',
|
||||||
};
|
};
|
||||||
|
|
||||||
const mappedPlatform = platformMap[plat];
|
const mappedPlatform = platformMap[plat];
|
||||||
@@ -54,7 +56,7 @@ function getBinaryInfo() {
|
|||||||
platform: mappedPlatform,
|
platform: mappedPlatform,
|
||||||
arch: mappedArch,
|
arch: mappedArch,
|
||||||
binaryName,
|
binaryName,
|
||||||
originalPlatform: plat
|
originalPlatform: plat,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,7 +124,9 @@ async function main() {
|
|||||||
const binaryInfo = getBinaryInfo();
|
const binaryInfo = getBinaryInfo();
|
||||||
|
|
||||||
if (!binaryInfo.supported) {
|
if (!binaryInfo.supported) {
|
||||||
console.error(`❌ Error: Unsupported platform/architecture: ${binaryInfo.platform}/${binaryInfo.arch}`);
|
console.error(
|
||||||
|
`❌ Error: Unsupported platform/architecture: ${binaryInfo.platform}/${binaryInfo.arch}`,
|
||||||
|
);
|
||||||
console.error('');
|
console.error('');
|
||||||
console.error('Supported platforms:');
|
console.error('Supported platforms:');
|
||||||
console.error(' • Linux (x64, arm64)');
|
console.error(' • Linux (x64, arm64)');
|
||||||
@@ -185,7 +189,9 @@ async function main() {
|
|||||||
console.error('You can try:');
|
console.error('You can try:');
|
||||||
console.error('1. Installing from source: https://code.foss.global/serve.zone/nupst');
|
console.error('1. Installing from source: https://code.foss.global/serve.zone/nupst');
|
||||||
console.error('2. Downloading the binary manually from the releases page');
|
console.error('2. Downloading the binary manually from the releases page');
|
||||||
console.error('3. Using the install script: curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash');
|
console.error(
|
||||||
|
'3. Using the install script: curl -sSL https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh | sudo bash',
|
||||||
|
);
|
||||||
|
|
||||||
// Clean up partial download
|
// Clean up partial download
|
||||||
if (existsSync(binaryPath)) {
|
if (existsSync(binaryPath)) {
|
||||||
@@ -225,7 +231,7 @@ async function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Run the installation
|
// Run the installation
|
||||||
main().catch(err => {
|
main().catch((err) => {
|
||||||
console.error(`❌ Installation failed: ${err.message}`);
|
console.error(`❌ Installation failed: ${err.message}`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# Manual Docker Testing Scripts
|
# Manual Docker Testing Scripts
|
||||||
|
|
||||||
This directory contains scripts for manually testing NUPST installation and migration in Docker containers with systemd support.
|
This directory contains scripts for manually testing NUPST installation and migration in Docker
|
||||||
|
containers with systemd support.
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
@@ -15,12 +16,14 @@ This directory contains scripts for manually testing NUPST installation and migr
|
|||||||
Creates a Docker container with systemd and installs NUPST v3.
|
Creates a Docker container with systemd and installs NUPST v3.
|
||||||
|
|
||||||
**What it does:**
|
**What it does:**
|
||||||
|
|
||||||
- Creates Ubuntu 22.04 container with systemd enabled
|
- Creates Ubuntu 22.04 container with systemd enabled
|
||||||
- Installs NUPST v3 from commit `806f81c6` (last v3 version)
|
- Installs NUPST v3 from commit `806f81c6` (last v3 version)
|
||||||
- Enables and starts the systemd service
|
- Enables and starts the systemd service
|
||||||
- Leaves container running for testing
|
- Leaves container running for testing
|
||||||
|
|
||||||
**Usage:**
|
**Usage:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
chmod +x 01-setup-v3-container.sh
|
chmod +x 01-setup-v3-container.sh
|
||||||
./01-setup-v3-container.sh
|
./01-setup-v3-container.sh
|
||||||
@@ -33,6 +36,7 @@ chmod +x 01-setup-v3-container.sh
|
|||||||
Tests the migration from v3 to v4.
|
Tests the migration from v3 to v4.
|
||||||
|
|
||||||
**What it does:**
|
**What it does:**
|
||||||
|
|
||||||
- Checks current v3 installation
|
- Checks current v3 installation
|
||||||
- Pulls v4 code from `migration/deno-v4` branch
|
- Pulls v4 code from `migration/deno-v4` branch
|
||||||
- Runs install.sh (should auto-detect and migrate)
|
- Runs install.sh (should auto-detect and migrate)
|
||||||
@@ -40,6 +44,7 @@ Tests the migration from v3 to v4.
|
|||||||
- Tests basic commands
|
- Tests basic commands
|
||||||
|
|
||||||
**Usage:**
|
**Usage:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
chmod +x 02-test-v3-to-v4-migration.sh
|
chmod +x 02-test-v3-to-v4-migration.sh
|
||||||
./02-test-v3-to-v4-migration.sh
|
./02-test-v3-to-v4-migration.sh
|
||||||
@@ -52,6 +57,7 @@ chmod +x 02-test-v3-to-v4-migration.sh
|
|||||||
Removes the test container.
|
Removes the test container.
|
||||||
|
|
||||||
**Usage:**
|
**Usage:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
chmod +x 03-cleanup.sh
|
chmod +x 03-cleanup.sh
|
||||||
./03-cleanup.sh
|
./03-cleanup.sh
|
||||||
@@ -134,16 +140,19 @@ docker rm -f nupst-test-v3
|
|||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
### Container won't start
|
### Container won't start
|
||||||
|
|
||||||
- Ensure Docker daemon is running
|
- Ensure Docker daemon is running
|
||||||
- Check you have privileged access
|
- Check you have privileged access
|
||||||
- Try: `docker logs nupst-test-v3`
|
- Try: `docker logs nupst-test-v3`
|
||||||
|
|
||||||
### Systemd not working in container
|
### Systemd not working in container
|
||||||
|
|
||||||
- Requires Linux host (not macOS/Windows)
|
- Requires Linux host (not macOS/Windows)
|
||||||
- Needs `--privileged` and cgroup volume mounts
|
- Needs `--privileged` and cgroup volume mounts
|
||||||
- Check: `docker exec nupst-test-v3 systemctl --version`
|
- Check: `docker exec nupst-test-v3 systemctl --version`
|
||||||
|
|
||||||
### Migration fails
|
### Migration fails
|
||||||
|
|
||||||
- Check logs: `docker exec nupst-test-v3 journalctl -xe`
|
- Check logs: `docker exec nupst-test-v3 journalctl -xe`
|
||||||
- Verify install.sh ran: `docker exec nupst-test-v3 ls -la /opt/nupst/`
|
- Verify install.sh ran: `docker exec nupst-test-v3 ls -la /opt/nupst/`
|
||||||
- Check service: `docker exec nupst-test-v3 systemctl status nupst`
|
- Check service: `docker exec nupst-test-v3 systemctl status nupst`
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
* Run with: deno run --allow-all test/showcase.ts
|
* Run with: deno run --allow-all test/showcase.ts
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { logger, type ITableColumn } from '../ts/logger.ts';
|
import { type ITableColumn, logger } from '../ts/logger.ts';
|
||||||
import { theme, symbols, getBatteryColor, formatPowerStatus } from '../ts/colors.ts';
|
import { formatPowerStatus, getBatteryColor, symbols, theme } from '../ts/colors.ts';
|
||||||
|
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log('═'.repeat(80));
|
console.log('═'.repeat(80));
|
||||||
@@ -38,31 +38,51 @@ logger.logBoxEnd();
|
|||||||
|
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
||||||
logger.logBox('Success Box (Green)', [
|
logger.logBox(
|
||||||
'Used for successful operations',
|
'Success Box (Green)',
|
||||||
'Installation complete, service started, etc.',
|
[
|
||||||
], 60, 'success');
|
'Used for successful operations',
|
||||||
|
'Installation complete, service started, etc.',
|
||||||
|
],
|
||||||
|
60,
|
||||||
|
'success',
|
||||||
|
);
|
||||||
|
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
||||||
logger.logBox('Error Box (Red)', [
|
logger.logBox(
|
||||||
'Used for critical errors and failures',
|
'Error Box (Red)',
|
||||||
'Configuration errors, connection failures, etc.',
|
[
|
||||||
], 60, 'error');
|
'Used for critical errors and failures',
|
||||||
|
'Configuration errors, connection failures, etc.',
|
||||||
|
],
|
||||||
|
60,
|
||||||
|
'error',
|
||||||
|
);
|
||||||
|
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
||||||
logger.logBox('Warning Box (Yellow)', [
|
logger.logBox(
|
||||||
'Used for warnings and deprecations',
|
'Warning Box (Yellow)',
|
||||||
'Old command format, missing config, etc.',
|
[
|
||||||
], 60, 'warning');
|
'Used for warnings and deprecations',
|
||||||
|
'Old command format, missing config, etc.',
|
||||||
|
],
|
||||||
|
60,
|
||||||
|
'warning',
|
||||||
|
);
|
||||||
|
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
||||||
logger.logBox('Info Box (Cyan)', [
|
logger.logBox(
|
||||||
'Used for informational messages',
|
'Info Box (Cyan)',
|
||||||
'Version info, update available, etc.',
|
[
|
||||||
], 60, 'info');
|
'Used for informational messages',
|
||||||
|
'Version info, update available, etc.',
|
||||||
|
],
|
||||||
|
60,
|
||||||
|
'info',
|
||||||
|
);
|
||||||
|
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
||||||
@@ -112,15 +132,24 @@ const upsColumns: ITableColumn[] = [
|
|||||||
{ header: 'ID', key: 'id' },
|
{ header: 'ID', key: 'id' },
|
||||||
{ header: 'Name', key: 'name' },
|
{ header: 'Name', key: 'name' },
|
||||||
{ header: 'Host', key: 'host' },
|
{ header: 'Host', key: 'host' },
|
||||||
{ header: 'Status', key: 'status', color: (v) => {
|
{
|
||||||
if (v.includes('Online')) return theme.success(v);
|
header: 'Status',
|
||||||
if (v.includes('Battery')) return theme.warning(v);
|
key: 'status',
|
||||||
return theme.dim(v);
|
color: (v) => {
|
||||||
}},
|
if (v.includes('Online')) return theme.success(v);
|
||||||
{ header: 'Battery', key: 'battery', align: 'right', color: (v) => {
|
if (v.includes('Battery')) return theme.warning(v);
|
||||||
const pct = parseInt(v);
|
return theme.dim(v);
|
||||||
return getBatteryColor(pct)(v);
|
},
|
||||||
}},
|
},
|
||||||
|
{
|
||||||
|
header: 'Battery',
|
||||||
|
key: 'battery',
|
||||||
|
align: 'right',
|
||||||
|
color: (v) => {
|
||||||
|
const pct = parseInt(v);
|
||||||
|
return getBatteryColor(pct)(v);
|
||||||
|
},
|
||||||
|
},
|
||||||
{ header: 'Runtime', key: 'runtime', align: 'right' },
|
{ header: 'Runtime', key: 'runtime', align: 'right' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
47
test/test.ts
47
test/test.ts
@@ -1,9 +1,9 @@
|
|||||||
import { assert, assertEquals, assertExists } from 'jsr:@std/assert@^1.0.0';
|
import { assert, assertEquals, assertExists } from 'jsr:@std/assert@^1.0.0';
|
||||||
import { NupstSnmp } from '../ts/snmp/manager.ts';
|
import { NupstSnmp } from '../ts/snmp/manager.ts';
|
||||||
import { UpsOidSets } from '../ts/snmp/oid-sets.ts';
|
import { UpsOidSets } from '../ts/snmp/oid-sets.ts';
|
||||||
import type { ISnmpConfig, TUpsModel, IOidSet } from '../ts/snmp/types.ts';
|
import type { IOidSet, ISnmpConfig, TUpsModel } from '../ts/snmp/types.ts';
|
||||||
import { shortId } from '../ts/helpers/shortid.ts';
|
import { shortId } from '../ts/helpers/shortid.ts';
|
||||||
import { TIMING, SNMP, THRESHOLDS, HTTP_SERVER, UI } from '../ts/constants.ts';
|
import { HTTP_SERVER, SNMP, THRESHOLDS, TIMING, UI } from '../ts/constants.ts';
|
||||||
import { Action, type IActionContext } from '../ts/actions/base-action.ts';
|
import { Action, type IActionContext } from '../ts/actions/base-action.ts';
|
||||||
|
|
||||||
import * as qenv from 'npm:@push.rocks/qenv@^6.0.0';
|
import * as qenv from 'npm:@push.rocks/qenv@^6.0.0';
|
||||||
@@ -56,8 +56,14 @@ Deno.test('SNMP constants: port is 161', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
Deno.test('SNMP constants: timeouts increase with security level', () => {
|
Deno.test('SNMP constants: timeouts increase with security level', () => {
|
||||||
assert(SNMP.TIMEOUT_NO_AUTH_MS <= SNMP.TIMEOUT_AUTH_MS, 'Auth timeout should be >= noAuth timeout');
|
assert(
|
||||||
assert(SNMP.TIMEOUT_AUTH_MS <= SNMP.TIMEOUT_AUTH_PRIV_MS, 'AuthPriv timeout should be >= Auth timeout');
|
SNMP.TIMEOUT_NO_AUTH_MS <= SNMP.TIMEOUT_AUTH_MS,
|
||||||
|
'Auth timeout should be >= noAuth timeout',
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
SNMP.TIMEOUT_AUTH_MS <= SNMP.TIMEOUT_AUTH_PRIV_MS,
|
||||||
|
'AuthPriv timeout should be >= Auth timeout',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
Deno.test('THRESHOLDS constants: defaults are reasonable', () => {
|
Deno.test('THRESHOLDS constants: defaults are reasonable', () => {
|
||||||
@@ -92,21 +98,21 @@ Deno.test('UpsOidSets: all models have OID sets', () => {
|
|||||||
Deno.test('UpsOidSets: all non-custom models have complete OIDs', () => {
|
Deno.test('UpsOidSets: all non-custom models have complete OIDs', () => {
|
||||||
const requiredOids = ['POWER_STATUS', 'BATTERY_CAPACITY', 'BATTERY_RUNTIME', 'OUTPUT_LOAD'];
|
const requiredOids = ['POWER_STATUS', 'BATTERY_CAPACITY', 'BATTERY_RUNTIME', 'OUTPUT_LOAD'];
|
||||||
|
|
||||||
for (const model of UPS_MODELS.filter(m => m !== 'custom')) {
|
for (const model of UPS_MODELS.filter((m) => m !== 'custom')) {
|
||||||
const oidSet = UpsOidSets.getOidSet(model);
|
const oidSet = UpsOidSets.getOidSet(model);
|
||||||
|
|
||||||
for (const oid of requiredOids) {
|
for (const oid of requiredOids) {
|
||||||
const value = oidSet[oid as keyof IOidSet];
|
const value = oidSet[oid as keyof IOidSet];
|
||||||
assert(
|
assert(
|
||||||
typeof value === 'string' && value.length > 0,
|
typeof value === 'string' && value.length > 0,
|
||||||
`${model} should have non-empty ${oid}`
|
`${model} should have non-empty ${oid}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Deno.test('UpsOidSets: power status values defined for non-custom models', () => {
|
Deno.test('UpsOidSets: power status values defined for non-custom models', () => {
|
||||||
for (const model of UPS_MODELS.filter(m => m !== 'custom')) {
|
for (const model of UPS_MODELS.filter((m) => m !== 'custom')) {
|
||||||
const oidSet = UpsOidSets.getOidSet(model);
|
const oidSet = UpsOidSets.getOidSet(model);
|
||||||
assertExists(oidSet.POWER_STATUS_VALUES, `${model} should have POWER_STATUS_VALUES`);
|
assertExists(oidSet.POWER_STATUS_VALUES, `${model} should have POWER_STATUS_VALUES`);
|
||||||
assertExists(oidSet.POWER_STATUS_VALUES?.online, `${model} should have online value`);
|
assertExists(oidSet.POWER_STATUS_VALUES?.online, `${model} should have online value`);
|
||||||
@@ -200,11 +206,11 @@ Deno.test('Action.shouldExecute: onlyPowerChanges mode', () => {
|
|||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
action.testShouldExecute(createMockContext({ triggerReason: 'powerStatusChange' })),
|
action.testShouldExecute(createMockContext({ triggerReason: 'powerStatusChange' })),
|
||||||
true
|
true,
|
||||||
);
|
);
|
||||||
assertEquals(
|
assertEquals(
|
||||||
action.testShouldExecute(createMockContext({ triggerReason: 'thresholdViolation' })),
|
action.testShouldExecute(createMockContext({ triggerReason: 'thresholdViolation' })),
|
||||||
false
|
false,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -218,13 +224,13 @@ Deno.test('Action.shouldExecute: onlyThresholds mode', () => {
|
|||||||
// Below thresholds - should execute
|
// Below thresholds - should execute
|
||||||
assertEquals(
|
assertEquals(
|
||||||
action.testShouldExecute(createMockContext({ batteryCapacity: 50, batteryRuntime: 10 })),
|
action.testShouldExecute(createMockContext({ batteryCapacity: 50, batteryRuntime: 10 })),
|
||||||
true
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Above thresholds - should not execute
|
// Above thresholds - should not execute
|
||||||
assertEquals(
|
assertEquals(
|
||||||
action.testShouldExecute(createMockContext({ batteryCapacity: 100, batteryRuntime: 60 })),
|
action.testShouldExecute(createMockContext({ batteryCapacity: 100, batteryRuntime: 60 })),
|
||||||
false
|
false,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -248,7 +254,7 @@ Deno.test('Action.shouldExecute: powerChangesAndThresholds mode (default)', () =
|
|||||||
// Power change - should execute
|
// Power change - should execute
|
||||||
assertEquals(
|
assertEquals(
|
||||||
action.testShouldExecute(createMockContext({ triggerReason: 'powerStatusChange' })),
|
action.testShouldExecute(createMockContext({ triggerReason: 'powerStatusChange' })),
|
||||||
true
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Threshold violation - should execute
|
// Threshold violation - should execute
|
||||||
@@ -257,7 +263,7 @@ Deno.test('Action.shouldExecute: powerChangesAndThresholds mode (default)', () =
|
|||||||
triggerReason: 'thresholdViolation',
|
triggerReason: 'thresholdViolation',
|
||||||
batteryCapacity: 50,
|
batteryCapacity: 50,
|
||||||
})),
|
})),
|
||||||
true
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
// No power change and above thresholds - should not execute
|
// No power change and above thresholds - should not execute
|
||||||
@@ -267,7 +273,7 @@ Deno.test('Action.shouldExecute: powerChangesAndThresholds mode (default)', () =
|
|||||||
batteryCapacity: 100,
|
batteryCapacity: 100,
|
||||||
batteryRuntime: 60,
|
batteryRuntime: 60,
|
||||||
})),
|
})),
|
||||||
false
|
false,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -279,11 +285,11 @@ Deno.test('Action.shouldExecute: anyChange mode always returns true', () => {
|
|||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
action.testShouldExecute(createMockContext({ triggerReason: 'powerStatusChange' })),
|
action.testShouldExecute(createMockContext({ triggerReason: 'powerStatusChange' })),
|
||||||
true
|
true,
|
||||||
);
|
);
|
||||||
assertEquals(
|
assertEquals(
|
||||||
action.testShouldExecute(createMockContext({ triggerReason: 'thresholdViolation' })),
|
action.testShouldExecute(createMockContext({ triggerReason: 'thresholdViolation' })),
|
||||||
true
|
true,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -339,7 +345,7 @@ async function testUpsConnection(
|
|||||||
assertExists(status, 'Status should exist');
|
assertExists(status, 'Status should exist');
|
||||||
assert(
|
assert(
|
||||||
['online', 'onBattery', 'unknown'].includes(status.powerStatus),
|
['online', 'onBattery', 'unknown'].includes(status.powerStatus),
|
||||||
`Power status should be valid: ${status.powerStatus}`
|
`Power status should be valid: ${status.powerStatus}`,
|
||||||
);
|
);
|
||||||
assertEquals(typeof status.batteryCapacity, 'number', 'Battery capacity should be a number');
|
assertEquals(typeof status.batteryCapacity, 'number', 'Battery capacity should be a number');
|
||||||
assertEquals(typeof status.batteryRuntime, 'number', 'Battery runtime should be a number');
|
assertEquals(typeof status.batteryRuntime, 'number', 'Battery runtime should be a number');
|
||||||
@@ -347,9 +353,12 @@ async function testUpsConnection(
|
|||||||
// Validate ranges
|
// Validate ranges
|
||||||
assert(
|
assert(
|
||||||
status.batteryCapacity >= 0 && status.batteryCapacity <= 100,
|
status.batteryCapacity >= 0 && status.batteryCapacity <= 100,
|
||||||
`Battery capacity should be 0-100: ${status.batteryCapacity}`
|
`Battery capacity should be 0-100: ${status.batteryCapacity}`,
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
status.batteryRuntime >= 0,
|
||||||
|
`Battery runtime should be non-negative: ${status.batteryRuntime}`,
|
||||||
);
|
);
|
||||||
assert(status.batteryRuntime >= 0, `Battery runtime should be non-negative: ${status.batteryRuntime}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create SNMP instance for integration tests
|
// Create SNMP instance for integration tests
|
||||||
|
|||||||
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@serve.zone/nupst',
|
name: '@serve.zone/nupst',
|
||||||
version: '5.2.0',
|
version: '5.2.4',
|
||||||
description: 'Network UPS Shutdown Tool - Monitor SNMP-enabled UPS devices and orchestrate graceful system shutdowns during power emergencies'
|
description: 'Network UPS Shutdown Tool - Monitor SNMP-enabled UPS devices and orchestrate graceful system shutdowns during power emergencies'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import * as fs from 'node:fs';
|
|||||||
import process from 'node:process';
|
import process from 'node:process';
|
||||||
import { exec } from 'node:child_process';
|
import { exec } from 'node:child_process';
|
||||||
import { promisify } from 'node:util';
|
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 { logger } from '../logger.ts';
|
||||||
|
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
@@ -26,7 +26,11 @@ export class ScriptAction extends Action {
|
|||||||
async execute(context: IActionContext): Promise<void> {
|
async execute(context: IActionContext): Promise<void> {
|
||||||
// Check if we should execute based on trigger mode
|
// Check if we should execute based on trigger mode
|
||||||
if (!this.shouldExecute(context)) {
|
if (!this.shouldExecute(context)) {
|
||||||
logger.info(`Script action skipped (trigger mode: ${this.config.triggerMode || 'powerChangesAndThresholds'})`);
|
logger.info(
|
||||||
|
`Script action skipped (trigger mode: ${
|
||||||
|
this.config.triggerMode || 'powerChangesAndThresholds'
|
||||||
|
})`,
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,9 @@ export class ShutdownAction extends Action {
|
|||||||
// CRITICAL SAFETY CHECK: Shutdown should NEVER trigger unless UPS is on battery
|
// 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)
|
// A low battery while on grid power is not an emergency (the battery is charging)
|
||||||
if (context.powerStatus !== 'onBattery') {
|
if (context.powerStatus !== 'onBattery') {
|
||||||
logger.info(`Shutdown action skipped: UPS is not on battery (status: ${context.powerStatus})`);
|
logger.info(
|
||||||
|
`Shutdown action skipped: UPS is not on battery (status: ${context.powerStatus})`,
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,7 +56,9 @@ export class ShutdownAction extends Action {
|
|||||||
if (context.triggerReason === 'powerStatusChange') {
|
if (context.triggerReason === 'powerStatusChange') {
|
||||||
// 'onlyThresholds' mode ignores power status changes
|
// 'onlyThresholds' mode ignores power status changes
|
||||||
if (mode === 'onlyThresholds') {
|
if (mode === 'onlyThresholds') {
|
||||||
logger.info('Shutdown action skipped: triggerMode is onlyThresholds, ignoring power change');
|
logger.info(
|
||||||
|
'Shutdown action skipped: triggerMode is onlyThresholds, ignoring power change',
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,15 +75,22 @@ export class ShutdownAction extends Action {
|
|||||||
// the daemon for testing, or the UPS may have been on battery for a while.
|
// the daemon for testing, or the UPS may have been on battery for a while.
|
||||||
// Only trigger if mode explicitly includes power changes.
|
// Only trigger if mode explicitly includes power changes.
|
||||||
if (prev === 'unknown') {
|
if (prev === 'unknown') {
|
||||||
if (mode === 'onlyPowerChanges' || mode === 'powerChangesAndThresholds' || mode === 'anyChange') {
|
if (
|
||||||
logger.info('Shutdown action triggered: UPS on battery at daemon startup (unknown → onBattery)');
|
mode === 'onlyPowerChanges' || mode === 'powerChangesAndThresholds' ||
|
||||||
|
mode === 'anyChange'
|
||||||
|
) {
|
||||||
|
logger.info(
|
||||||
|
'Shutdown action triggered: UPS on battery at daemon startup (unknown → onBattery)',
|
||||||
|
);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Other transitions (e.g., onBattery → onBattery) should not trigger
|
// Other transitions (e.g., onBattery → onBattery) should not trigger
|
||||||
logger.info(`Shutdown action skipped: non-emergency transition (${prev} → ${context.powerStatus})`);
|
logger.info(
|
||||||
|
`Shutdown action skipped: non-emergency transition (${prev} → ${context.powerStatus})`,
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +109,11 @@ export class ShutdownAction extends Action {
|
|||||||
async execute(context: IActionContext): Promise<void> {
|
async execute(context: IActionContext): Promise<void> {
|
||||||
// Check if we should execute based on trigger mode and thresholds
|
// Check if we should execute based on trigger mode and thresholds
|
||||||
if (!this.shouldExecute(context)) {
|
if (!this.shouldExecute(context)) {
|
||||||
logger.info(`Shutdown action skipped (trigger mode: ${this.config.triggerMode || 'powerChangesAndThresholds'})`);
|
logger.info(
|
||||||
|
`Shutdown action skipped (trigger mode: ${
|
||||||
|
this.config.triggerMode || 'powerChangesAndThresholds'
|
||||||
|
})`,
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as http from 'node:http';
|
import * as http from 'node:http';
|
||||||
import * as https from 'node:https';
|
import * as https from 'node:https';
|
||||||
import { URL } from 'node:url';
|
import { URL } from 'node:url';
|
||||||
import { Action, type IActionConfig, type IActionContext } from './base-action.ts';
|
import { Action, type IActionContext } from './base-action.ts';
|
||||||
import { logger } from '../logger.ts';
|
import { logger } from '../logger.ts';
|
||||||
import { WEBHOOK } from '../constants.ts';
|
import { WEBHOOK } from '../constants.ts';
|
||||||
|
|
||||||
@@ -46,7 +46,11 @@ export class WebhookAction extends Action {
|
|||||||
async execute(context: IActionContext): Promise<void> {
|
async execute(context: IActionContext): Promise<void> {
|
||||||
// Check if we should execute based on trigger mode
|
// Check if we should execute based on trigger mode
|
||||||
if (!this.shouldExecute(context)) {
|
if (!this.shouldExecute(context)) {
|
||||||
logger.info(`Webhook action skipped (trigger mode: ${this.config.triggerMode || 'powerChangesAndThresholds'})`);
|
logger.info(
|
||||||
|
`Webhook action skipped (trigger mode: ${
|
||||||
|
this.config.triggerMode || 'powerChangesAndThresholds'
|
||||||
|
})`,
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +81,7 @@ export class WebhookAction extends Action {
|
|||||||
* @param method HTTP method (GET or POST)
|
* @param method HTTP method (GET or POST)
|
||||||
* @param timeout Request timeout in milliseconds
|
* @param timeout Request timeout in milliseconds
|
||||||
*/
|
*/
|
||||||
private async callWebhook(
|
private callWebhook(
|
||||||
context: IActionContext,
|
context: IActionContext,
|
||||||
method: 'GET' | 'POST',
|
method: 'GET' | 'POST',
|
||||||
timeout: number,
|
timeout: number,
|
||||||
@@ -109,7 +113,7 @@ export class WebhookAction extends Action {
|
|||||||
url.searchParams.append('powerStatus', payload.powerStatus);
|
url.searchParams.append('powerStatus', payload.powerStatus);
|
||||||
url.searchParams.append('batteryCapacity', String(payload.batteryCapacity));
|
url.searchParams.append('batteryCapacity', String(payload.batteryCapacity));
|
||||||
url.searchParams.append('batteryRuntime', String(payload.batteryRuntime));
|
url.searchParams.append('batteryRuntime', String(payload.batteryRuntime));
|
||||||
|
|
||||||
url.searchParams.append('triggerReason', payload.triggerReason);
|
url.searchParams.append('triggerReason', payload.triggerReason);
|
||||||
url.searchParams.append('timestamp', String(payload.timestamp));
|
url.searchParams.append('timestamp', String(payload.timestamp));
|
||||||
}
|
}
|
||||||
|
|||||||
187
ts/cli.ts
187
ts/cli.ts
@@ -1,7 +1,7 @@
|
|||||||
import { execSync } from 'node:child_process';
|
import { execSync } from 'node:child_process';
|
||||||
import { Nupst } from './nupst.ts';
|
import { Nupst } from './nupst.ts';
|
||||||
import { logger, type ITableColumn } from './logger.ts';
|
import { type ITableColumn, logger } from './logger.ts';
|
||||||
import { theme, symbols } from './colors.ts';
|
import { theme } from './colors.ts';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class for handling CLI commands
|
* Class for handling CLI commands
|
||||||
@@ -287,10 +287,15 @@ export class NupstCli {
|
|||||||
try {
|
try {
|
||||||
await this.nupst.getDaemon().loadConfig();
|
await this.nupst.getDaemon().loadConfig();
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
logger.logBox('Configuration Error', [
|
logger.logBox(
|
||||||
'No configuration found.',
|
'Configuration Error',
|
||||||
"Please run 'nupst ups add' first to create a configuration.",
|
[
|
||||||
], 50, 'error');
|
'No configuration found.',
|
||||||
|
"Please run 'nupst ups add' first to create a configuration.",
|
||||||
|
],
|
||||||
|
50,
|
||||||
|
'error',
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,17 +305,22 @@ export class NupstCli {
|
|||||||
// 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 ===
|
||||||
|
|
||||||
// Overview Box
|
// Overview Box
|
||||||
logger.log('');
|
logger.log('');
|
||||||
logger.logBox('NUPST Configuration', [
|
logger.logBox(
|
||||||
`UPS Devices: ${theme.highlight(String(config.upsDevices.length))}`,
|
'NUPST Configuration',
|
||||||
`Groups: ${theme.highlight(String(config.groups ? config.groups.length : 0))}`,
|
[
|
||||||
`Check Interval: ${theme.info(String(config.checkInterval / 1000))} seconds`,
|
`UPS Devices: ${theme.highlight(String(config.upsDevices.length))}`,
|
||||||
'',
|
`Groups: ${theme.highlight(String(config.groups ? config.groups.length : 0))}`,
|
||||||
theme.dim('Configuration File:'),
|
`Check Interval: ${theme.info(String(config.checkInterval / 1000))} seconds`,
|
||||||
` ${theme.path('/etc/nupst/config.json')}`,
|
'',
|
||||||
], 60, 'info');
|
theme.dim('Configuration File:'),
|
||||||
|
` ${theme.path('/etc/nupst/config.json')}`,
|
||||||
|
],
|
||||||
|
60,
|
||||||
|
'info',
|
||||||
|
);
|
||||||
|
|
||||||
// HTTP Server Status (if configured)
|
// HTTP Server Status (if configured)
|
||||||
if (config.httpServer) {
|
if (config.httpServer) {
|
||||||
@@ -319,17 +329,24 @@ export class NupstCli {
|
|||||||
: theme.dim('Disabled');
|
: theme.dim('Disabled');
|
||||||
|
|
||||||
logger.log('');
|
logger.log('');
|
||||||
logger.logBox('HTTP Server', [
|
logger.logBox(
|
||||||
`Status: ${serverStatus}`,
|
'HTTP Server',
|
||||||
...(config.httpServer.enabled ? [
|
[
|
||||||
`Port: ${theme.highlight(String(config.httpServer.port))}`,
|
`Status: ${serverStatus}`,
|
||||||
`Path: ${theme.highlight(config.httpServer.path)}`,
|
...(config.httpServer.enabled
|
||||||
`Auth Token: ${theme.dim('***' + config.httpServer.authToken.slice(-4))}`,
|
? [
|
||||||
'',
|
`Port: ${theme.highlight(String(config.httpServer.port))}`,
|
||||||
theme.dim('Usage:'),
|
`Path: ${theme.highlight(config.httpServer.path)}`,
|
||||||
` curl -H "Authorization: Bearer TOKEN" http://localhost:${config.httpServer.port}${config.httpServer.path}`,
|
`Auth Token: ${theme.dim('***' + config.httpServer.authToken.slice(-4))}`,
|
||||||
] : []),
|
'',
|
||||||
], 70, config.httpServer.enabled ? 'success' : 'default');
|
theme.dim('Usage:'),
|
||||||
|
` curl -H "Authorization: Bearer TOKEN" http://localhost:${config.httpServer.port}${config.httpServer.path}`,
|
||||||
|
]
|
||||||
|
: []),
|
||||||
|
],
|
||||||
|
70,
|
||||||
|
config.httpServer.enabled ? 'success' : 'default',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// UPS Devices Table
|
// UPS Devices Table
|
||||||
@@ -369,8 +386,8 @@ export class NupstCli {
|
|||||||
id: theme.dim(group.id),
|
id: theme.dim(group.id),
|
||||||
mode: group.mode,
|
mode: group.mode,
|
||||||
upsCount: String(upsInGroup.length),
|
upsCount: String(upsInGroup.length),
|
||||||
ups: upsInGroup.length > 0
|
ups: upsInGroup.length > 0
|
||||||
? upsInGroup.map((ups) => ups.name).join(', ')
|
? upsInGroup.map((ups) => ups.name).join(', ')
|
||||||
: theme.dim('None'),
|
: theme.dim('None'),
|
||||||
description: group.description || theme.dim('—'),
|
description: group.description || theme.dim('—'),
|
||||||
};
|
};
|
||||||
@@ -392,62 +409,68 @@ export class NupstCli {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// === Legacy Single UPS Configuration ===
|
// === Legacy Single UPS Configuration ===
|
||||||
|
|
||||||
if (!config.snmp) {
|
if (!config.snmp) {
|
||||||
logger.logBox('Configuration Error', [
|
logger.logBox(
|
||||||
'Error: Legacy configuration missing SNMP settings',
|
'Configuration Error',
|
||||||
], 60, 'error');
|
[
|
||||||
|
'Error: Legacy configuration missing SNMP settings',
|
||||||
|
],
|
||||||
|
60,
|
||||||
|
'error',
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.log('');
|
logger.log('');
|
||||||
logger.logBox('NUPST Configuration (Legacy)', [
|
logger.logBox(
|
||||||
theme.warning('Legacy single-UPS configuration format'),
|
'NUPST Configuration (Legacy)',
|
||||||
'',
|
[
|
||||||
theme.dim('SNMP Settings:'),
|
theme.warning('Legacy single-UPS configuration format'),
|
||||||
` Host: ${theme.info(config.snmp.host)}`,
|
'',
|
||||||
` Port: ${theme.info(String(config.snmp.port))}`,
|
theme.dim('SNMP Settings:'),
|
||||||
` Version: ${config.snmp.version}`,
|
` Host: ${theme.info(config.snmp.host)}`,
|
||||||
` UPS Model: ${config.snmp.upsModel || 'cyberpower'}`,
|
` Port: ${theme.info(String(config.snmp.port))}`,
|
||||||
...(config.snmp.version === 1 || config.snmp.version === 2
|
` Version: ${config.snmp.version}`,
|
||||||
? [` Community: ${config.snmp.community}`]
|
` UPS Model: ${config.snmp.upsModel || 'cyberpower'}`,
|
||||||
: []
|
...(config.snmp.version === 1 || config.snmp.version === 2
|
||||||
),
|
? [` Community: ${config.snmp.community}`]
|
||||||
...(config.snmp.version === 3
|
: []),
|
||||||
? [
|
...(config.snmp.version === 3
|
||||||
|
? [
|
||||||
` Security Level: ${config.snmp.securityLevel}`,
|
` Security Level: ${config.snmp.securityLevel}`,
|
||||||
` Username: ${config.snmp.username}`,
|
` Username: ${config.snmp.username}`,
|
||||||
...(config.snmp.securityLevel === 'authNoPriv' || config.snmp.securityLevel === 'authPriv'
|
...(config.snmp.securityLevel === 'authNoPriv' ||
|
||||||
|
config.snmp.securityLevel === 'authPriv'
|
||||||
? [` Auth Protocol: ${config.snmp.authProtocol || 'None'}`]
|
? [` Auth Protocol: ${config.snmp.authProtocol || 'None'}`]
|
||||||
: []
|
: []),
|
||||||
),
|
|
||||||
...(config.snmp.securityLevel === 'authPriv'
|
...(config.snmp.securityLevel === 'authPriv'
|
||||||
? [` Privacy Protocol: ${config.snmp.privProtocol || 'None'}`]
|
? [` Privacy Protocol: ${config.snmp.privProtocol || 'None'}`]
|
||||||
: []
|
: []),
|
||||||
),
|
|
||||||
` Timeout: ${config.snmp.timeout / 1000} seconds`,
|
` Timeout: ${config.snmp.timeout / 1000} seconds`,
|
||||||
]
|
]
|
||||||
: []
|
: []),
|
||||||
),
|
...(config.snmp.upsModel === 'custom' && config.snmp.customOIDs
|
||||||
...(config.snmp.upsModel === 'custom' && config.snmp.customOIDs
|
? [
|
||||||
? [
|
|
||||||
theme.dim('Custom OIDs:'),
|
theme.dim('Custom OIDs:'),
|
||||||
` Power Status: ${config.snmp.customOIDs.POWER_STATUS || 'Not set'}`,
|
` Power Status: ${config.snmp.customOIDs.POWER_STATUS || 'Not set'}`,
|
||||||
` Battery Capacity: ${config.snmp.customOIDs.BATTERY_CAPACITY || 'Not set'}`,
|
` Battery Capacity: ${config.snmp.customOIDs.BATTERY_CAPACITY || 'Not set'}`,
|
||||||
` Battery Runtime: ${config.snmp.customOIDs.BATTERY_RUNTIME || 'Not set'}`,
|
` Battery Runtime: ${config.snmp.customOIDs.BATTERY_RUNTIME || 'Not set'}`,
|
||||||
]
|
]
|
||||||
: []
|
: []),
|
||||||
),
|
'',
|
||||||
'',
|
|
||||||
|
` Check Interval: ${config.checkInterval / 1000} seconds`,
|
||||||
` Check Interval: ${config.checkInterval / 1000} seconds`,
|
'',
|
||||||
'',
|
theme.dim('Configuration File:'),
|
||||||
theme.dim('Configuration File:'),
|
` ${theme.path('/etc/nupst/config.json')}`,
|
||||||
` ${theme.path('/etc/nupst/config.json')}`,
|
'',
|
||||||
'',
|
theme.warning('Note: Using legacy single-UPS configuration format.'),
|
||||||
theme.warning('Note: Using legacy single-UPS configuration format.'),
|
`Consider using ${theme.command('nupst ups add')} to migrate to multi-UPS format.`,
|
||||||
`Consider using ${theme.command('nupst ups add')} to migrate to multi-UPS format.`,
|
],
|
||||||
], 70, 'warning');
|
70,
|
||||||
|
'warning',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Service Status
|
// Service Status
|
||||||
@@ -458,10 +481,15 @@ export class NupstCli {
|
|||||||
execSync('systemctl is-enabled nupst.service || true').toString().trim() === 'enabled';
|
execSync('systemctl is-enabled nupst.service || true').toString().trim() === 'enabled';
|
||||||
|
|
||||||
logger.log('');
|
logger.log('');
|
||||||
logger.logBox('Service Status', [
|
logger.logBox(
|
||||||
`Active: ${isActive ? theme.success('Yes') : theme.dim('No')}`,
|
'Service Status',
|
||||||
`Enabled: ${isEnabled ? theme.success('Yes') : theme.dim('No')}`,
|
[
|
||||||
], 50, isActive ? 'success' : 'default');
|
`Active: ${isActive ? theme.success('Yes') : theme.dim('No')}`,
|
||||||
|
`Enabled: ${isEnabled ? theme.success('Yes') : theme.dim('No')}`,
|
||||||
|
],
|
||||||
|
50,
|
||||||
|
isActive ? 'success' : 'default',
|
||||||
|
);
|
||||||
logger.log('');
|
logger.log('');
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
// Ignore errors checking service status
|
// Ignore errors checking service status
|
||||||
@@ -514,8 +542,16 @@ export class NupstCli {
|
|||||||
|
|
||||||
// Service subcommands
|
// Service subcommands
|
||||||
logger.log(theme.info('Service Subcommands:'));
|
logger.log(theme.info('Service Subcommands:'));
|
||||||
this.printCommand('nupst service enable', 'Install and enable systemd service', theme.dim('(requires root)'));
|
this.printCommand(
|
||||||
this.printCommand('nupst service disable', 'Stop and disable systemd service', theme.dim('(requires root)'));
|
'nupst service enable',
|
||||||
|
'Install and enable systemd service',
|
||||||
|
theme.dim('(requires root)'),
|
||||||
|
);
|
||||||
|
this.printCommand(
|
||||||
|
'nupst service disable',
|
||||||
|
'Stop and disable systemd service',
|
||||||
|
theme.dim('(requires root)'),
|
||||||
|
);
|
||||||
this.printCommand('nupst service start', 'Start the systemd service');
|
this.printCommand('nupst service start', 'Start the systemd service');
|
||||||
this.printCommand('nupst service stop', 'Stop the systemd service');
|
this.printCommand('nupst service stop', 'Stop the systemd service');
|
||||||
this.printCommand('nupst service restart', 'Restart the systemd service');
|
this.printCommand('nupst service restart', 'Restart the systemd service');
|
||||||
@@ -545,7 +581,10 @@ export class NupstCli {
|
|||||||
logger.log(theme.info('Action Subcommands:'));
|
logger.log(theme.info('Action Subcommands:'));
|
||||||
this.printCommand('nupst action add <target-id>', 'Add a new action to a UPS or group');
|
this.printCommand('nupst action add <target-id>', 'Add a new action to a UPS or group');
|
||||||
this.printCommand('nupst action remove <target-id> <index>', 'Remove an action by index');
|
this.printCommand('nupst action remove <target-id> <index>', 'Remove an action by index');
|
||||||
this.printCommand('nupst action list [target-id]', 'List all actions (optionally for specific target)');
|
this.printCommand(
|
||||||
|
'nupst action list [target-id]',
|
||||||
|
'List all actions (optionally for specific target)',
|
||||||
|
);
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
||||||
// Feature subcommands
|
// Feature subcommands
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import process from 'node:process';
|
import process from 'node:process';
|
||||||
import { Nupst } from '../nupst.ts';
|
import { Nupst } from '../nupst.ts';
|
||||||
import { logger, type ITableColumn } from '../logger.ts';
|
import { type ITableColumn, logger } from '../logger.ts';
|
||||||
import { theme, symbols } from '../colors.ts';
|
import { symbols, theme } from '../colors.ts';
|
||||||
import type { IActionConfig } from '../actions/base-action.ts';
|
import type { IActionConfig } from '../actions/base-action.ts';
|
||||||
import type { IUpsConfig, IGroupConfig } from '../daemon.ts';
|
import type { IGroupConfig, IUpsConfig } from '../daemon.ts';
|
||||||
import * as helpers from '../helpers/index.ts';
|
import * as helpers from '../helpers/index.ts';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -48,7 +48,9 @@ export class ActionHandler {
|
|||||||
if (!ups && !group) {
|
if (!ups && !group) {
|
||||||
logger.error(`UPS or Group with ID '${targetId}' not found`);
|
logger.error(`UPS or Group with ID '${targetId}' not found`);
|
||||||
logger.log('');
|
logger.log('');
|
||||||
logger.log(` ${theme.dim('List available UPS devices:')} ${theme.command('nupst ups list')}`);
|
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(` ${theme.dim('List available groups:')} ${theme.command('nupst group list')}`);
|
||||||
logger.log('');
|
logger.log('');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
@@ -90,12 +92,16 @@ export class ActionHandler {
|
|||||||
// Trigger mode
|
// Trigger mode
|
||||||
logger.log('');
|
logger.log('');
|
||||||
logger.log(` ${theme.dim('Trigger mode:')}`);
|
logger.log(` ${theme.dim('Trigger mode:')}`);
|
||||||
logger.log(` ${theme.dim('1)')} onlyPowerChanges - Trigger only when power status changes`);
|
logger.log(
|
||||||
|
` ${theme.dim('1)')} onlyPowerChanges - Trigger only when power status changes`,
|
||||||
|
);
|
||||||
logger.log(
|
logger.log(
|
||||||
` ${theme.dim('2)')} onlyThresholds - Trigger only when thresholds are violated`,
|
` ${theme.dim('2)')} onlyThresholds - Trigger only when thresholds are violated`,
|
||||||
);
|
);
|
||||||
logger.log(
|
logger.log(
|
||||||
` ${theme.dim('3)')} powerChangesAndThresholds - Trigger on power change AND thresholds`,
|
` ${
|
||||||
|
theme.dim('3)')
|
||||||
|
} powerChangesAndThresholds - Trigger on power change AND thresholds`,
|
||||||
);
|
);
|
||||||
logger.log(` ${theme.dim('4)')} anyChange - Trigger on any status change`);
|
logger.log(` ${theme.dim('4)')} anyChange - Trigger on any status change`);
|
||||||
const triggerChoice = await prompt(` ${theme.dim('Choice')} ${theme.dim('[2]:')} `);
|
const triggerChoice = await prompt(` ${theme.dim('Choice')} ${theme.dim('[2]:')} `);
|
||||||
@@ -158,7 +164,9 @@ export class ActionHandler {
|
|||||||
if (!targetId || !actionIndexStr) {
|
if (!targetId || !actionIndexStr) {
|
||||||
logger.error('Target ID and action index are required');
|
logger.error('Target ID and action index are required');
|
||||||
logger.log(
|
logger.log(
|
||||||
` ${theme.dim('Usage:')} ${theme.command('nupst action remove <ups-id|group-id> <action-index>')}`,
|
` ${theme.dim('Usage:')} ${
|
||||||
|
theme.command('nupst action remove <ups-id|group-id> <action-index>')
|
||||||
|
}`,
|
||||||
);
|
);
|
||||||
logger.log('');
|
logger.log('');
|
||||||
logger.log(` ${theme.dim('List actions:')} ${theme.command('nupst action list')}`);
|
logger.log(` ${theme.dim('List actions:')} ${theme.command('nupst action list')}`);
|
||||||
@@ -182,7 +190,9 @@ export class ActionHandler {
|
|||||||
if (!ups && !group) {
|
if (!ups && !group) {
|
||||||
logger.error(`UPS or Group with ID '${targetId}' not found`);
|
logger.error(`UPS or Group with ID '${targetId}' not found`);
|
||||||
logger.log('');
|
logger.log('');
|
||||||
logger.log(` ${theme.dim('List available UPS devices:')} ${theme.command('nupst ups list')}`);
|
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(` ${theme.dim('List available groups:')} ${theme.command('nupst group list')}`);
|
||||||
logger.log('');
|
logger.log('');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
@@ -200,7 +210,9 @@ export class ActionHandler {
|
|||||||
|
|
||||||
if (actionIndex >= target!.actions.length) {
|
if (actionIndex >= target!.actions.length) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`Invalid action index. ${targetType} '${targetName}' has ${target!.actions.length} action(s) (index 0-${target!.actions.length - 1})`,
|
`Invalid action index. ${targetType} '${targetName}' has ${
|
||||||
|
target!.actions.length
|
||||||
|
} action(s) (index 0-${target!.actions.length - 1})`,
|
||||||
);
|
);
|
||||||
logger.log('');
|
logger.log('');
|
||||||
logger.log(
|
logger.log(
|
||||||
@@ -220,7 +232,9 @@ export class ActionHandler {
|
|||||||
logger.log(` ${theme.dim('Type:')} ${removedAction.type}`);
|
logger.log(` ${theme.dim('Type:')} ${removedAction.type}`);
|
||||||
if (removedAction.thresholds) {
|
if (removedAction.thresholds) {
|
||||||
logger.log(
|
logger.log(
|
||||||
` ${theme.dim('Thresholds:')} Battery: ${removedAction.thresholds.battery}%, Runtime: ${removedAction.thresholds.runtime}min`,
|
` ${
|
||||||
|
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(` ${theme.dim('Changes saved and will be applied automatically')}`);
|
||||||
@@ -248,8 +262,12 @@ export class ActionHandler {
|
|||||||
if (!ups && !group) {
|
if (!ups && !group) {
|
||||||
logger.error(`UPS or Group with ID '${targetId}' not found`);
|
logger.error(`UPS or Group with ID '${targetId}' not found`);
|
||||||
logger.log('');
|
logger.log('');
|
||||||
logger.log(` ${theme.dim('List available UPS devices:')} ${theme.command('nupst ups list')}`);
|
logger.log(
|
||||||
logger.log(` ${theme.dim('List available groups:')} ${theme.command('nupst group list')}`);
|
` ${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('');
|
logger.log('');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
@@ -287,7 +305,9 @@ export class ActionHandler {
|
|||||||
logger.log(` ${theme.dim('No actions configured')}`);
|
logger.log(` ${theme.dim('No actions configured')}`);
|
||||||
logger.log('');
|
logger.log('');
|
||||||
logger.log(
|
logger.log(
|
||||||
` ${theme.dim('Add an action:')} ${theme.command('nupst action add <ups-id|group-id>')}`,
|
` ${theme.dim('Add an action:')} ${
|
||||||
|
theme.command('nupst action add <ups-id|group-id>')
|
||||||
|
}`,
|
||||||
);
|
);
|
||||||
logger.log('');
|
logger.log('');
|
||||||
}
|
}
|
||||||
@@ -308,7 +328,9 @@ export class ActionHandler {
|
|||||||
targetType: 'UPS' | 'Group',
|
targetType: 'UPS' | 'Group',
|
||||||
): void {
|
): void {
|
||||||
logger.log(
|
logger.log(
|
||||||
`${symbols.info} ${targetType} ${theme.highlight(target.name)} ${theme.dim(`(${target.id})`)}`,
|
`${symbols.info} ${targetType} ${theme.highlight(target.name)} ${
|
||||||
|
theme.dim(`(${target.id})`)
|
||||||
|
}`,
|
||||||
);
|
);
|
||||||
logger.log('');
|
logger.log('');
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
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 } from '../logger.ts';
|
||||||
@@ -29,7 +28,9 @@ export class FeatureHandler {
|
|||||||
await this.runHttpServerConfig(prompt);
|
await this.runHttpServerConfig(prompt);
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`HTTP Server config error: ${error instanceof Error ? error.message : String(error)}`);
|
logger.error(
|
||||||
|
`HTTP Server config error: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,7 +150,9 @@ export class FeatureHandler {
|
|||||||
logger.logBoxLine(`Auth Token: ${theme.warning(authToken)}`);
|
logger.logBoxLine(`Auth Token: ${theme.warning(authToken)}`);
|
||||||
logger.logBoxLine('');
|
logger.logBoxLine('');
|
||||||
logger.logBoxLine(theme.dim('Usage examples:'));
|
logger.logBoxLine(theme.dim('Usage examples:'));
|
||||||
logger.logBoxLine(` curl -H "Authorization: Bearer ${authToken}" http://localhost:${port}${finalPath}`);
|
logger.logBoxLine(
|
||||||
|
` curl -H "Authorization: Bearer ${authToken}" http://localhost:${port}${finalPath}`,
|
||||||
|
);
|
||||||
logger.logBoxLine(` curl "http://localhost:${port}${finalPath}?token=${authToken}"`);
|
logger.logBoxLine(` curl "http://localhost:${port}${finalPath}?token=${authToken}"`);
|
||||||
logger.logBoxEnd();
|
logger.logBoxEnd();
|
||||||
logger.log('');
|
logger.log('');
|
||||||
@@ -165,7 +168,8 @@ export class FeatureHandler {
|
|||||||
*/
|
*/
|
||||||
private async restartServiceIfRunning(): Promise<void> {
|
private async restartServiceIfRunning(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const isActive = execSync('systemctl is-active nupst.service || true').toString().trim() === 'active';
|
const isActive =
|
||||||
|
execSync('systemctl is-active nupst.service || true').toString().trim() === 'active';
|
||||||
|
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
logger.log('');
|
logger.log('');
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import process from 'node:process';
|
|
||||||
import { Nupst } from '../nupst.ts';
|
import { Nupst } from '../nupst.ts';
|
||||||
import { logger, type ITableColumn } from '../logger.ts';
|
import { type ITableColumn, logger } from '../logger.ts';
|
||||||
import { theme } from '../colors.ts';
|
import { theme } from '../colors.ts';
|
||||||
import * as helpers from '../helpers/index.ts';
|
import * as helpers from '../helpers/index.ts';
|
||||||
import type { IGroupConfig, IUpsConfig, INupstConfig } from '../daemon.ts';
|
import type { IGroupConfig, INupstConfig, IUpsConfig } from '../daemon.ts';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class for handling group-related CLI commands
|
* Class for handling group-related CLI commands
|
||||||
@@ -29,10 +28,15 @@ export class GroupHandler {
|
|||||||
try {
|
try {
|
||||||
await this.nupst.getDaemon().loadConfig();
|
await this.nupst.getDaemon().loadConfig();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.logBox('Configuration Error', [
|
logger.logBox(
|
||||||
'No configuration found.',
|
'Configuration Error',
|
||||||
"Please run 'nupst ups add' first to create a configuration.",
|
[
|
||||||
], 50, 'error');
|
'No configuration found.',
|
||||||
|
"Please run 'nupst ups add' first to create a configuration.",
|
||||||
|
],
|
||||||
|
50,
|
||||||
|
'error',
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,21 +45,35 @@ 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)) {
|
||||||
logger.logBox('UPS Groups', [
|
logger.logBox(
|
||||||
'No groups configured.',
|
'UPS Groups',
|
||||||
'',
|
[
|
||||||
`${theme.dim('Run')} ${theme.command('nupst group add')} ${theme.dim('to add a group')}`,
|
'No groups configured.',
|
||||||
], 50, 'info');
|
'',
|
||||||
|
`${theme.dim('Run')} ${theme.command('nupst group add')} ${
|
||||||
|
theme.dim('to add a group')
|
||||||
|
}`,
|
||||||
|
],
|
||||||
|
50,
|
||||||
|
'info',
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Display group list with modern table
|
// Display group list with modern table
|
||||||
if (config.groups.length === 0) {
|
if (config.groups.length === 0) {
|
||||||
logger.logBox('UPS Groups', [
|
logger.logBox(
|
||||||
'No UPS groups configured.',
|
'UPS Groups',
|
||||||
'',
|
[
|
||||||
`${theme.dim('Run')} ${theme.command('nupst group add')} ${theme.dim('to add a group')}`,
|
'No UPS groups configured.',
|
||||||
], 60, 'info');
|
'',
|
||||||
|
`${theme.dim('Run')} ${theme.command('nupst group add')} ${
|
||||||
|
theme.dim('to add a group')
|
||||||
|
}`,
|
||||||
|
],
|
||||||
|
60,
|
||||||
|
'info',
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ export class ServiceHandler {
|
|||||||
/**
|
/**
|
||||||
* Update NUPST from repository and refresh systemd service
|
* Update NUPST from repository and refresh systemd service
|
||||||
*/
|
*/
|
||||||
public async update(): Promise<void> {
|
public update(): void {
|
||||||
try {
|
try {
|
||||||
// Check if running as root
|
// Check if running as root
|
||||||
this.checkRootAccess(
|
this.checkRootAccess(
|
||||||
@@ -147,8 +147,12 @@ export class ServiceHandler {
|
|||||||
const latestVersion = release.tag_name; // e.g., "v4.0.7"
|
const latestVersion = release.tag_name; // e.g., "v4.0.7"
|
||||||
|
|
||||||
// Normalize versions for comparison (ensure both have "v" prefix)
|
// Normalize versions for comparison (ensure both have "v" prefix)
|
||||||
const normalizedCurrent = currentVersion.startsWith('v') ? currentVersion : `v${currentVersion}`;
|
const normalizedCurrent = currentVersion.startsWith('v')
|
||||||
const normalizedLatest = latestVersion.startsWith('v') ? latestVersion : `v${latestVersion}`;
|
? currentVersion
|
||||||
|
: `v${currentVersion}`;
|
||||||
|
const normalizedLatest = latestVersion.startsWith('v')
|
||||||
|
? latestVersion
|
||||||
|
: `v${latestVersion}`;
|
||||||
|
|
||||||
logger.dim(`Current version: ${normalizedCurrent}`);
|
logger.dim(`Current version: ${normalizedCurrent}`);
|
||||||
logger.dim(`Latest version: ${normalizedLatest}`);
|
logger.dim(`Latest version: ${normalizedLatest}`);
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
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, type ITableColumn } from '../logger.ts';
|
import { type ITableColumn, logger } from '../logger.ts';
|
||||||
import { theme } from '../colors.ts';
|
import { theme } from '../colors.ts';
|
||||||
import * as helpers from '../helpers/index.ts';
|
import * as helpers from '../helpers/index.ts';
|
||||||
import type { ISnmpConfig, TUpsModel, IUpsStatus as ISnmpUpsStatus } from '../snmp/types.ts';
|
import type { ISnmpConfig, IUpsStatus as ISnmpUpsStatus, TUpsModel } from '../snmp/types.ts';
|
||||||
import type { INupstConfig, IUpsConfig, IUpsStatus } from '../daemon.ts';
|
import type { INupstConfig, IUpsConfig } from '../daemon.ts';
|
||||||
import type { IActionConfig } from '../actions/base-action.ts';
|
import type { IActionConfig } from '../actions/base-action.ts';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -66,10 +66,10 @@ export class UpsHandler {
|
|||||||
checkInterval: config.checkInterval,
|
checkInterval: config.checkInterval,
|
||||||
upsDevices: [{
|
upsDevices: [{
|
||||||
id: 'default',
|
id: 'default',
|
||||||
name: 'Default UPS',
|
name: 'Default UPS',
|
||||||
snmp: config.snmp,
|
snmp: config.snmp,
|
||||||
groups: [],
|
groups: [],
|
||||||
actions: [],
|
actions: [],
|
||||||
}],
|
}],
|
||||||
groups: [],
|
groups: [],
|
||||||
};
|
};
|
||||||
@@ -123,7 +123,7 @@ export class UpsHandler {
|
|||||||
await groupHandler.assignUpsToGroups(newUps, config.groups, prompt);
|
await groupHandler.assignUpsToGroups(newUps, config.groups, prompt);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gather action settings
|
// Gather action settings
|
||||||
await this.gatherActionSettings(newUps.actions, prompt);
|
await this.gatherActionSettings(newUps.actions, prompt);
|
||||||
|
|
||||||
// Add the new UPS to the config
|
// Add the new UPS to the config
|
||||||
@@ -343,10 +343,15 @@ export class UpsHandler {
|
|||||||
try {
|
try {
|
||||||
await this.nupst.getDaemon().loadConfig();
|
await this.nupst.getDaemon().loadConfig();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.logBox('Configuration Error', [
|
logger.logBox(
|
||||||
'No configuration found.',
|
'Configuration Error',
|
||||||
"Please run 'nupst ups add' first to create a configuration.",
|
[
|
||||||
], 50, 'error');
|
'No configuration found.',
|
||||||
|
"Please run 'nupst ups add' first to create a configuration.",
|
||||||
|
],
|
||||||
|
50,
|
||||||
|
'error',
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,31 +361,38 @@ 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
|
||||||
logger.logBox('UPS Devices', [
|
logger.logBox(
|
||||||
'Legacy single-UPS configuration detected.',
|
'UPS Devices',
|
||||||
'',
|
[
|
||||||
...(!config.snmp
|
'Legacy single-UPS configuration detected.',
|
||||||
? ['Error: Configuration missing SNMP settings']
|
'',
|
||||||
: [
|
...(!config.snmp ? ['Error: Configuration missing SNMP settings'] : [
|
||||||
'Default UPS:',
|
'Default UPS:',
|
||||||
` Host: ${config.snmp.host}:${config.snmp.port}`,
|
` Host: ${config.snmp.host}:${config.snmp.port}`,
|
||||||
` Model: ${config.snmp.upsModel || 'cyberpower'}`,
|
` Model: ${config.snmp.upsModel || 'cyberpower'}`,
|
||||||
'',
|
'',
|
||||||
'Use "nupst ups add" to add more UPS devices and migrate',
|
'Use "nupst ups add" to add more UPS devices and migrate',
|
||||||
'to the multi-UPS configuration format.',
|
'to the multi-UPS configuration format.',
|
||||||
]
|
]),
|
||||||
),
|
],
|
||||||
], 60, 'warning');
|
60,
|
||||||
|
'warning',
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Display UPS list with modern table
|
// Display UPS list with modern table
|
||||||
if (config.upsDevices.length === 0) {
|
if (config.upsDevices.length === 0) {
|
||||||
logger.logBox('UPS Devices', [
|
logger.logBox(
|
||||||
'No UPS devices configured.',
|
'UPS Devices',
|
||||||
'',
|
[
|
||||||
`${theme.dim('Run')} ${theme.command('nupst ups add')} ${theme.dim('to add a device')}`,
|
'No UPS devices configured.',
|
||||||
], 60, 'info');
|
'',
|
||||||
|
`${theme.dim('Run')} ${theme.command('nupst ups add')} ${theme.dim('to add a device')}`,
|
||||||
|
],
|
||||||
|
60,
|
||||||
|
'info',
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -467,18 +479,27 @@ export class UpsHandler {
|
|||||||
* @param config Current configuration or individual UPS configuration
|
* @param config Current configuration or individual UPS configuration
|
||||||
*/
|
*/
|
||||||
private displayTestConfig(config: IUpsConfig | INupstConfig): void {
|
private displayTestConfig(config: IUpsConfig | INupstConfig): void {
|
||||||
// Check if this is a UPS device or full configuration
|
// Type guard: IUpsConfig has 'id' and 'name' at root level, INupstConfig doesn't
|
||||||
const isUpsConfig = config.snmp;
|
const isUpsConfig = 'id' in config && 'name' in config;
|
||||||
const snmpConfig = isUpsConfig ? config.snmp : config.snmp || {};
|
|
||||||
const checkInterval = config.checkInterval || 30000;
|
|
||||||
|
|
||||||
// Get UPS name and ID if available
|
// Get SNMP config and other values based on config type
|
||||||
const upsName = config.name ? config.name : 'Default UPS';
|
const snmpConfig: ISnmpConfig | undefined = isUpsConfig
|
||||||
const upsId = config.id ? config.id : 'default';
|
? (config as IUpsConfig).snmp
|
||||||
|
: (config as INupstConfig).snmp;
|
||||||
|
const checkInterval = isUpsConfig ? 30000 : (config as INupstConfig).checkInterval || 30000;
|
||||||
|
const upsName = isUpsConfig ? (config as IUpsConfig).name : 'Default UPS';
|
||||||
|
const upsId = isUpsConfig ? (config as IUpsConfig).id : 'default';
|
||||||
|
|
||||||
const boxWidth = 45;
|
const boxWidth = 45;
|
||||||
logger.logBoxTitle(`Testing Configuration: ${upsName}`, boxWidth);
|
logger.logBoxTitle(`Testing Configuration: ${upsName}`, boxWidth);
|
||||||
logger.logBoxLine(`UPS ID: ${upsId}`);
|
logger.logBoxLine(`UPS ID: ${upsId}`);
|
||||||
|
|
||||||
|
if (!snmpConfig) {
|
||||||
|
logger.logBoxLine('SNMP Settings: Not configured');
|
||||||
|
logger.logBoxEnd();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
logger.logBoxLine('SNMP Settings:');
|
logger.logBoxLine('SNMP Settings:');
|
||||||
logger.logBoxLine(` Host: ${snmpConfig.host}`);
|
logger.logBoxLine(` Host: ${snmpConfig.host}`);
|
||||||
logger.logBoxLine(` Port: ${snmpConfig.port}`);
|
logger.logBoxLine(` Port: ${snmpConfig.port}`);
|
||||||
@@ -514,9 +535,10 @@ export class UpsHandler {
|
|||||||
logger.logBoxLine(` Battery Runtime: ${snmpConfig.customOIDs.BATTERY_RUNTIME || 'Not set'}`);
|
logger.logBoxLine(` Battery Runtime: ${snmpConfig.customOIDs.BATTERY_RUNTIME || 'Not set'}`);
|
||||||
}
|
}
|
||||||
// Show group assignments if this is a UPS config
|
// Show group assignments if this is a UPS config
|
||||||
if (config.groups && Array.isArray(config.groups)) {
|
if (isUpsConfig) {
|
||||||
|
const groups = (config as IUpsConfig).groups;
|
||||||
logger.logBoxLine(
|
logger.logBoxLine(
|
||||||
`Group Assignments: ${config.groups.length === 0 ? 'None' : config.groups.join(', ')}`,
|
`Group Assignments: ${groups.length === 0 ? 'None' : groups.join(', ')}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -529,15 +551,23 @@ export class UpsHandler {
|
|||||||
* @param config Current UPS configuration or legacy config
|
* @param config Current UPS configuration or legacy config
|
||||||
*/
|
*/
|
||||||
private async testConnection(config: IUpsConfig | INupstConfig): Promise<void> {
|
private async testConnection(config: IUpsConfig | INupstConfig): Promise<void> {
|
||||||
const upsId = config.id || 'default';
|
// Type guard: IUpsConfig has 'id' and 'name' at root level
|
||||||
const upsName = config.name || 'Default UPS';
|
const isUpsConfig = 'id' in config && 'name' in config;
|
||||||
|
const upsId = isUpsConfig ? (config as IUpsConfig).id : 'default';
|
||||||
|
const upsName = isUpsConfig ? (config as IUpsConfig).name : 'Default UPS';
|
||||||
logger.log(`\nTesting connection to UPS: ${upsName} (${upsId})...`);
|
logger.log(`\nTesting connection to UPS: ${upsName} (${upsId})...`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Create a test config with a short timeout
|
// Get SNMP config based on config type
|
||||||
const snmpConfig = config.snmp ? config.snmp : config.snmp;
|
const snmpConfig: ISnmpConfig | undefined = isUpsConfig
|
||||||
|
? (config as IUpsConfig).snmp
|
||||||
|
: (config as INupstConfig).snmp;
|
||||||
|
|
||||||
const testConfig = {
|
if (!snmpConfig) {
|
||||||
|
throw new Error('SNMP configuration not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const testConfig: ISnmpConfig = {
|
||||||
...snmpConfig,
|
...snmpConfig,
|
||||||
timeout: Math.min(snmpConfig.timeout, 10000), // Use at most 10 seconds for testing
|
timeout: Math.min(snmpConfig.timeout, 10000), // Use at most 10 seconds for testing
|
||||||
};
|
};
|
||||||
@@ -551,8 +581,6 @@ export class UpsHandler {
|
|||||||
logger.logBoxLine(` Battery Capacity: ${status.batteryCapacity}%`);
|
logger.logBoxLine(` Battery Capacity: ${status.batteryCapacity}%`);
|
||||||
logger.logBoxLine(` Runtime Remaining: ${status.batteryRuntime} minutes`);
|
logger.logBoxLine(` Runtime Remaining: ${status.batteryRuntime} minutes`);
|
||||||
logger.logBoxEnd();
|
logger.logBoxEnd();
|
||||||
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorBoxWidth = 45;
|
const errorBoxWidth = 45;
|
||||||
logger.logBoxTitle(`Connection Failed: ${upsName}`, errorBoxWidth);
|
logger.logBoxTitle(`Connection Failed: ${upsName}`, errorBoxWidth);
|
||||||
@@ -675,17 +703,17 @@ export class UpsHandler {
|
|||||||
if (secLevel === 1) {
|
if (secLevel === 1) {
|
||||||
snmpConfig.securityLevel = 'noAuthNoPriv';
|
snmpConfig.securityLevel = 'noAuthNoPriv';
|
||||||
// No auth, no priv - clear out authentication and privacy settings
|
// No auth, no priv - clear out authentication and privacy settings
|
||||||
snmpConfig.authProtocol = '';
|
snmpConfig.authProtocol = undefined;
|
||||||
snmpConfig.authKey = '';
|
snmpConfig.authKey = undefined;
|
||||||
snmpConfig.privProtocol = '';
|
snmpConfig.privProtocol = undefined;
|
||||||
snmpConfig.privKey = '';
|
snmpConfig.privKey = undefined;
|
||||||
// Set appropriate timeout for security level
|
// Set appropriate timeout for security level
|
||||||
snmpConfig.timeout = 5000; // 5 seconds for basic security
|
snmpConfig.timeout = 5000; // 5 seconds for basic security
|
||||||
} else if (secLevel === 2) {
|
} else if (secLevel === 2) {
|
||||||
snmpConfig.securityLevel = 'authNoPriv';
|
snmpConfig.securityLevel = 'authNoPriv';
|
||||||
// Auth, no priv - clear out privacy settings
|
// Auth, no priv - clear out privacy settings
|
||||||
snmpConfig.privProtocol = '';
|
snmpConfig.privProtocol = undefined;
|
||||||
snmpConfig.privKey = '';
|
snmpConfig.privKey = undefined;
|
||||||
// Set appropriate timeout for security level
|
// Set appropriate timeout for security level
|
||||||
snmpConfig.timeout = 10000; // 10 seconds for authentication
|
snmpConfig.timeout = 10000; // 10 seconds for authentication
|
||||||
} else {
|
} else {
|
||||||
@@ -825,16 +853,21 @@ export class UpsHandler {
|
|||||||
logger.info('Enter custom OIDs for your UPS:');
|
logger.info('Enter custom OIDs for your UPS:');
|
||||||
logger.dim('(Leave blank to use standard RFC 1628 OIDs as fallback)');
|
logger.dim('(Leave blank to use standard RFC 1628 OIDs as fallback)');
|
||||||
|
|
||||||
// Custom OIDs
|
// Custom OIDs - prompt for essential OIDs
|
||||||
const powerStatusOID = await prompt('Power Status OID: ');
|
const powerStatusOID = await prompt('Power Status OID: ');
|
||||||
const batteryCapacityOID = await prompt('Battery Capacity OID: ');
|
const batteryCapacityOID = await prompt('Battery Capacity OID: ');
|
||||||
const batteryRuntimeOID = await prompt('Battery Runtime OID: ');
|
const batteryRuntimeOID = await prompt('Battery Runtime OID: ');
|
||||||
|
|
||||||
// Create custom OIDs object
|
// Create custom OIDs object with all required fields
|
||||||
|
// Empty strings will use RFC 1628 fallback for non-essential OIDs
|
||||||
snmpConfig.customOIDs = {
|
snmpConfig.customOIDs = {
|
||||||
POWER_STATUS: powerStatusOID.trim(),
|
POWER_STATUS: powerStatusOID.trim(),
|
||||||
BATTERY_CAPACITY: batteryCapacityOID.trim(),
|
BATTERY_CAPACITY: batteryCapacityOID.trim(),
|
||||||
BATTERY_RUNTIME: batteryRuntimeOID.trim(),
|
BATTERY_RUNTIME: batteryRuntimeOID.trim(),
|
||||||
|
OUTPUT_LOAD: '',
|
||||||
|
OUTPUT_POWER: '',
|
||||||
|
OUTPUT_VOLTAGE: '',
|
||||||
|
OUTPUT_CURRENT: '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -936,7 +969,7 @@ export class UpsHandler {
|
|||||||
logger.dim(' 4) Any change (every ~30s check)');
|
logger.dim(' 4) Any change (every ~30s check)');
|
||||||
const triggerInput = await prompt('Select trigger mode [1]: ');
|
const triggerInput = await prompt('Select trigger mode [1]: ');
|
||||||
const triggerValue = parseInt(triggerInput, 10) || 1;
|
const triggerValue = parseInt(triggerInput, 10) || 1;
|
||||||
|
|
||||||
switch (triggerValue) {
|
switch (triggerValue) {
|
||||||
case 2:
|
case 2:
|
||||||
action.triggerMode = 'onlyPowerChanges';
|
action.triggerMode = 'onlyPowerChanges';
|
||||||
@@ -952,11 +985,16 @@ export class UpsHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Configure thresholds if needed for onlyThresholds or powerChangesAndThresholds modes
|
// Configure thresholds if needed for onlyThresholds or powerChangesAndThresholds modes
|
||||||
if (action.triggerMode === 'onlyThresholds' || action.triggerMode === 'powerChangesAndThresholds') {
|
if (
|
||||||
|
action.triggerMode === 'onlyThresholds' ||
|
||||||
|
action.triggerMode === 'powerChangesAndThresholds'
|
||||||
|
) {
|
||||||
logger.log('');
|
logger.log('');
|
||||||
logger.info('Action Thresholds:');
|
logger.info('Action Thresholds:');
|
||||||
logger.dim('Action will trigger when battery or runtime falls below these values (while on battery)');
|
logger.dim(
|
||||||
|
'Action will trigger when battery or runtime falls below these values (while on battery)',
|
||||||
|
);
|
||||||
|
|
||||||
const batteryInput = await prompt('Battery threshold percentage [60]: ');
|
const batteryInput = await prompt('Battery threshold percentage [60]: ');
|
||||||
const battery = parseInt(batteryInput, 10);
|
const battery = parseInt(batteryInput, 10);
|
||||||
const batteryThreshold = (batteryInput.trim() && !isNaN(battery)) ? battery : 60;
|
const batteryThreshold = (batteryInput.trim() && !isNaN(battery)) ? battery : 60;
|
||||||
@@ -972,7 +1010,11 @@ export class UpsHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
actions.push(action as IActionConfig);
|
actions.push(action as IActionConfig);
|
||||||
logger.success(`${action.type!.charAt(0).toUpperCase() + action.type!.slice(1)} action added (mode: ${action.triggerMode || 'powerChangesAndThresholds'})`);
|
logger.success(
|
||||||
|
`${action.type!.charAt(0).toUpperCase() + action.type!.slice(1)} action added (mode: ${
|
||||||
|
action.triggerMode || 'powerChangesAndThresholds'
|
||||||
|
})`,
|
||||||
|
);
|
||||||
|
|
||||||
const more = await prompt('Add another action? (y/N): ');
|
const more = await prompt('Add another action? (y/N): ');
|
||||||
addMore = more.toLowerCase() === 'y';
|
addMore = more.toLowerCase() === 'y';
|
||||||
@@ -996,7 +1038,7 @@ export class UpsHandler {
|
|||||||
logger.logBoxLine(`SNMP Host: ${ups.snmp.host}:${ups.snmp.port}`);
|
logger.logBoxLine(`SNMP Host: ${ups.snmp.host}:${ups.snmp.port}`);
|
||||||
logger.logBoxLine(`SNMP Version: ${ups.snmp.version}`);
|
logger.logBoxLine(`SNMP Version: ${ups.snmp.version}`);
|
||||||
logger.logBoxLine(`UPS Model: ${ups.snmp.upsModel}`);
|
logger.logBoxLine(`UPS Model: ${ups.snmp.upsModel}`);
|
||||||
|
|
||||||
if (ups.groups && ups.groups.length > 0) {
|
if (ups.groups && ups.groups.length > 0) {
|
||||||
logger.logBoxLine(`Groups: ${ups.groups.join(', ')}`);
|
logger.logBoxLine(`Groups: ${ups.groups.join(', ')}`);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
74
ts/daemon.ts
74
ts/daemon.ts
@@ -5,13 +5,13 @@ 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, IUpsStatus as ISnmpUpsStatus } from './snmp/types.ts';
|
import type { ISnmpConfig, IUpsStatus as ISnmpUpsStatus } from './snmp/types.ts';
|
||||||
import { logger, type ITableColumn } from './logger.ts';
|
import { logger } from './logger.ts';
|
||||||
import { MigrationRunner } from './migrations/index.ts';
|
import { MigrationRunner } from './migrations/index.ts';
|
||||||
import { theme, symbols, getBatteryColor, getRuntimeColor, formatPowerStatus } from './colors.ts';
|
import { formatPowerStatus, getBatteryColor, getRuntimeColor, theme } from './colors.ts';
|
||||||
import type { IActionConfig } from './actions/base-action.ts';
|
import type { IActionConfig } from './actions/base-action.ts';
|
||||||
import { ActionManager, type IActionContext, type TPowerStatus } from './actions/index.ts';
|
import { ActionManager, type IActionContext, type TPowerStatus } from './actions/index.ts';
|
||||||
import { NupstHttpServer } from './http-server.ts';
|
import { NupstHttpServer } from './http-server.ts';
|
||||||
import { TIMING, THRESHOLDS, UI } from './constants.ts';
|
import { THRESHOLDS, TIMING, UI } from './constants.ts';
|
||||||
|
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
const execFileAsync = promisify(execFile);
|
const execFileAsync = promisify(execFile);
|
||||||
@@ -100,10 +100,10 @@ export interface IUpsStatus {
|
|||||||
powerStatus: 'online' | 'onBattery' | 'unknown';
|
powerStatus: 'online' | 'onBattery' | 'unknown';
|
||||||
batteryCapacity: number;
|
batteryCapacity: number;
|
||||||
batteryRuntime: number;
|
batteryRuntime: number;
|
||||||
outputLoad: number; // Load percentage (0-100%)
|
outputLoad: number; // Load percentage (0-100%)
|
||||||
outputPower: number; // Power in watts
|
outputPower: number; // Power in watts
|
||||||
outputVoltage: number; // Voltage in volts
|
outputVoltage: number; // Voltage in volts
|
||||||
outputCurrent: number; // Current in amps
|
outputCurrent: number; // Current in amps
|
||||||
lastStatusChange: number;
|
lastStatusChange: number;
|
||||||
lastCheckTime: number;
|
lastCheckTime: number;
|
||||||
}
|
}
|
||||||
@@ -155,7 +155,7 @@ export class NupstDaemon {
|
|||||||
],
|
],
|
||||||
groups: [],
|
groups: [],
|
||||||
checkInterval: TIMING.CHECK_INTERVAL_MS, // Check every 30 seconds
|
checkInterval: TIMING.CHECK_INTERVAL_MS, // Check every 30 seconds
|
||||||
}
|
};
|
||||||
|
|
||||||
private config: INupstConfig;
|
private config: INupstConfig;
|
||||||
private snmp: NupstSnmp;
|
private snmp: NupstSnmp;
|
||||||
@@ -249,7 +249,12 @@ export class NupstDaemon {
|
|||||||
* Helper method to log configuration errors consistently
|
* Helper method to log configuration errors consistently
|
||||||
*/
|
*/
|
||||||
private logConfigError(message: string): void {
|
private logConfigError(message: string): void {
|
||||||
logger.logBox('Configuration Error', [message, "Please run 'nupst setup' first to create a configuration."], 45, 'error');
|
logger.logBox(
|
||||||
|
'Configuration Error',
|
||||||
|
[message, "Please run 'nupst setup' first to create a configuration."],
|
||||||
|
45,
|
||||||
|
'error',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -311,11 +316,15 @@ export class NupstDaemon {
|
|||||||
this.config.httpServer.port,
|
this.config.httpServer.port,
|
||||||
this.config.httpServer.path,
|
this.config.httpServer.path,
|
||||||
this.config.httpServer.authToken,
|
this.config.httpServer.authToken,
|
||||||
() => this.upsStatus
|
() => this.upsStatus,
|
||||||
);
|
);
|
||||||
this.httpServer.start();
|
this.httpServer.start();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Failed to start HTTP server: ${error instanceof Error ? error.message : String(error)}`);
|
logger.error(
|
||||||
|
`Failed to start HTTP server: ${
|
||||||
|
error instanceof Error ? error.message : String(error)
|
||||||
|
}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -364,7 +373,6 @@ export class NupstDaemon {
|
|||||||
* Log the loaded configuration settings
|
* Log the loaded configuration settings
|
||||||
*/
|
*/
|
||||||
private logConfigLoaded(): void {
|
private logConfigLoaded(): void {
|
||||||
|
|
||||||
logger.log('');
|
logger.log('');
|
||||||
logger.logBoxTitle('Configuration Loaded', 70, 'success');
|
logger.logBoxTitle('Configuration Loaded', 70, 'success');
|
||||||
logger.logBoxLine(`Check Interval: ${this.config.checkInterval / 1000} seconds`);
|
logger.logBoxLine(`Check Interval: ${this.config.checkInterval / 1000} seconds`);
|
||||||
@@ -374,8 +382,10 @@ export class NupstDaemon {
|
|||||||
// Display UPS devices in a table
|
// Display UPS devices in a table
|
||||||
if (this.config.upsDevices && this.config.upsDevices.length > 0) {
|
if (this.config.upsDevices && this.config.upsDevices.length > 0) {
|
||||||
logger.info(`UPS Devices (${this.config.upsDevices.length}):`);
|
logger.info(`UPS Devices (${this.config.upsDevices.length}):`);
|
||||||
|
|
||||||
const upsColumns: Array<{ header: string; key: string; align?: 'left' | 'right'; color?: (val: string) => string }> = [
|
const upsColumns: Array<
|
||||||
|
{ header: string; key: string; align?: 'left' | 'right'; color?: (val: string) => string }
|
||||||
|
> = [
|
||||||
{ header: 'Name', key: 'name', align: 'left', color: theme.highlight },
|
{ header: 'Name', key: 'name', align: 'left', color: theme.highlight },
|
||||||
{ header: 'ID', key: 'id', align: 'left', color: theme.dim },
|
{ header: 'ID', key: 'id', align: 'left', color: theme.dim },
|
||||||
{ header: 'Host:Port', key: 'host', align: 'left', color: theme.info },
|
{ header: 'Host:Port', key: 'host', align: 'left', color: theme.info },
|
||||||
@@ -399,8 +409,10 @@ export class NupstDaemon {
|
|||||||
// Display groups in a table
|
// Display groups in a table
|
||||||
if (this.config.groups && this.config.groups.length > 0) {
|
if (this.config.groups && this.config.groups.length > 0) {
|
||||||
logger.info(`Groups (${this.config.groups.length}):`);
|
logger.info(`Groups (${this.config.groups.length}):`);
|
||||||
|
|
||||||
const groupColumns: Array<{ header: string; key: string; align?: 'left' | 'right'; color?: (val: string) => string }> = [
|
const groupColumns: Array<
|
||||||
|
{ header: string; key: string; align?: 'left' | 'right'; color?: (val: string) => string }
|
||||||
|
> = [
|
||||||
{ header: 'Name', key: 'name', align: 'left', color: theme.highlight },
|
{ header: 'Name', key: 'name', align: 'left', color: theme.highlight },
|
||||||
{ header: 'ID', key: 'id', align: 'left', color: theme.dim },
|
{ header: 'ID', key: 'id', align: 'left', color: theme.dim },
|
||||||
{ header: 'Mode', key: 'mode', align: 'left', color: theme.info },
|
{ header: 'Mode', key: 'mode', align: 'left', color: theme.info },
|
||||||
@@ -538,7 +550,7 @@ export class NupstDaemon {
|
|||||||
// Only check when on battery power
|
// Only check when on battery power
|
||||||
if (status.powerStatus === 'onBattery' && ups.actions && ups.actions.length > 0) {
|
if (status.powerStatus === 'onBattery' && ups.actions && ups.actions.length > 0) {
|
||||||
let anyThresholdExceeded = false;
|
let anyThresholdExceeded = false;
|
||||||
|
|
||||||
for (const actionConfig of ups.actions) {
|
for (const actionConfig of ups.actions) {
|
||||||
if (actionConfig.thresholds) {
|
if (actionConfig.thresholds) {
|
||||||
if (
|
if (
|
||||||
@@ -575,7 +587,7 @@ export class NupstDaemon {
|
|||||||
*/
|
*/
|
||||||
private logAllUpsStatus(): void {
|
private logAllUpsStatus(): void {
|
||||||
const timestamp = new Date().toISOString();
|
const timestamp = new Date().toISOString();
|
||||||
|
|
||||||
logger.log('');
|
logger.log('');
|
||||||
logger.logBoxTitle('Periodic Status Update', 70, 'info');
|
logger.logBoxTitle('Periodic Status Update', 70, 'info');
|
||||||
logger.logBoxLine(`Timestamp: ${timestamp}`);
|
logger.logBoxLine(`Timestamp: ${timestamp}`);
|
||||||
@@ -583,7 +595,9 @@ export class NupstDaemon {
|
|||||||
logger.log('');
|
logger.log('');
|
||||||
|
|
||||||
// Build table data
|
// Build table data
|
||||||
const columns: Array<{ header: string; key: string; align?: 'left' | 'right'; color?: (val: string) => string }> = [
|
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: 'UPS Name', key: 'name', align: 'left', color: theme.highlight },
|
||||||
{ header: 'ID', key: 'id', align: 'left', color: theme.dim },
|
{ header: 'ID', key: 'id', align: 'left', color: theme.dim },
|
||||||
{ header: 'Power Status', key: 'powerStatus', align: 'left' },
|
{ header: 'Power Status', key: 'powerStatus', align: 'left' },
|
||||||
@@ -595,7 +609,7 @@ export class NupstDaemon {
|
|||||||
for (const [id, status] of this.upsStatus.entries()) {
|
for (const [id, status] of this.upsStatus.entries()) {
|
||||||
const batteryColor = getBatteryColor(status.batteryCapacity);
|
const batteryColor = getBatteryColor(status.batteryCapacity);
|
||||||
const runtimeColor = getRuntimeColor(status.batteryRuntime);
|
const runtimeColor = getRuntimeColor(status.batteryRuntime);
|
||||||
|
|
||||||
rows.push({
|
rows.push({
|
||||||
name: status.name,
|
name: status.name,
|
||||||
id: id,
|
id: id,
|
||||||
@@ -609,10 +623,6 @@ export class NupstDaemon {
|
|||||||
logger.log('');
|
logger.log('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build action context from UPS state
|
* Build action context from UPS state
|
||||||
* @param ups UPS configuration
|
* @param ups UPS configuration
|
||||||
@@ -796,7 +806,9 @@ export class NupstDaemon {
|
|||||||
|
|
||||||
logger.log('');
|
logger.log('');
|
||||||
logger.logBoxTitle('Shutdown Monitoring Active', UI.WIDE_BOX_WIDTH, 'warning');
|
logger.logBoxTitle('Shutdown Monitoring Active', UI.WIDE_BOX_WIDTH, 'warning');
|
||||||
logger.logBoxLine(`Emergency threshold: ${THRESHOLDS.EMERGENCY_RUNTIME_MINUTES} minutes runtime`);
|
logger.logBoxLine(
|
||||||
|
`Emergency threshold: ${THRESHOLDS.EMERGENCY_RUNTIME_MINUTES} minutes runtime`,
|
||||||
|
);
|
||||||
logger.logBoxLine(`Check interval: ${TIMING.SHUTDOWN_CHECK_INTERVAL_MS / 1000} seconds`);
|
logger.logBoxLine(`Check interval: ${TIMING.SHUTDOWN_CHECK_INTERVAL_MS / 1000} seconds`);
|
||||||
logger.logBoxLine(`Max monitoring time: ${TIMING.MAX_SHUTDOWN_MONITORING_MS / 1000} seconds`);
|
logger.logBoxLine(`Max monitoring time: ${TIMING.MAX_SHUTDOWN_MONITORING_MS / 1000} seconds`);
|
||||||
logger.logBoxEnd();
|
logger.logBoxEnd();
|
||||||
@@ -808,7 +820,9 @@ export class NupstDaemon {
|
|||||||
logger.info('Checking UPS status during shutdown...');
|
logger.info('Checking UPS status during shutdown...');
|
||||||
|
|
||||||
// Build table for 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 }> = [
|
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: 'UPS Name', key: 'name', align: 'left', color: theme.highlight },
|
||||||
{ header: 'Battery', key: 'battery', align: 'right' },
|
{ header: 'Battery', key: 'battery', align: 'right' },
|
||||||
{ header: 'Runtime', key: 'runtime', align: 'right' },
|
{ header: 'Runtime', key: 'runtime', align: 'right' },
|
||||||
@@ -828,7 +842,7 @@ export class NupstDaemon {
|
|||||||
const runtimeColor = getRuntimeColor(status.batteryRuntime);
|
const runtimeColor = getRuntimeColor(status.batteryRuntime);
|
||||||
|
|
||||||
const isCritical = status.batteryRuntime < THRESHOLDS.EMERGENCY_RUNTIME_MINUTES;
|
const isCritical = status.batteryRuntime < THRESHOLDS.EMERGENCY_RUNTIME_MINUTES;
|
||||||
|
|
||||||
rows.push({
|
rows.push({
|
||||||
name: ups.name,
|
name: ups.name,
|
||||||
battery: batteryColor(status.batteryCapacity + '%'),
|
battery: batteryColor(status.batteryCapacity + '%'),
|
||||||
@@ -848,7 +862,7 @@ export class NupstDaemon {
|
|||||||
runtime: theme.error('N/A'),
|
runtime: theme.error('N/A'),
|
||||||
status: theme.error('ERROR'),
|
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)
|
||||||
@@ -991,7 +1005,9 @@ export class NupstDaemon {
|
|||||||
let lastConfigCheck = Date.now();
|
let lastConfigCheck = Date.now();
|
||||||
|
|
||||||
logger.log('Entering idle monitoring mode...');
|
logger.log('Entering idle monitoring mode...');
|
||||||
logger.log(`Daemon will check for config changes every ${TIMING.IDLE_CHECK_INTERVAL_MS / 1000} seconds`);
|
logger.log(
|
||||||
|
`Daemon will check for config changes every ${TIMING.IDLE_CHECK_INTERVAL_MS / 1000} seconds`,
|
||||||
|
);
|
||||||
|
|
||||||
// Start file watcher for hot-reload
|
// Start file watcher for hot-reload
|
||||||
this.watchConfigFile();
|
this.watchConfigFile();
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export class NupstHttpServer {
|
|||||||
port: number,
|
port: number,
|
||||||
path: string,
|
path: string,
|
||||||
authToken: string,
|
authToken: string,
|
||||||
getUpsStatus: () => Map<string, IUpsStatus>
|
getUpsStatus: () => Map<string, IUpsStatus>,
|
||||||
) {
|
) {
|
||||||
this.port = port;
|
this.port = port;
|
||||||
this.path = path;
|
this.path = path;
|
||||||
@@ -70,7 +70,7 @@ export class NupstHttpServer {
|
|||||||
if (!this.isAuthenticated(req)) {
|
if (!this.isAuthenticated(req)) {
|
||||||
res.writeHead(401, {
|
res.writeHead(401, {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'WWW-Authenticate': 'Bearer'
|
'WWW-Authenticate': 'Bearer',
|
||||||
});
|
});
|
||||||
res.end(JSON.stringify({ error: 'Unauthorized' }));
|
res.end(JSON.stringify({ error: 'Unauthorized' }));
|
||||||
return;
|
return;
|
||||||
@@ -82,7 +82,7 @@ export class NupstHttpServer {
|
|||||||
|
|
||||||
res.writeHead(200, {
|
res.writeHead(200, {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Cache-Control': 'no-cache'
|
'Cache-Control': 'no-cache',
|
||||||
});
|
});
|
||||||
res.end(JSON.stringify(statusArray, null, 2));
|
res.end(JSON.stringify(statusArray, null, 2));
|
||||||
} else {
|
} else {
|
||||||
@@ -95,7 +95,7 @@ export class NupstHttpServer {
|
|||||||
logger.success(`HTTP server started on port ${this.port} at ${this.path}`);
|
logger.success(`HTTP server started on port ${this.port} at ${this.path}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.server.on('error', (error: any) => {
|
this.server.on('error', (error: Error) => {
|
||||||
logger.error(`HTTP server error: ${error.message}`);
|
logger.error(`HTTP server error: ${error.message}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { theme, symbols } from './colors.ts';
|
import { symbols, theme } from './colors.ts';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Table column alignment options
|
* Table column alignment options
|
||||||
@@ -230,7 +230,8 @@ export class Logger {
|
|||||||
* Strip ANSI color codes from string for accurate length calculation
|
* Strip ANSI color codes from string for accurate length calculation
|
||||||
*/
|
*/
|
||||||
private stripAnsi(text: string): string {
|
private stripAnsi(text: string): string {
|
||||||
// Remove ANSI escape codes
|
// Remove ANSI escape codes (intentional control character regex)
|
||||||
|
// deno-lint-ignore no-control-regex
|
||||||
return text.replace(/\x1b\[[0-9;]*m/g, '');
|
return text.replace(/\x1b\[[0-9;]*m/g, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,9 @@ export abstract class BaseMigration {
|
|||||||
* @param config - Raw configuration object to check (unknown schema for migrations)
|
* @param config - Raw configuration object to check (unknown schema for migrations)
|
||||||
* @returns True if migration should run, false otherwise
|
* @returns True if migration should run, false otherwise
|
||||||
*/
|
*/
|
||||||
abstract shouldRun(config: Record<string, unknown>): Promise<boolean>;
|
abstract shouldRun(
|
||||||
|
config: Record<string, unknown>,
|
||||||
|
): boolean | Promise<boolean>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Perform the migration on the given config
|
* Perform the migration on the given config
|
||||||
@@ -39,7 +41,9 @@ export abstract class BaseMigration {
|
|||||||
* @param config - Raw configuration object to migrate (unknown schema for migrations)
|
* @param config - Raw configuration object to migrate (unknown schema for migrations)
|
||||||
* @returns Migrated configuration object
|
* @returns Migrated configuration object
|
||||||
*/
|
*/
|
||||||
abstract migrate(config: Record<string, unknown>): Promise<Record<string, unknown>>;
|
abstract migrate(
|
||||||
|
config: Record<string, unknown>,
|
||||||
|
): Record<string, unknown> | Promise<Record<string, unknown>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get human-readable name for this migration
|
* Get human-readable name for this migration
|
||||||
|
|||||||
@@ -23,12 +23,12 @@ export class MigrationV1ToV2 extends BaseMigration {
|
|||||||
readonly fromVersion = '1.x';
|
readonly fromVersion = '1.x';
|
||||||
readonly toVersion = '2.0';
|
readonly toVersion = '2.0';
|
||||||
|
|
||||||
async shouldRun(config: any): Promise<boolean> {
|
shouldRun(config: Record<string, unknown>): boolean {
|
||||||
// V1 format has snmp field directly at root, no upsDevices or upsList
|
// V1 format has snmp field directly at root, no upsDevices or upsList
|
||||||
return !!config.snmp && !config.upsDevices && !config.upsList;
|
return !!config.snmp && !config.upsDevices && !config.upsList;
|
||||||
}
|
}
|
||||||
|
|
||||||
async migrate(config: any): Promise<any> {
|
migrate(config: Record<string, unknown>): Record<string, unknown> {
|
||||||
logger.info(`${this.getName()}: Converting single SNMP config to multi-UPS format...`);
|
logger.info(`${this.getName()}: Converting single SNMP config to multi-UPS format...`);
|
||||||
|
|
||||||
const migrated = {
|
const migrated = {
|
||||||
|
|||||||
@@ -42,15 +42,16 @@ export class MigrationV3ToV4 extends BaseMigration {
|
|||||||
readonly fromVersion = '3.x';
|
readonly fromVersion = '3.x';
|
||||||
readonly toVersion = '4.0';
|
readonly toVersion = '4.0';
|
||||||
|
|
||||||
async shouldRun(config: any): Promise<boolean> {
|
shouldRun(config: Record<string, unknown>): boolean {
|
||||||
// V3 format has upsList OR has upsDevices with flat structure (host at top level)
|
// V3 format has upsList OR has upsDevices with flat structure (host at top level)
|
||||||
if (config.upsList && !config.upsDevices) {
|
if (config.upsList && !config.upsDevices) {
|
||||||
return true; // Classic v3 with upsList
|
return true; // Classic v3 with upsList
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if upsDevices exists but has flat structure (v3 format)
|
// Check if upsDevices exists but has flat structure (v3 format)
|
||||||
if (config.upsDevices && config.upsDevices.length > 0) {
|
const upsDevices = config.upsDevices as Array<Record<string, unknown>> | undefined;
|
||||||
const firstDevice = config.upsDevices[0];
|
if (upsDevices && upsDevices.length > 0) {
|
||||||
|
const firstDevice = upsDevices[0];
|
||||||
// V3 has host at top level, v4 has it nested in snmp object
|
// V3 has host at top level, v4 has it nested in snmp object
|
||||||
return !!firstDevice.host && !firstDevice.snmp;
|
return !!firstDevice.host && !firstDevice.snmp;
|
||||||
}
|
}
|
||||||
@@ -58,17 +59,17 @@ export class MigrationV3ToV4 extends BaseMigration {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async migrate(config: any): Promise<any> {
|
migrate(config: Record<string, unknown>): Record<string, unknown> {
|
||||||
logger.info(`${this.getName()}: Migrating v3 config to v4 format...`);
|
logger.info(`${this.getName()}: Migrating v3 config to v4 format...`);
|
||||||
logger.dim(` - Restructuring UPS devices (flat → nested snmp config)`);
|
logger.dim(` - Restructuring UPS devices (flat → nested snmp config)`);
|
||||||
|
|
||||||
// Get devices from either upsList or upsDevices (for partially migrated configs)
|
// Get devices from either upsList or upsDevices (for partially migrated configs)
|
||||||
const sourceDevices = config.upsList || config.upsDevices;
|
const sourceDevices = (config.upsList || config.upsDevices) as Array<Record<string, unknown>>;
|
||||||
|
|
||||||
// Transform each UPS device from v3 flat structure to v4 nested structure
|
// Transform each UPS device from v3 flat structure to v4 nested structure
|
||||||
const transformedDevices = sourceDevices.map((device: any) => {
|
const transformedDevices = sourceDevices.map((device: Record<string, unknown>) => {
|
||||||
// Build SNMP config object
|
// Build SNMP config object
|
||||||
const snmpConfig: any = {
|
const snmpConfig: Record<string, unknown> = {
|
||||||
host: device.host,
|
host: device.host,
|
||||||
port: device.port || 161,
|
port: device.port || 161,
|
||||||
version: typeof device.version === 'string' ? parseInt(device.version, 10) : device.version,
|
version: typeof device.version === 'string' ? parseInt(device.version, 10) : device.version,
|
||||||
@@ -112,7 +113,9 @@ export class MigrationV3ToV4 extends BaseMigration {
|
|||||||
checkInterval: config.checkInterval || 30000,
|
checkInterval: config.checkInterval || 30000,
|
||||||
};
|
};
|
||||||
|
|
||||||
logger.success(`${this.getName()}: Migration complete (${transformedDevices.length} devices transformed)`);
|
logger.success(
|
||||||
|
`${this.getName()}: Migration complete (${transformedDevices.length} devices transformed)`,
|
||||||
|
);
|
||||||
return migrated;
|
return migrated;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export class MigrationV4_0ToV4_1 extends BaseMigration {
|
|||||||
readonly fromVersion = '4.0';
|
readonly fromVersion = '4.0';
|
||||||
readonly toVersion = '4.1';
|
readonly toVersion = '4.1';
|
||||||
|
|
||||||
async shouldRun(config: Record<string, unknown>): Promise<boolean> {
|
shouldRun(config: Record<string, unknown>): boolean {
|
||||||
// Run if config is version 4.0
|
// Run if config is version 4.0
|
||||||
if (config.version === '4.0') {
|
if (config.version === '4.0') {
|
||||||
return true;
|
return true;
|
||||||
@@ -65,7 +65,7 @@ export class MigrationV4_0ToV4_1 extends BaseMigration {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async migrate(config: Record<string, unknown>): Promise<Record<string, unknown>> {
|
migrate(config: Record<string, unknown>): Record<string, unknown> {
|
||||||
logger.info(`${this.getName()}: Migrating v4.0 config to v4.1 format...`);
|
logger.info(`${this.getName()}: Migrating v4.0 config to v4.1 format...`);
|
||||||
logger.dim(` - Moving thresholds from UPS level to action level`);
|
logger.dim(` - Moving thresholds from UPS level to action level`);
|
||||||
logger.dim(` - Creating default shutdown actions from existing thresholds`);
|
logger.dim(` - Creating default shutdown actions from existing thresholds`);
|
||||||
@@ -81,7 +81,9 @@ export class MigrationV4_0ToV4_1 extends BaseMigration {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// If device has thresholds at UPS level, convert to shutdown action
|
// If device has thresholds at UPS level, convert to shutdown action
|
||||||
const deviceThresholds = device.thresholds as { battery: number; runtime: number } | undefined;
|
const deviceThresholds = device.thresholds as
|
||||||
|
| { battery: number; runtime: number }
|
||||||
|
| undefined;
|
||||||
if (deviceThresholds) {
|
if (deviceThresholds) {
|
||||||
migrated.actions = [
|
migrated.actions = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -171,8 +171,8 @@ export class Nupst implements INupstAccessor {
|
|||||||
const response = JSON.parse(data);
|
const response = JSON.parse(data);
|
||||||
if (response.tag_name) {
|
if (response.tag_name) {
|
||||||
// Strip 'v' prefix from tag name (e.g., "v5.1.7" -> "5.1.7")
|
// Strip 'v' prefix from tag name (e.g., "v5.1.7" -> "5.1.7")
|
||||||
const version = response.tag_name.startsWith('v')
|
const version = response.tag_name.startsWith('v')
|
||||||
? response.tag_name.substring(1)
|
? response.tag_name.substring(1)
|
||||||
: response.tag_name;
|
: response.tag_name;
|
||||||
resolve(version);
|
resolve(version);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -94,7 +94,8 @@ export class NupstSnmp {
|
|||||||
public snmpGet(
|
public snmpGet(
|
||||||
oid: string,
|
oid: string,
|
||||||
config = this.DEFAULT_CONFIG,
|
config = this.DEFAULT_CONFIG,
|
||||||
retryCount = 0,
|
_retryCount = 0,
|
||||||
|
// deno-lint-ignore no-explicit-any
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (this.debug) {
|
if (this.debug) {
|
||||||
@@ -105,6 +106,7 @@ export class NupstSnmp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create SNMP options based on configuration
|
// Create SNMP options based on configuration
|
||||||
|
// deno-lint-ignore no-explicit-any
|
||||||
const options: any = {
|
const options: any = {
|
||||||
port: config.port,
|
port: config.port,
|
||||||
retries: SNMP.RETRIES, // Number of retries
|
retries: SNMP.RETRIES, // Number of retries
|
||||||
@@ -132,6 +134,7 @@ export class NupstSnmp {
|
|||||||
const securityLevel = config.securityLevel || 'noAuthNoPriv';
|
const securityLevel = config.securityLevel || 'noAuthNoPriv';
|
||||||
|
|
||||||
// Create the user object with required structure for net-snmp
|
// Create the user object with required structure for net-snmp
|
||||||
|
// deno-lint-ignore no-explicit-any
|
||||||
const user: any = {
|
const user: any = {
|
||||||
name: config.username || '',
|
name: config.username || '',
|
||||||
};
|
};
|
||||||
@@ -197,7 +200,11 @@ export class NupstSnmp {
|
|||||||
const levelName = Object.keys(snmp.SecurityLevel).find((key) =>
|
const levelName = Object.keys(snmp.SecurityLevel).find((key) =>
|
||||||
snmp.SecurityLevel[key] === user.level
|
snmp.SecurityLevel[key] === user.level
|
||||||
);
|
);
|
||||||
logger.dim(`SNMPv3 user configuration: name=${user.name}, level=${levelName}, authProtocol=${user.authProtocol ? 'Set' : 'Not Set'}, privProtocol=${user.privProtocol ? 'Set' : 'Not Set'}`);
|
logger.dim(
|
||||||
|
`SNMPv3 user configuration: name=${user.name}, level=${levelName}, authProtocol=${
|
||||||
|
user.authProtocol ? 'Set' : 'Not Set'
|
||||||
|
}, privProtocol=${user.privProtocol ? 'Set' : 'Not Set'}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
session = snmp.createV3Session(config.host, user, options);
|
session = snmp.createV3Session(config.host, user, options);
|
||||||
@@ -210,7 +217,8 @@ export class NupstSnmp {
|
|||||||
const oids = [oid];
|
const oids = [oid];
|
||||||
|
|
||||||
// Send the GET request
|
// Send the GET request
|
||||||
session.get(oids, (error: any, varbinds: any[]) => {
|
// deno-lint-ignore no-explicit-any
|
||||||
|
session.get(oids, (error: Error | null, varbinds: any[]) => {
|
||||||
// Close the session to release resources
|
// Close the session to release resources
|
||||||
session.close();
|
session.close();
|
||||||
|
|
||||||
@@ -259,7 +267,9 @@ export class NupstSnmp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.debug) {
|
if (this.debug) {
|
||||||
logger.dim(`SNMP response: oid=${varbinds[0].oid}, type=${varbinds[0].type}, value=${value}`);
|
logger.dim(
|
||||||
|
`SNMP response: oid=${varbinds[0].oid}, type=${varbinds[0].type}, value=${value}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
resolve(value);
|
resolve(value);
|
||||||
@@ -422,6 +432,7 @@ export class NupstSnmp {
|
|||||||
oid: string,
|
oid: string,
|
||||||
description: string,
|
description: string,
|
||||||
config: ISnmpConfig,
|
config: ISnmpConfig,
|
||||||
|
// deno-lint-ignore no-explicit-any
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
if (oid === '') {
|
if (oid === '') {
|
||||||
if (this.debug) {
|
if (this.debug) {
|
||||||
@@ -476,6 +487,7 @@ export class NupstSnmp {
|
|||||||
oid: string,
|
oid: string,
|
||||||
description: string,
|
description: string,
|
||||||
config: ISnmpConfig,
|
config: ISnmpConfig,
|
||||||
|
// deno-lint-ignore no-explicit-any
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
if (this.debug) {
|
if (this.debug) {
|
||||||
logger.dim(`Retrying ${description} with fallback security level...`);
|
logger.dim(`Retrying ${description} with fallback security level...`);
|
||||||
@@ -483,7 +495,7 @@ export class NupstSnmp {
|
|||||||
|
|
||||||
// Try with authNoPriv if current level is authPriv
|
// Try with authNoPriv if current level is authPriv
|
||||||
if (config.securityLevel === 'authPriv') {
|
if (config.securityLevel === 'authPriv') {
|
||||||
const retryConfig = { ...config, securityLevel: 'authNoPriv' as 'authNoPriv' };
|
const retryConfig = { ...config, securityLevel: 'authNoPriv' as const };
|
||||||
try {
|
try {
|
||||||
if (this.debug) {
|
if (this.debug) {
|
||||||
logger.dim(`Retrying with authNoPriv security level`);
|
logger.dim(`Retrying with authNoPriv security level`);
|
||||||
@@ -496,7 +508,9 @@ export class NupstSnmp {
|
|||||||
} catch (retryError) {
|
} catch (retryError) {
|
||||||
if (this.debug) {
|
if (this.debug) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`Retry failed for ${description}: ${retryError instanceof Error ? retryError.message : String(retryError)}`,
|
`Retry failed for ${description}: ${
|
||||||
|
retryError instanceof Error ? retryError.message : String(retryError)
|
||||||
|
}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -504,7 +518,7 @@ export class NupstSnmp {
|
|||||||
|
|
||||||
// Try with noAuthNoPriv as a last resort
|
// Try with noAuthNoPriv as a last resort
|
||||||
if (config.securityLevel === 'authPriv' || config.securityLevel === 'authNoPriv') {
|
if (config.securityLevel === 'authPriv' || config.securityLevel === 'authNoPriv') {
|
||||||
const retryConfig = { ...config, securityLevel: 'noAuthNoPriv' as 'noAuthNoPriv' };
|
const retryConfig = { ...config, securityLevel: 'noAuthNoPriv' as const };
|
||||||
try {
|
try {
|
||||||
if (this.debug) {
|
if (this.debug) {
|
||||||
logger.dim(`Retrying with noAuthNoPriv security level`);
|
logger.dim(`Retrying with noAuthNoPriv security level`);
|
||||||
@@ -517,7 +531,9 @@ export class NupstSnmp {
|
|||||||
} catch (retryError) {
|
} catch (retryError) {
|
||||||
if (this.debug) {
|
if (this.debug) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`Retry failed for ${description}: ${retryError instanceof Error ? retryError.message : String(retryError)}`,
|
`Retry failed for ${description}: ${
|
||||||
|
retryError instanceof Error ? retryError.message : String(retryError)
|
||||||
|
}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -528,15 +544,16 @@ export class NupstSnmp {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Try standard OIDs as fallback
|
* Try standard OIDs as fallback
|
||||||
* @param oid OID to query
|
* @param _oid Original OID (unused, kept for method signature consistency)
|
||||||
* @param description Description of the value for logging
|
* @param description Description of the value for logging
|
||||||
* @param config SNMP configuration
|
* @param config SNMP configuration
|
||||||
* @returns Promise resolving to the SNMP value
|
* @returns Promise resolving to the SNMP value
|
||||||
*/
|
*/
|
||||||
private async tryStandardOids(
|
private async tryStandardOids(
|
||||||
oid: string,
|
_oid: string,
|
||||||
description: string,
|
description: string,
|
||||||
config: ISnmpConfig,
|
config: ISnmpConfig,
|
||||||
|
// deno-lint-ignore no-explicit-any
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
try {
|
try {
|
||||||
// Try RFC 1628 standard UPS MIB OIDs
|
// Try RFC 1628 standard UPS MIB OIDs
|
||||||
@@ -556,7 +573,9 @@ export class NupstSnmp {
|
|||||||
} catch (stdError) {
|
} catch (stdError) {
|
||||||
if (this.debug) {
|
if (this.debug) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`Standard OID retry failed for ${description}: ${stdError instanceof Error ? stdError.message : String(stdError)}`,
|
`Standard OID retry failed for ${description}: ${
|
||||||
|
stdError instanceof Error ? stdError.message : String(stdError)
|
||||||
|
}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export interface IUpsStatus {
|
|||||||
/** Output current in amps */
|
/** Output current in amps */
|
||||||
outputCurrent: number;
|
outputCurrent: number;
|
||||||
/** Raw values from SNMP responses */
|
/** Raw values from SNMP responses */
|
||||||
raw: Record<string, any>;
|
raw: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
116
ts/systemd.ts
116
ts/systemd.ts
@@ -1,10 +1,10 @@
|
|||||||
import process from 'node:process';
|
import process from 'node:process';
|
||||||
import { promises as fs } from 'node:fs';
|
import { promises as fs } from 'node:fs';
|
||||||
import { execSync } from 'node:child_process';
|
import { execSync } from 'node:child_process';
|
||||||
import { NupstDaemon, type IUpsConfig } from './daemon.ts';
|
import { type IUpsConfig, NupstDaemon } from './daemon.ts';
|
||||||
import { NupstSnmp } from './snmp/manager.ts';
|
import { NupstSnmp } from './snmp/manager.ts';
|
||||||
import { logger } from './logger.ts';
|
import { logger } from './logger.ts';
|
||||||
import { theme, symbols, getBatteryColor, getRuntimeColor, formatPowerStatus } from './colors.ts';
|
import { formatPowerStatus, getBatteryColor, getRuntimeColor, symbols, theme } from './colors.ts';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class for managing systemd service
|
* Class for managing systemd service
|
||||||
@@ -54,7 +54,11 @@ WantedBy=multi-user.target
|
|||||||
logger.log('');
|
logger.log('');
|
||||||
logger.error('No configuration found');
|
logger.error('No configuration found');
|
||||||
logger.log(` ${theme.dim('Config file:')} ${configPath}`);
|
logger.log(` ${theme.dim('Config file:')} ${configPath}`);
|
||||||
logger.log(` ${theme.dim('Run')} ${theme.command('nupst ups add')} ${theme.dim('to create a configuration')}`);
|
logger.log(
|
||||||
|
` ${theme.dim('Run')} ${theme.command('nupst ups add')} ${
|
||||||
|
theme.dim('to create a configuration')
|
||||||
|
}`,
|
||||||
|
);
|
||||||
logger.log('');
|
logger.log('');
|
||||||
throw new Error('Configuration not found');
|
throw new Error('Configuration not found');
|
||||||
}
|
}
|
||||||
@@ -142,32 +146,43 @@ WantedBy=multi-user.target
|
|||||||
private async displayVersionInfo(): Promise<void> {
|
private async displayVersionInfo(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const nupst = this.daemon.getNupstSnmp().getNupst();
|
const nupst = this.daemon.getNupstSnmp().getNupst();
|
||||||
|
if (!nupst) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const version = nupst.getVersion();
|
const version = nupst.getVersion();
|
||||||
|
|
||||||
// Check for updates
|
// Check for updates
|
||||||
const updateAvailable = await nupst.checkForUpdates();
|
const updateAvailable = await nupst.checkForUpdates();
|
||||||
|
|
||||||
// Display version info
|
// Display version info
|
||||||
if (updateAvailable) {
|
if (updateAvailable) {
|
||||||
const updateStatus = nupst.getUpdateStatus();
|
const updateStatus = nupst.getUpdateStatus();
|
||||||
logger.log('');
|
logger.log('');
|
||||||
logger.log(
|
logger.log(
|
||||||
`${theme.dim('NUPST')} ${theme.dim('v' + version)} ${symbols.warning} ${theme.statusWarning(`Update available: v${updateStatus.latestVersion}`)}`,
|
`${theme.dim('NUPST')} ${theme.dim('v' + version)} ${symbols.warning} ${
|
||||||
|
theme.statusWarning(`Update available: v${updateStatus.latestVersion}`)
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
logger.log(
|
||||||
|
` ${theme.dim('Run')} ${theme.command('sudo nupst update')} ${theme.dim('to upgrade')}`,
|
||||||
);
|
);
|
||||||
logger.log(` ${theme.dim('Run')} ${theme.command('sudo nupst update')} ${theme.dim('to upgrade')}`);
|
|
||||||
} else {
|
} else {
|
||||||
logger.log('');
|
logger.log('');
|
||||||
logger.log(
|
logger.log(
|
||||||
`${theme.dim('NUPST')} ${theme.dim('v' + version)} ${symbols.success} ${theme.success('Up to date')}`,
|
`${theme.dim('NUPST')} ${theme.dim('v' + version)} ${symbols.success} ${
|
||||||
|
theme.success('Up to date')
|
||||||
|
}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// If version check fails, show at least the current version
|
// If version check fails, show at least the current version
|
||||||
try {
|
try {
|
||||||
const nupst = this.daemon.getNupstSnmp().getNupst();
|
const nupst = this.daemon.getNupstSnmp().getNupst();
|
||||||
const version = nupst.getVersion();
|
if (nupst) {
|
||||||
logger.log('');
|
const version = nupst.getVersion();
|
||||||
logger.log(`${theme.dim('NUPST')} ${theme.dim('v' + version)}`);
|
logger.log('');
|
||||||
|
logger.log(`${theme.dim('NUPST')} ${theme.dim('v' + version)}`);
|
||||||
|
}
|
||||||
} catch (_innerError) {
|
} catch (_innerError) {
|
||||||
// Silently fail if we can't even get the version
|
// Silently fail if we can't even get the version
|
||||||
}
|
}
|
||||||
@@ -237,9 +252,15 @@ WantedBy=multi-user.target
|
|||||||
// Display beautiful status
|
// Display beautiful status
|
||||||
logger.log('');
|
logger.log('');
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
logger.log(`${symbols.running} ${theme.success('Service:')} ${theme.statusActive('active (running)')}`);
|
logger.log(
|
||||||
|
`${symbols.running} ${theme.success('Service:')} ${
|
||||||
|
theme.statusActive('active (running)')
|
||||||
|
}`,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
logger.log(`${symbols.stopped} ${theme.dim('Service:')} ${theme.statusInactive('inactive')}`);
|
logger.log(
|
||||||
|
`${symbols.stopped} ${theme.dim('Service:')} ${theme.statusInactive('inactive')}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pid || memory || cpu) {
|
if (pid || memory || cpu) {
|
||||||
@@ -250,10 +271,11 @@ WantedBy=multi-user.target
|
|||||||
logger.log(` ${details.join(' ')}`);
|
logger.log(` ${details.join(' ')}`);
|
||||||
}
|
}
|
||||||
logger.log('');
|
logger.log('');
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.log('');
|
logger.log('');
|
||||||
logger.log(`${symbols.stopped} ${theme.dim('Service:')} ${theme.statusInactive('not installed')}`);
|
logger.log(
|
||||||
|
`${symbols.stopped} ${theme.dim('Service:')} ${theme.statusInactive('not installed')}`,
|
||||||
|
);
|
||||||
logger.log('');
|
logger.log('');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -290,13 +312,13 @@ WantedBy=multi-user.target
|
|||||||
groups: [],
|
groups: [],
|
||||||
actions: config.thresholds
|
actions: config.thresholds
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
type: 'shutdown',
|
type: 'shutdown',
|
||||||
thresholds: config.thresholds,
|
thresholds: config.thresholds,
|
||||||
triggerMode: 'onlyThresholds',
|
triggerMode: 'onlyThresholds',
|
||||||
shutdownDelay: 5,
|
shutdownDelay: 5,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: [],
|
: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -304,7 +326,9 @@ WantedBy=multi-user.target
|
|||||||
} else {
|
} else {
|
||||||
logger.log('');
|
logger.log('');
|
||||||
logger.warn('No UPS devices configured');
|
logger.warn('No UPS devices configured');
|
||||||
logger.log(` ${theme.dim('Run')} ${theme.command('nupst ups add')} ${theme.dim('to add a device')}`);
|
logger.log(
|
||||||
|
` ${theme.dim('Run')} ${theme.command('nupst ups add')} ${theme.dim('to add a device')}`,
|
||||||
|
);
|
||||||
logger.log('');
|
logger.log('');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -339,7 +363,9 @@ WantedBy=multi-user.target
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Display UPS name and power status
|
// Display UPS name and power status
|
||||||
logger.log(` ${statusSymbol} ${theme.highlight(ups.name)} - ${formatPowerStatus(status.powerStatus)}`);
|
logger.log(
|
||||||
|
` ${statusSymbol} ${theme.highlight(ups.name)} - ${formatPowerStatus(status.powerStatus)}`,
|
||||||
|
);
|
||||||
|
|
||||||
// Display battery with color coding
|
// Display battery with color coding
|
||||||
const batteryColor = getBatteryColor(status.batteryCapacity);
|
const batteryColor = getBatteryColor(status.batteryCapacity);
|
||||||
@@ -347,16 +373,27 @@ WantedBy=multi-user.target
|
|||||||
// Get threshold from actions (if any action has thresholds defined)
|
// Get threshold from actions (if any action has thresholds defined)
|
||||||
const actionWithThresholds = ups.actions?.find((action) => action.thresholds);
|
const actionWithThresholds = ups.actions?.find((action) => action.thresholds);
|
||||||
const batteryThreshold = actionWithThresholds?.thresholds?.battery;
|
const batteryThreshold = actionWithThresholds?.thresholds?.battery;
|
||||||
const batterySymbol = batteryThreshold !== undefined && status.batteryCapacity >= batteryThreshold
|
const batterySymbol =
|
||||||
? symbols.success
|
batteryThreshold !== undefined && status.batteryCapacity >= batteryThreshold
|
||||||
: batteryThreshold !== undefined
|
? symbols.success
|
||||||
? symbols.warning
|
: batteryThreshold !== undefined
|
||||||
: '';
|
? symbols.warning
|
||||||
|
: '';
|
||||||
|
|
||||||
logger.log(` Battery: ${batteryColor(status.batteryCapacity + '%')} ${batterySymbol} Runtime: ${getRuntimeColor(status.batteryRuntime)(status.batteryRuntime + ' min')}`);
|
logger.log(
|
||||||
|
` Battery: ${batteryColor(status.batteryCapacity + '%')} ${batterySymbol} Runtime: ${
|
||||||
|
getRuntimeColor(status.batteryRuntime)(status.batteryRuntime + ' min')
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
|
||||||
// Display power metrics
|
// Display power metrics
|
||||||
logger.log(` Load: ${theme.highlight(status.outputLoad + '%')} Power: ${theme.highlight(status.outputPower + 'W')} Voltage: ${theme.highlight(status.outputVoltage + 'V')} Current: ${theme.highlight(status.outputCurrent + 'A')}`);
|
logger.log(
|
||||||
|
` Load: ${theme.highlight(status.outputLoad + '%')} Power: ${
|
||||||
|
theme.highlight(status.outputPower + 'W')
|
||||||
|
} Voltage: ${theme.highlight(status.outputVoltage + 'V')} Current: ${
|
||||||
|
theme.highlight(status.outputCurrent + 'A')
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
|
||||||
// Display host info
|
// Display host info
|
||||||
logger.log(` ${theme.dim(`Host: ${ups.snmp.host}:${ups.snmp.port}`)}`);
|
logger.log(` ${theme.dim(`Host: ${ups.snmp.host}:${ups.snmp.port}`)}`);
|
||||||
@@ -376,7 +413,9 @@ WantedBy=multi-user.target
|
|||||||
for (const action of ups.actions) {
|
for (const action of ups.actions) {
|
||||||
let actionDesc = `${action.type}`;
|
let actionDesc = `${action.type}`;
|
||||||
if (action.thresholds) {
|
if (action.thresholds) {
|
||||||
actionDesc += ` (${action.triggerMode || 'onlyThresholds'}: battery<${action.thresholds.battery}%, runtime<${action.thresholds.runtime}min`;
|
actionDesc += ` (${
|
||||||
|
action.triggerMode || 'onlyThresholds'
|
||||||
|
}: battery<${action.thresholds.battery}%, runtime<${action.thresholds.runtime}min`;
|
||||||
if (action.shutdownDelay) {
|
if (action.shutdownDelay) {
|
||||||
actionDesc += `, delay=${action.shutdownDelay}s`;
|
actionDesc += `, delay=${action.shutdownDelay}s`;
|
||||||
}
|
}
|
||||||
@@ -393,10 +432,11 @@ WantedBy=multi-user.target
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.log('');
|
logger.log('');
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Display error for this UPS
|
// Display error for this UPS
|
||||||
logger.log(` ${symbols.error} ${theme.highlight(ups.name)} - ${theme.error('Connection failed')}`);
|
logger.log(
|
||||||
|
` ${symbols.error} ${theme.highlight(ups.name)} - ${theme.error('Connection failed')}`,
|
||||||
|
);
|
||||||
logger.log(` ${theme.dim(error instanceof Error ? error.message : String(error))}`);
|
logger.log(` ${theme.dim(error instanceof Error ? error.message : String(error))}`);
|
||||||
logger.log(` ${theme.dim(`Host: ${ups.snmp.host}:${ups.snmp.port}`)}`);
|
logger.log(` ${theme.dim(`Host: ${ups.snmp.host}:${ups.snmp.port}`)}`);
|
||||||
logger.log('');
|
logger.log('');
|
||||||
@@ -421,7 +461,9 @@ WantedBy=multi-user.target
|
|||||||
// Display group name and mode
|
// Display group name and mode
|
||||||
const modeColor = group.mode === 'redundant' ? theme.success : theme.warning;
|
const modeColor = group.mode === 'redundant' ? theme.success : theme.warning;
|
||||||
logger.log(
|
logger.log(
|
||||||
` ${symbols.info} ${theme.highlight(group.name)} ${theme.dim(`(${modeColor(group.mode)})`)}`,
|
` ${symbols.info} ${theme.highlight(group.name)} ${
|
||||||
|
theme.dim(`(${modeColor(group.mode)})`)
|
||||||
|
}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Display description if present
|
// Display description if present
|
||||||
@@ -446,7 +488,9 @@ WantedBy=multi-user.target
|
|||||||
for (const action of group.actions) {
|
for (const action of group.actions) {
|
||||||
let actionDesc = `${action.type}`;
|
let actionDesc = `${action.type}`;
|
||||||
if (action.thresholds) {
|
if (action.thresholds) {
|
||||||
actionDesc += ` (${action.triggerMode || 'onlyThresholds'}: battery<${action.thresholds.battery}%, runtime<${action.thresholds.runtime}min`;
|
actionDesc += ` (${
|
||||||
|
action.triggerMode || 'onlyThresholds'
|
||||||
|
}: battery<${action.thresholds.battery}%, runtime<${action.thresholds.runtime}min`;
|
||||||
if (action.shutdownDelay) {
|
if (action.shutdownDelay) {
|
||||||
actionDesc += `, delay=${action.shutdownDelay}s`;
|
actionDesc += `, delay=${action.shutdownDelay}s`;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user