feat: add notification alert rules
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user