Nya notifikations alternativ
This commit is contained in:
@@ -1,4 +1,28 @@
|
||||
[
|
||||
{
|
||||
"id": "1780770465729-72o2",
|
||||
"timestamp": "2026-06-06T18:27:45.730Z",
|
||||
"provider": "cpanel",
|
||||
"operation": "GET /execute/DomainInfo/list_domains",
|
||||
"method": "GET",
|
||||
"url": "https://cpsrv32.misshosting.com/execute/DomainInfo/list_domains",
|
||||
"status": 200,
|
||||
"latency": 18,
|
||||
"ok": true,
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"id": "1780770465709-j66b",
|
||||
"timestamp": "2026-06-06T18:27:45.709Z",
|
||||
"provider": "cpanel",
|
||||
"operation": "GET /execute/DNS/list_zones",
|
||||
"method": "GET",
|
||||
"url": "https://cpsrv32.misshosting.com/execute/DNS/list_zones",
|
||||
"status": 200,
|
||||
"latency": 82,
|
||||
"ok": true,
|
||||
"error": null
|
||||
},
|
||||
{
|
||||
"id": "1780397326074-s7tw",
|
||||
"timestamp": "2026-06-02T10:48:46.074Z",
|
||||
|
||||
Generated
+10
@@ -14,6 +14,7 @@
|
||||
"express": "^4.19.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"node-schedule": "^2.1.1",
|
||||
"nodemailer": "^6.9.14",
|
||||
"xml2js": "^0.6.2",
|
||||
"xmlbuilder2": "^3.1.1"
|
||||
},
|
||||
@@ -1008,6 +1009,15 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/nodemailer": {
|
||||
"version": "6.10.1",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz",
|
||||
"integrity": "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==",
|
||||
"license": "MIT-0",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nodemon": {
|
||||
"version": "3.1.14",
|
||||
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.19.2",
|
||||
"bcryptjs": "^2.4.3",
|
||||
"nodemailer": "^6.9.14",
|
||||
"node-schedule": "^2.1.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"xml2js": "^0.6.2",
|
||||
|
||||
+128
-35
@@ -1,69 +1,162 @@
|
||||
const settings = require('./settings');
|
||||
const db = require('./db');
|
||||
const nodemailer = require('nodemailer');
|
||||
|
||||
/**
|
||||
* Send a Gotify notification if enabled and configured.
|
||||
* Errors are logged but never thrown — notifications are best-effort.
|
||||
*/
|
||||
async function notify(title, message) {
|
||||
// ─── Channel senders ──────────────────────────────────────────────────────────
|
||||
|
||||
async function sendGotify(title, message) {
|
||||
const { gotify } = settings.get();
|
||||
|
||||
if (!gotify.enabled || !gotify.url || !gotify.token) return;
|
||||
|
||||
const base = gotify.url.replace(/\/$/, '');
|
||||
|
||||
try {
|
||||
const res = await fetch(`${base}/message?token=${encodeURIComponent(gotify.token)}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
title,
|
||||
message,
|
||||
priority: gotify.priority ?? 5,
|
||||
}),
|
||||
body: JSON.stringify({ title, message, priority: gotify.priority ?? 5 }),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text().catch(() => '');
|
||||
console.error(`[notify] Gotify error ${res.status}: ${text}`);
|
||||
}
|
||||
if (!res.ok) console.error(`[notify] Gotify error ${res.status}: ${await res.text().catch(() => '')}`);
|
||||
} catch (err) {
|
||||
console.error('[notify] Failed to send Gotify notification:', err.message);
|
||||
console.error('[notify] Gotify failed:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Convenience helpers ─────────────────────────────────────────────────────
|
||||
async function sendNtfy(title, message) {
|
||||
const { ntfy } = settings.get();
|
||||
if (!ntfy.enabled || !ntfy.url || !ntfy.topic) return;
|
||||
const base = ntfy.url.replace(/\/$/, '');
|
||||
const headers = {
|
||||
'Content-Type': 'text/plain',
|
||||
'Title': title,
|
||||
'Priority': String(ntfy.priority ?? 3),
|
||||
};
|
||||
if (ntfy.token) headers['Authorization'] = `Bearer ${ntfy.token}`;
|
||||
try {
|
||||
const res = await fetch(`${base}/${encodeURIComponent(ntfy.topic)}`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: message,
|
||||
});
|
||||
if (!res.ok) console.error(`[notify] ntfy error ${res.status}: ${await res.text().catch(() => '')}`);
|
||||
} catch (err) {
|
||||
console.error('[notify] ntfy failed:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function sendSmtp(title, message) {
|
||||
const { smtp } = settings.get();
|
||||
if (!smtp.enabled || !smtp.host || !smtp.to || !smtp.from) return;
|
||||
try {
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: smtp.host,
|
||||
port: Number(smtp.port) || 587,
|
||||
secure: smtp.secure === true || smtp.secure === 'true',
|
||||
auth: smtp.username ? { user: smtp.username, pass: smtp.password } : undefined,
|
||||
});
|
||||
await transporter.sendMail({
|
||||
from: smtp.from,
|
||||
to: smtp.to,
|
||||
subject: title,
|
||||
text: message,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('[notify] SMTP failed:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function sendWebhook(title, message) {
|
||||
const { webhook } = settings.get();
|
||||
if (!webhook.enabled || !webhook.url) return;
|
||||
const body = JSON.stringify({ content: `**${title}**\n${message}` });
|
||||
const headers = { 'Content-Type': 'application/json' };
|
||||
if (webhook.secret) headers['X-Webhook-Secret'] = webhook.secret;
|
||||
try {
|
||||
const res = await fetch(webhook.url, { method: 'POST', headers, body });
|
||||
if (!res.ok) console.error(`[notify] Webhook error ${res.status}: ${await res.text().catch(() => '')}`);
|
||||
} catch (err) {
|
||||
console.error('[notify] Webhook failed:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Broadcast to all enabled channels ───────────────────────────────────────
|
||||
|
||||
async function notify(title, message) {
|
||||
await Promise.all([
|
||||
sendGotify(title, message),
|
||||
sendNtfy(title, message),
|
||||
sendSmtp(title, message),
|
||||
sendWebhook(title, message),
|
||||
]);
|
||||
}
|
||||
|
||||
// ─── Exported senders for test routes ────────────────────────────────────────
|
||||
|
||||
async function testGotify(cfg) {
|
||||
const base = cfg.url.replace(/\/$/, '');
|
||||
const res = await fetch(`${base}/message?token=${encodeURIComponent(cfg.token)}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ title: '🦥 Sloth Manager — Test', message: 'Gotify notifications are working correctly.', priority: cfg.priority ?? 5 }),
|
||||
});
|
||||
if (!res.ok) throw new Error(`Gotify returned ${res.status}: ${await res.text().catch(() => '')}`);
|
||||
}
|
||||
|
||||
async function testNtfy(cfg) {
|
||||
const base = cfg.url.replace(/\/$/, '');
|
||||
const headers = { 'Content-Type': 'text/plain', Title: '🦥 Sloth Manager — Test', Priority: String(cfg.priority ?? 3) };
|
||||
if (cfg.token) headers['Authorization'] = `Bearer ${cfg.token}`;
|
||||
const res = await fetch(`${base}/${encodeURIComponent(cfg.topic)}`, { method: 'POST', headers, body: 'ntfy notifications are working correctly.' });
|
||||
if (!res.ok) throw new Error(`ntfy returned ${res.status}: ${await res.text().catch(() => '')}`);
|
||||
}
|
||||
|
||||
async function testSmtp(cfg) {
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: cfg.host,
|
||||
port: Number(cfg.port) || 587,
|
||||
secure: cfg.secure === true || cfg.secure === 'true',
|
||||
auth: cfg.username ? { user: cfg.username, pass: cfg.password } : undefined,
|
||||
});
|
||||
await transporter.verify();
|
||||
await transporter.sendMail({
|
||||
from: cfg.from,
|
||||
to: cfg.to,
|
||||
subject: '🦥 Sloth Manager — Test',
|
||||
text: 'SMTP notifications are working correctly.',
|
||||
});
|
||||
}
|
||||
|
||||
async function testWebhook(cfg) {
|
||||
const headers = { 'Content-Type': 'application/json' };
|
||||
if (cfg.secret) headers['X-Webhook-Secret'] = cfg.secret;
|
||||
const res = await fetch(cfg.url, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({ content: '**🦥 Sloth Manager — Test**\nWebhook notifications are working correctly.' }),
|
||||
});
|
||||
if (!res.ok) throw new Error(`Webhook returned ${res.status}: ${await res.text().catch(() => '')}`);
|
||||
}
|
||||
|
||||
// ─── Event helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
function eventEnabled(key) {
|
||||
const { notifications } = settings.get();
|
||||
return notifications?.[key] !== false;
|
||||
return settings.get().notifications?.[key] !== false;
|
||||
}
|
||||
|
||||
function recordAdded(provider, zoneId, record) {
|
||||
if (!eventEnabled('dns_add')) return;
|
||||
const zoneName = db.getZoneName(provider, zoneId);
|
||||
return notify(
|
||||
`DNS Record Added`,
|
||||
`[${provider}] ${zoneName}\n+ ${record.type} ${record.name} → ${record.content}`,
|
||||
);
|
||||
return notify(`DNS Record Added`, `[${provider}] ${zoneName}\n+ ${record.type} ${record.name} → ${record.content}`);
|
||||
}
|
||||
|
||||
function recordUpdated(provider, zoneId, oldRecord, newRecord) {
|
||||
if (!eventEnabled('dns_update')) return;
|
||||
const zoneName = db.getZoneName(provider, zoneId);
|
||||
return notify(
|
||||
`DNS Record Updated`,
|
||||
`[${provider}] ${zoneName}\n✎ ${oldRecord.type} ${oldRecord.name}\n ${oldRecord.content} → ${newRecord.content}`,
|
||||
);
|
||||
return notify(`DNS Record Updated`, `[${provider}] ${zoneName}\n✎ ${oldRecord.type} ${oldRecord.name}\n ${oldRecord.content} → ${newRecord.content}`);
|
||||
}
|
||||
|
||||
function recordDeleted(provider, zoneId, record) {
|
||||
if (!eventEnabled('dns_delete')) return;
|
||||
const zoneName = db.getZoneName(provider, zoneId);
|
||||
return notify(
|
||||
`DNS Record Deleted`,
|
||||
`[${provider}] ${zoneName}\n− ${record.type} ${record.name} ${record.content}`,
|
||||
);
|
||||
return notify(`DNS Record Deleted`, `[${provider}] ${zoneName}\n− ${record.type} ${record.name} ${record.content}`);
|
||||
}
|
||||
|
||||
module.exports = { notify, recordAdded, recordUpdated, recordDeleted };
|
||||
module.exports = { notify, testGotify, testNtfy, testSmtp, testWebhook, recordAdded, recordUpdated, recordDeleted };
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const settings = require('../settings');
|
||||
const { notify } = require('../notify');
|
||||
const { testGotify, testNtfy, testSmtp, testWebhook } = require('../notify');
|
||||
|
||||
// GET /api/settings
|
||||
router.get('/', (req, res) => {
|
||||
@@ -12,8 +12,7 @@ router.get('/', (req, res) => {
|
||||
router.put('/', (req, res) => {
|
||||
try {
|
||||
const updated = settings.update(req.body);
|
||||
// Reschedule secret check if the notification time changed
|
||||
if (req.body.notifications?.secret_check_time) {
|
||||
if (req.body.notifications?.secret_check_time || req.body.notifications?.timezone) {
|
||||
try { require('../index').scheduleSecretCheck(); } catch { /* safe to ignore */ }
|
||||
}
|
||||
res.json(updated);
|
||||
@@ -22,36 +21,51 @@ router.put('/', (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/settings/test-notification
|
||||
router.post('/test-notification', async (req, res) => {
|
||||
// Use the payload from the request body so the user can test
|
||||
// before saving (the frontend sends the current form values)
|
||||
// POST /api/settings/test-gotify
|
||||
router.post('/test-gotify', async (req, res) => {
|
||||
const { url, token, priority } = req.body;
|
||||
|
||||
if (!url || !token) {
|
||||
return res.status(400).json({ error: 'URL and token are required' });
|
||||
}
|
||||
|
||||
const base = url.replace(/\/$/, '');
|
||||
if (!url || !token) return res.status(400).json({ error: 'URL and token are required' });
|
||||
try {
|
||||
const response = await fetch(`${base}/message?token=${encodeURIComponent(token)}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
title: '🦥 Sloth Manager — Test',
|
||||
message: 'Gotify notifications are working correctly.',
|
||||
priority: priority ?? 5,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text().catch(() => '');
|
||||
return res.status(502).json({ error: `Gotify returned ${response.status}: ${text}` });
|
||||
}
|
||||
|
||||
await testGotify({ url, token, priority });
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
res.status(502).json({ error: `Could not reach Gotify: ${err.message}` });
|
||||
res.status(502).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/settings/test-ntfy
|
||||
router.post('/test-ntfy', async (req, res) => {
|
||||
const { url, topic, token, priority } = req.body;
|
||||
if (!url || !topic) return res.status(400).json({ error: 'URL and topic are required' });
|
||||
try {
|
||||
await testNtfy({ url, topic, token, priority });
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
res.status(502).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/settings/test-smtp
|
||||
router.post('/test-smtp', async (req, res) => {
|
||||
const { host, port, secure, username, password, from, to } = req.body;
|
||||
if (!host || !from || !to) return res.status(400).json({ error: 'Host, from, and to are required' });
|
||||
try {
|
||||
await testSmtp({ host, port, secure, username, password, from, to });
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
res.status(502).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/settings/test-webhook
|
||||
router.post('/test-webhook', async (req, res) => {
|
||||
const { url, secret } = req.body;
|
||||
if (!url) return res.status(400).json({ error: 'Webhook URL is required' });
|
||||
try {
|
||||
await testWebhook({ url, secret });
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
res.status(502).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -18,6 +18,28 @@ const DEFAULTS = {
|
||||
secret_check_time: '08:00',
|
||||
timezone: 'UTC',
|
||||
},
|
||||
ntfy: {
|
||||
enabled: false,
|
||||
url: 'https://ntfy.sh',
|
||||
topic: '',
|
||||
token: '',
|
||||
priority: 3,
|
||||
},
|
||||
smtp: {
|
||||
enabled: false,
|
||||
host: '',
|
||||
port: 587,
|
||||
secure: false,
|
||||
username: '',
|
||||
password: '',
|
||||
from: '',
|
||||
to: '',
|
||||
},
|
||||
webhook: {
|
||||
enabled: false,
|
||||
url: '',
|
||||
secret: '',
|
||||
},
|
||||
providerColors: {
|
||||
cloudflare: '#f6821f',
|
||||
loopia: '#2ecc71',
|
||||
@@ -36,6 +58,9 @@ function load() {
|
||||
...raw,
|
||||
gotify: { ...DEFAULTS.gotify, ...(raw.gotify ?? {}) },
|
||||
notifications: { ...DEFAULTS.notifications, ...(raw.notifications ?? {}) },
|
||||
ntfy: { ...DEFAULTS.ntfy, ...(raw.ntfy ?? {}) },
|
||||
smtp: { ...DEFAULTS.smtp, ...(raw.smtp ?? {}) },
|
||||
webhook: { ...DEFAULTS.webhook, ...(raw.webhook ?? {}) },
|
||||
providerColors: { ...DEFAULTS.providerColors, ...(raw.providerColors ?? {}) },
|
||||
};
|
||||
} catch {
|
||||
@@ -58,6 +83,9 @@ function update(partial) {
|
||||
...partial,
|
||||
gotify: { ...current.gotify, ...(partial.gotify ?? {}) },
|
||||
notifications: { ...current.notifications, ...(partial.notifications ?? {}) },
|
||||
ntfy: { ...current.ntfy, ...(partial.ntfy ?? {}) },
|
||||
smtp: { ...current.smtp, ...(partial.smtp ?? {}) },
|
||||
webhook: { ...current.webhook, ...(partial.webhook ?? {}) },
|
||||
providerColors: { ...current.providerColors, ...(partial.providerColors ?? {}) },
|
||||
};
|
||||
save(merged);
|
||||
|
||||
+22
-1
@@ -190,12 +190,33 @@ export async function clearCache() {
|
||||
}
|
||||
|
||||
export async function testNotification(gotify) {
|
||||
return handleResponse(await fetch(`${BASE}/settings/test-notification`, {
|
||||
return handleResponse(await fetch(`${BASE}/settings/test-gotify`, {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json', ...authHeaders() },
|
||||
body: JSON.stringify(gotify),
|
||||
}));
|
||||
}
|
||||
|
||||
export async function testNtfyNotification(ntfy) {
|
||||
return handleResponse(await fetch(`${BASE}/settings/test-ntfy`, {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json', ...authHeaders() },
|
||||
body: JSON.stringify(ntfy),
|
||||
}));
|
||||
}
|
||||
|
||||
export async function testSmtpNotification(smtp) {
|
||||
return handleResponse(await fetch(`${BASE}/settings/test-smtp`, {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json', ...authHeaders() },
|
||||
body: JSON.stringify(smtp),
|
||||
}));
|
||||
}
|
||||
|
||||
export async function testWebhookNotification(webhook) {
|
||||
return handleResponse(await fetch(`${BASE}/settings/test-webhook`, {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json', ...authHeaders() },
|
||||
body: JSON.stringify(webhook),
|
||||
}));
|
||||
}
|
||||
|
||||
export async function getSyncStatus(provider) {
|
||||
return handleResponse(await fetch(`${BASE}/sync-status/${provider}`, { headers: authHeaders() }));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { getSettings, saveSettings, testNotification, clearCache, getUsers, createUser, deleteUser, getProviderHealth } from '../api/dns';
|
||||
import { getSettings, saveSettings, testNotification, testNtfyNotification, testSmtpNotification, testWebhookNotification, clearCache, getUsers, createUser, deleteUser, getProviderHealth } from '../api/dns';
|
||||
import ConfirmDialog from './ConfirmDialog';
|
||||
import { exportCsv } from '../utils/exportCsv';
|
||||
import { useProviderColors, providerBadgeStyle } from '../context/ProviderColors';
|
||||
@@ -16,18 +16,30 @@ const TABS = [
|
||||
|
||||
function NotificationsTab() {
|
||||
const [form, setForm] = useState({ enabled: false, url: '', token: '', priority: 5 });
|
||||
const [ntfy, setNtfy] = useState({ enabled: false, url: 'https://ntfy.sh', topic: '', token: '', priority: 3 });
|
||||
const [smtp, setSmtp] = useState({ enabled: false, host: '', port: 587, secure: false, username: '', password: '', from: '', to: '' });
|
||||
const [webhook, setWebhook] = useState({ enabled: false, url: '', secret: '' });
|
||||
const [events, setEvents] = useState({ dns_add: true, dns_update: true, dns_delete: true, secret_check: true, secret_check_time: '08:00', timezone: 'UTC' });
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [testing, setTesting] = useState(false);
|
||||
const [testingNtfy, setTestingNtfy] = useState(false);
|
||||
const [testingSmtp, setTestingSmtp] = useState(false);
|
||||
const [testingWebhook, setTestingWebhook] = useState(false);
|
||||
const [saved, setSaved] = useState(false);
|
||||
const [testResult, setTestResult] = useState(null);
|
||||
const [testResultNtfy, setTestResultNtfy] = useState(null);
|
||||
const [testResultSmtp, setTestResultSmtp] = useState(null);
|
||||
const [testResultWebhook, setTestResultWebhook] = useState(null);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
getSettings()
|
||||
.then(s => {
|
||||
setForm({ ...s.gotify });
|
||||
setForm({ ...{ enabled: false, url: '', token: '', priority: 5 }, ...s.gotify });
|
||||
setNtfy({ ...{ enabled: false, url: 'https://ntfy.sh', topic: '', token: '', priority: 3 }, ...s.ntfy });
|
||||
setSmtp({ ...{ enabled: false, host: '', port: 587, secure: false, username: '', password: '', from: '', to: '' }, ...s.smtp });
|
||||
setWebhook({ ...{ enabled: false, url: '', secret: '' }, ...s.webhook });
|
||||
setEvents({ ...{ dns_add: true, dns_update: true, dns_delete: true, secret_check: true, secret_check_time: '08:00', timezone: 'UTC' }, ...s.notifications });
|
||||
})
|
||||
.catch(e => setError(e.message))
|
||||
@@ -51,7 +63,7 @@ function NotificationsTab() {
|
||||
e.preventDefault();
|
||||
setSaving(true); setError(''); setSaved(false);
|
||||
try {
|
||||
await saveSettings({ gotify: { ...form, priority: Number(form.priority) }, notifications: events });
|
||||
await saveSettings({ gotify: { ...form, priority: Number(form.priority) }, ntfy: { ...ntfy, priority: Number(ntfy.priority) }, smtp: { ...smtp, port: Number(smtp.port) }, webhook, notifications: events });
|
||||
setSaved(true);
|
||||
setTimeout(() => setSaved(false), 3000);
|
||||
} catch (err) { setError(err.message); }
|
||||
@@ -68,6 +80,36 @@ function NotificationsTab() {
|
||||
} finally { setTesting(false); }
|
||||
}
|
||||
|
||||
async function handleTestNtfy() {
|
||||
setTestingNtfy(true); setTestResultNtfy(null);
|
||||
try {
|
||||
await testNtfyNotification({ url: ntfy.url, topic: ntfy.topic, token: ntfy.token, priority: Number(ntfy.priority) });
|
||||
setTestResultNtfy({ ok: true, message: 'Notification sent successfully.' });
|
||||
} catch (err) {
|
||||
setTestResultNtfy({ ok: false, message: err.message });
|
||||
} finally { setTestingNtfy(false); }
|
||||
}
|
||||
|
||||
async function handleTestSmtp() {
|
||||
setTestingSmtp(true); setTestResultSmtp(null);
|
||||
try {
|
||||
await testSmtpNotification({ ...smtp, port: Number(smtp.port) });
|
||||
setTestResultSmtp({ ok: true, message: 'Test email sent successfully.' });
|
||||
} catch (err) {
|
||||
setTestResultSmtp({ ok: false, message: err.message });
|
||||
} finally { setTestingSmtp(false); }
|
||||
}
|
||||
|
||||
async function handleTestWebhook() {
|
||||
setTestingWebhook(true); setTestResultWebhook(null);
|
||||
try {
|
||||
await testWebhookNotification({ url: webhook.url, secret: webhook.secret });
|
||||
setTestResultWebhook({ ok: true, message: 'Webhook delivered successfully.' });
|
||||
} catch (err) {
|
||||
setTestResultWebhook({ ok: false, message: err.message });
|
||||
} finally { setTestingWebhook(false); }
|
||||
}
|
||||
|
||||
if (loading) return <p className="hint">Loading…</p>;
|
||||
|
||||
return (
|
||||
@@ -167,14 +209,98 @@ function NotificationsTab() {
|
||||
</section>
|
||||
|
||||
<section className="settings-section">
|
||||
<h3>About Gotify</h3>
|
||||
<h3>ntfy</h3>
|
||||
<p className="settings-desc">Send notifications via <a href="https://ntfy.sh" target="_blank" rel="noreferrer" style={{ color: 'var(--accent)' }}>ntfy.sh</a> or a self-hosted ntfy instance.</p>
|
||||
<div className="settings-form">
|
||||
<label className="toggle-row">
|
||||
<span>Enable ntfy</span>
|
||||
<input type="checkbox" checked={!!ntfy.enabled} onChange={e => setNtfy(f => ({ ...f, enabled: e.target.checked }))} className="toggle" />
|
||||
</label>
|
||||
<div className="form-row">
|
||||
<label>Server URL<input value={ntfy.url} onChange={e => setNtfy(f => ({ ...f, url: e.target.value }))} placeholder="https://ntfy.sh" disabled={!ntfy.enabled} /></label>
|
||||
<label>Topic<input value={ntfy.topic} onChange={e => setNtfy(f => ({ ...f, topic: e.target.value }))} placeholder="your-topic" disabled={!ntfy.enabled} /></label>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label>Access Token (optional)<input type="password" value={ntfy.token} onChange={e => setNtfy(f => ({ ...f, token: e.target.value }))} placeholder="For protected topics" disabled={!ntfy.enabled} /></label>
|
||||
<label>Priority <span className="settings-hint">(1–5)</span><input type="number" min={1} max={5} value={ntfy.priority} onChange={e => setNtfy(f => ({ ...f, priority: e.target.value }))} disabled={!ntfy.enabled} /></label>
|
||||
</div>
|
||||
{testResultNtfy && <p className={testResultNtfy.ok ? 'test-ok' : 'test-fail'}>{testResultNtfy.ok ? '✓' : '✕'} {testResultNtfy.message}</p>}
|
||||
<div className="form-actions">
|
||||
<button type="button" className="btn-secondary" onClick={handleTestNtfy} disabled={testingNtfy || !ntfy.url || !ntfy.topic}>{testingNtfy ? 'Sending…' : 'Send Test'}</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="settings-section">
|
||||
<h3>Email (SMTP)</h3>
|
||||
<p className="settings-desc">Send notifications via email using any SMTP server.</p>
|
||||
<div className="settings-form">
|
||||
<label className="toggle-row">
|
||||
<span>Enable SMTP</span>
|
||||
<input type="checkbox" checked={!!smtp.enabled} onChange={e => setSmtp(f => ({ ...f, enabled: e.target.checked }))} className="toggle" />
|
||||
</label>
|
||||
<div className="form-row">
|
||||
<label>SMTP Host<input value={smtp.host} onChange={e => setSmtp(f => ({ ...f, host: e.target.value }))} placeholder="smtp.example.com" disabled={!smtp.enabled} /></label>
|
||||
<label>Port<input type="number" value={smtp.port} onChange={e => setSmtp(f => ({ ...f, port: e.target.value }))} placeholder="587" disabled={!smtp.enabled} /></label>
|
||||
</div>
|
||||
<label className="toggle-row" style={{ color: 'var(--text)', textTransform: 'none', letterSpacing: 0, fontSize: 14, fontWeight: 500 }}>
|
||||
<span>Use TLS/SSL (port 465)</span>
|
||||
<input type="checkbox" checked={!!smtp.secure} onChange={e => setSmtp(f => ({ ...f, secure: e.target.checked }))} className="toggle" disabled={!smtp.enabled} />
|
||||
</label>
|
||||
<div className="form-row">
|
||||
<label>Username<input value={smtp.username} onChange={e => setSmtp(f => ({ ...f, username: e.target.value }))} placeholder="Optional" disabled={!smtp.enabled} /></label>
|
||||
<label>Password<input type="password" value={smtp.password} onChange={e => setSmtp(f => ({ ...f, password: e.target.value }))} placeholder="Optional" disabled={!smtp.enabled} /></label>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
<label>From<input type="email" value={smtp.from} onChange={e => setSmtp(f => ({ ...f, from: e.target.value }))} placeholder="sloth@example.com" disabled={!smtp.enabled} /></label>
|
||||
<label>To<input type="email" value={smtp.to} onChange={e => setSmtp(f => ({ ...f, to: e.target.value }))} placeholder="you@example.com" disabled={!smtp.enabled} /></label>
|
||||
</div>
|
||||
{testResultSmtp && <p className={testResultSmtp.ok ? 'test-ok' : 'test-fail'}>{testResultSmtp.ok ? '✓' : '✕'} {testResultSmtp.message}</p>}
|
||||
<div className="form-actions">
|
||||
<button type="button" className="btn-secondary" onClick={handleTestSmtp} disabled={testingSmtp || !smtp.host || !smtp.from || !smtp.to}>{testingSmtp ? 'Sending…' : 'Send Test'}</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="settings-section">
|
||||
<h3>Webhook</h3>
|
||||
<p className="settings-desc">
|
||||
POST a JSON payload to any HTTP endpoint. Compatible with Discord, Slack, and custom receivers. For Discord use a <strong>Webhook URL</strong> from Server Settings → Integrations → Webhooks.
|
||||
</p>
|
||||
<div className="settings-form">
|
||||
<label className="toggle-row">
|
||||
<span>Enable Webhook</span>
|
||||
<input type="checkbox" checked={!!webhook.enabled} onChange={e => setWebhook(f => ({ ...f, enabled: e.target.checked }))} className="toggle" />
|
||||
</label>
|
||||
<label>Webhook URL
|
||||
<input type="url" value={webhook.url} onChange={e => setWebhook(f => ({ ...f, url: e.target.value }))} placeholder="https://discord.com/api/webhooks/…" disabled={!webhook.enabled} />
|
||||
</label>
|
||||
<label>Secret / Token <span className="settings-hint">(optional — sent as X-Webhook-Secret header)</span>
|
||||
<input type="password" value={webhook.secret} onChange={e => setWebhook(f => ({ ...f, secret: e.target.value }))} placeholder="Optional shared secret" disabled={!webhook.enabled} />
|
||||
</label>
|
||||
{testResultWebhook && <p className={testResultWebhook.ok ? 'test-ok' : 'test-fail'}>{testResultWebhook.ok ? '✓' : '✕'} {testResultWebhook.message}</p>}
|
||||
<div className="form-actions">
|
||||
<button type="button" className="btn-secondary" onClick={handleTestWebhook} disabled={testingWebhook || !webhook.url}>{testingWebhook ? 'Sending…' : 'Send Test'}</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="settings-section">
|
||||
<h3>About Notifications</h3>
|
||||
<p className="settings-desc" style={{ lineHeight: 1.8 }}>
|
||||
<a href="https://gotify.net" target="_blank" rel="noreferrer" style={{ color: 'var(--accent)' }}>Gotify</a> is a self-hosted push notification server.<br /><br />
|
||||
<strong>To get started:</strong><br />
|
||||
1. Log in to your Gotify instance<br />
|
||||
2. Go to <strong>Apps → Create application</strong><br />
|
||||
3. Copy the generated token and paste it into App Token<br /><br />
|
||||
Notifications fire on every record add, update, or delete — not on sync.
|
||||
Sloth Manager can deliver notifications through four independent channels — enable as many as you need. All enabled channels receive the same events simultaneously.<br /><br />
|
||||
|
||||
<strong>Gotify</strong> — self-hosted push notification server. Create an application in your Gotify instance and paste the app token here. <a href="https://gotify.net" target="_blank" rel="noreferrer" style={{ color: 'var(--accent)' }}>gotify.net</a><br /><br />
|
||||
|
||||
<strong>ntfy</strong> — open-source pub/sub push service. Use the public <a href="https://ntfy.sh" target="_blank" rel="noreferrer" style={{ color: 'var(--accent)' }}>ntfy.sh</a> server or a self-hosted instance. Set a topic name and optionally an access token for protected topics.<br /><br />
|
||||
|
||||
<strong>Email (SMTP)</strong> — send notifications via any SMTP server. TLS/SSL (port 465) and STARTTLS (port 587) are both supported. Username and password are optional for open relays.<br /><br />
|
||||
|
||||
<strong>Webhook</strong> — POST a JSON payload to any HTTP endpoint. The body uses Discord's <code>{`{"content":"…"}`}</code> format, which is also accepted by many other services. An optional shared secret is sent as the <code>X-Webhook-Secret</code> header for request verification.<br /><br />
|
||||
|
||||
<strong>Notify on</strong> controls which events trigger a notification. DNS record events fire immediately. The secret expiry reminder runs once daily at the configured time and timezone, sending one message listing all expiring or expired secrets.<br /><br />
|
||||
|
||||
Sync operations never trigger notifications regardless of settings.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user