Nya notifikations alternativ

This commit is contained in:
2026-06-16 21:37:11 +02:00
parent 8491fe1386
commit 125fddfb1b
8 changed files with 395 additions and 78 deletions
+22 -1
View File
@@ -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() }));
}
+137 -11
View File
@@ -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 [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">(15)</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>