Pushover and API Change truenas

This commit is contained in:
2026-01-27 09:59:32 +00:00
parent 945125b069
commit 3e6132c1bb
29 changed files with 6566 additions and 66 deletions
+236
View File
@@ -0,0 +1,236 @@
import { sendPushoverMessage } from './pushover.js';
const LEVELS = {
none: 0,
warn: 1,
crit: 2
};
const alertState = new Map();
const toNumber = (value, fallback) => {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : fallback;
};
const clampPercent = (value, fallback) => {
const parsed = toNumber(value, fallback);
if (!Number.isFinite(parsed)) {
return fallback;
}
return Math.max(1, Math.min(100, parsed));
};
const formatBytes = (bytes) => {
const value = Number(bytes);
if (!Number.isFinite(value) || value <= 0) {
return '0 B';
}
const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB'];
let current = value;
let unitIndex = 0;
while (current >= 1024 && unitIndex < units.length - 1) {
current /= 1024;
unitIndex += 1;
}
return `${current.toFixed(current >= 100 ? 0 : 2)} ${units[unitIndex]}`;
};
const percentUsed = (usedBytes, totalBytes) => {
const total = toNumber(totalBytes, 0);
const used = toNumber(usedBytes, 0);
if (total <= 0 || used <= 0) {
return 0;
}
return Math.max(0, Math.min(100, (used / total) * 100));
};
const normalizeSettings = (settings) => {
const warnPercent = clampPercent(settings.warnPercent, 80);
const critPercent = clampPercent(settings.critPercent, 90);
const normalizedCrit = Math.max(critPercent, warnPercent + 1);
const cooldownMinutes = Math.max(0, Math.round(toNumber(settings.cooldownMinutes, 60)));
return {
enabled: settings.enabled !== false,
warnPercent,
critPercent: normalizedCrit,
cooldownMinutes,
cooldownMs: cooldownMinutes * 60 * 1000,
once: Boolean(settings.once),
includePools: settings.includePools !== false
};
};
const mergeSettings = (base, override) => {
if (!override || typeof override !== 'object') {
return base;
}
return normalizeSettings({
...base,
...override
});
};
const resolveSettings = (alertsConfig, serverConfig, poolName) => {
const base = normalizeSettings(alertsConfig?.defaults || {});
const serverAlerts = serverConfig?.alerts || {};
const poolOverrides = poolName
? serverAlerts?.pools?.[poolName] || serverAlerts?.pools?.[String(poolName)]
: null;
const serverMerged = mergeSettings(base, serverAlerts);
const poolMerged = poolOverrides ? mergeSettings(serverMerged, poolOverrides) : serverMerged;
return {
...poolMerged,
enabled: Boolean(alertsConfig?.enabled) && poolMerged.enabled
};
};
const levelForPercent = (percent, settings) => {
if (percent >= settings.critPercent) {
return 'crit';
}
if (percent >= settings.warnPercent) {
return 'warn';
}
return 'none';
};
const shouldSend = (key, level, settings) => {
if (level === 'none') {
alertState.delete(key);
return false;
}
const now = Date.now();
const currentRank = LEVELS[level];
const previous = alertState.get(key);
if (!previous) {
alertState.set(key, { lastSentAt: 0, levelRank: 0, latched: false });
}
const state = alertState.get(key);
if (currentRank > state.levelRank) {
return true;
}
if (settings.once && state.latched) {
return false;
}
if (!settings.cooldownMs) {
return true;
}
return now - state.lastSentAt >= settings.cooldownMs;
};
const markSent = (key, level, settings) => {
const now = Date.now();
const levelRank = LEVELS[level] || 0;
alertState.set(key, {
lastSentAt: now,
levelRank,
latched: settings.once ? true : false
});
};
const buildServerMessage = (server, percent, level) => {
const used = formatBytes(server.usedBytes);
const total = formatBytes(server.totalBytes);
const rounded = Math.round(percent);
const levelLabel = level === 'crit' ? 'CRITICAL' : 'Warning';
return {
title: `Vault: ${server.name} ${levelLabel}`,
message: `Server total at ${rounded}% (${used} / ${total}).`,
priority: level === 'crit' ? 1 : 0
};
};
const buildPoolMessage = (server, pool, percent, level) => {
const used = formatBytes(pool.usedBytes);
const total = formatBytes(pool.totalBytes);
const rounded = Math.round(percent);
const levelLabel = level === 'crit' ? 'CRITICAL' : 'Warning';
return {
title: `Vault: ${server.name} / ${pool.name} ${levelLabel}`,
message: `${pool.name} at ${rounded}% (${used} / ${total}).`,
priority: level === 'crit' ? 1 : 0
};
};
const poolKey = (serverId, poolName) => `${serverId}::pool::${poolName}`;
const serverKey = (serverId) => `${serverId}::server`;
export async function evaluateAndSendAlerts(payload, storedServers, alertsConfig) {
if (!payload || !Array.isArray(payload.servers)) {
return { sent: 0, skipped: 0 };
}
const serverMap = new Map((storedServers || []).map((server) => [server.id, server]));
let sent = 0;
let skipped = 0;
for (const server of payload.servers) {
const stored = serverMap.get(server.id) || {};
const serverSettings = resolveSettings(alertsConfig, stored, null);
if (serverSettings.enabled) {
const percent = percentUsed(server.usedBytes, server.totalBytes);
const level = levelForPercent(percent, serverSettings);
const key = serverKey(server.id);
if (shouldSend(key, level, serverSettings)) {
try {
await sendPushoverMessage(alertsConfig, buildServerMessage(server, percent, level));
markSent(key, level, serverSettings);
sent += 1;
} catch (error) {
// eslint-disable-next-line no-console
console.error('Pushover server alert failed:', error.message || error);
}
} else {
skipped += 1;
}
}
if (!serverSettings.includePools || !Array.isArray(server.pools)) {
continue;
}
for (const pool of server.pools) {
const poolSettings = resolveSettings(alertsConfig, stored, pool.name);
if (!poolSettings.enabled) {
continue;
}
const percent = percentUsed(pool.usedBytes, pool.totalBytes);
const level = levelForPercent(percent, poolSettings);
const key = poolKey(server.id, pool.name);
if (shouldSend(key, level, poolSettings)) {
try {
await sendPushoverMessage(alertsConfig, buildPoolMessage(server, pool, percent, level));
markSent(key, level, poolSettings);
sent += 1;
} catch (error) {
// eslint-disable-next-line no-console
console.error('Pushover pool alert failed:', error.message || error);
}
} else {
skipped += 1;
}
}
}
return { sent, skipped };
}
export async function sendTestAlert(alertsConfig) {
return sendPushoverMessage(alertsConfig, {
title: 'Vault: Test Alert',
message: 'Pushover integration is working.',
priority: 0
});
}
+49
View File
@@ -0,0 +1,49 @@
const PUSHOVER_URL = 'https://api.pushover.net/1/messages.json';
const toNumber = (value, fallback) => {
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : fallback;
};
export async function sendPushoverMessage(config, message) {
const userKey = String(config?.pushover?.userKey || '').trim();
const apiToken = String(config?.pushover?.apiToken || '').trim();
if (!userKey || !apiToken) {
throw new Error('Pushover requires userKey and apiToken.');
}
const body = new URLSearchParams({
token: apiToken,
user: userKey,
title: String(message?.title || 'Vault Alert'),
message: String(message?.message || ''),
priority: String(toNumber(message?.priority, 0))
});
if (message?.url) {
body.set('url', String(message.url));
}
if (message?.urlTitle) {
body.set('url_title', String(message.urlTitle));
}
if (message?.sound) {
body.set('sound', String(message.sound));
}
const response = await fetch(PUSHOVER_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body
});
if (!response.ok) {
const text = await response.text().catch(() => '');
throw new Error(`Pushover error (${response.status}): ${text || 'request failed'}`);
}
return response.json().catch(() => ({ ok: true }));
}