Pushover and API Change truenas
This commit is contained in:
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 }));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user