feat: add notification alert rules

This commit is contained in:
Christian Krakau-Louis
2026-05-22 22:55:49 +02:00
parent 9d0375569b
commit 05b687b525
9 changed files with 703 additions and 10 deletions
+141
View File
@@ -235,7 +235,100 @@
</div>
</template>
<div class="border-t border-border pt-4 space-y-4">
<div>
<h3 class="text-sm font-semibold">Alert Rules</h3>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="form-control">
<label class="label cursor-pointer justify-start gap-3">
<input type="checkbox"
:checked="s['notifications.alert_new_sources_enabled'] === 'true'"
@change="s['notifications.alert_new_sources_enabled'] = $event.target.checked ? 'true' : 'false'"
class="checkbox checkbox-primary" />
<span class="label-text font-medium">New sending sources</span>
</label>
</div>
<div class="form-control">
<label class="label cursor-pointer justify-start gap-3">
<input type="checkbox"
:checked="s['notifications.alert_missing_reports_enabled'] === 'true'"
@change="s['notifications.alert_missing_reports_enabled'] = $event.target.checked ? 'true' : 'false'"
class="checkbox checkbox-primary" />
<span class="label-text font-medium">Missing reports</span>
</label>
</div>
<div class="form-control">
<label class="label cursor-pointer justify-start gap-3">
<input type="checkbox"
:checked="s['notifications.alert_failure_threshold_enabled'] === 'true'"
@change="s['notifications.alert_failure_threshold_enabled'] = $event.target.checked ? 'true' : 'false'"
class="checkbox checkbox-primary" />
<span class="label-text font-medium">High DMARC failures</span>
</label>
</div>
<div class="form-control">
<label class="label cursor-pointer justify-start gap-3">
<input type="checkbox"
:checked="s['notifications.alert_compliance_drop_enabled'] === 'true'"
@change="s['notifications.alert_compliance_drop_enabled'] = $event.target.checked ? 'true' : 'false'"
class="checkbox checkbox-primary" />
<span class="label-text font-medium">Compliance drops</span>
</label>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="form-control w-full">
<label class="label"><span class="label-text font-medium">Failure Threshold</span></label>
<input type="number" x-model.number="s['notifications.alert_failure_threshold_count']"
class="input input-bordered w-full" min="1" />
</div>
<div class="form-control w-full">
<label class="label"><span class="label-text font-medium">Missing After Days</span></label>
<input type="number" x-model.number="s['notifications.alert_missing_reports_days']"
class="input input-bordered w-full" min="1" />
</div>
<div class="form-control w-full">
<label class="label"><span class="label-text font-medium">Drop Points</span></label>
<input type="number" x-model.number="s['notifications.alert_compliance_drop_points']"
class="input input-bordered w-full" min="1" max="100" />
</div>
</div>
<template x-if="alertPreview !== null">
<div class="alert" :class="alertPreview > 0 ? 'alert-warning' : 'alert-success'">
<span x-text="alertPreview > 0 ? `${alertPreview} active alert${alertPreview === 1 ? '' : 's'} found.` : 'No active alerts found.'"></span>
</div>
</template>
</div>
<div class="flex flex-col sm:flex-row justify-end gap-2">
<button type="button" class="btn btn-outline btn-md" :disabled="saving || checkingAlerts" @click="checkAlerts()">
<template x-if="!checkingAlerts">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2">
<path d="M22 12h-4l-3 9L9 3l-3 9H2"></path>
</svg>
</template>
<template x-if="checkingAlerts"><span class="loading loading-spinner loading-xs mr-2"></span></template>
Check Alerts
</button>
<button type="button" class="btn btn-outline btn-md" :disabled="saving || sendingAlerts" @click="sendAlertSummary()">
<template x-if="!sendingAlerts">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2">
<path d="M10.268 21a2 2 0 0 0 3.464 0"></path>
<path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326"></path>
</svg>
</template>
<template x-if="sendingAlerts"><span class="loading loading-spinner loading-xs mr-2"></span></template>
Send Alerts Now
</button>
<button type="button" class="btn btn-outline btn-md" :disabled="saving || testingNotification" @click="sendTestNotification()">
<template x-if="!testingNotification">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none"
@@ -292,6 +385,9 @@ function settingsApp() {
flashMsg: '',
flashOk: true,
testingNotification: false,
checkingAlerts: false,
sendingAlerts: false,
alertPreview: null,
showCfToken: false,
// Session cookie is sent automatically by the browser (httpOnly, same-origin).
@@ -369,6 +465,51 @@ function settingsApp() {
}
},
async checkAlerts() {
this.checkingAlerts = true;
try {
await this.saveCategory('notifications');
const res = await fetch('/api/v1/settings/notifications/alerts', {
headers: this.apiHeaders(),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
this.showFlash('Alert check failed: ' + (data.detail || res.statusText), false);
} else {
this.alertPreview = (data.alerts || []).length;
this.showFlash(`${this.alertPreview} active alert${this.alertPreview === 1 ? '' : 's'} found.`, true);
}
} catch (err) {
this.showFlash('Error checking alerts: ' + err.message, false);
} finally {
this.checkingAlerts = false;
}
},
async sendAlertSummary() {
this.sendingAlerts = true;
try {
await this.saveCategory('notifications');
const res = await fetch('/api/v1/settings/notifications/alerts/send', {
method: 'POST',
headers: this.apiHeaders(),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
const detail = data.detail || {};
const notification = detail.notification || {};
this.showFlash('Alert send failed: ' + (notification.message || data.detail || res.statusText), false);
} else {
this.alertPreview = (data.alerts || []).length;
this.showFlash(data.notification.skipped ? 'No active alerts to send.' : 'Alert summary sent.', true);
}
} catch (err) {
this.showFlash('Error sending alerts: ' + err.message, false);
} finally {
this.sendingAlerts = false;
}
},
showFlash(msg, ok) {
this.flashMsg = msg;
this.flashOk = ok;