Add customizable email templates and remove alert recipients
All checks were successful
PostgreSQL Compatibility Matrix / PG14 smoke (push) Successful in 7s
PostgreSQL Compatibility Matrix / PG15 smoke (push) Successful in 8s
PostgreSQL Compatibility Matrix / PG16 smoke (push) Successful in 7s
PostgreSQL Compatibility Matrix / PG17 smoke (push) Successful in 7s
PostgreSQL Compatibility Matrix / PG18 smoke (push) Successful in 7s

Replaced the fixed `alert_recipients` list with customizable subject and body templates for alerts and warnings. This allows for more flexible and dynamic email notifications using placeholder variables. Updated relevant backend and frontend components to support this feature.
This commit is contained in:
2026-02-12 16:32:53 +01:00
parent e5a9acfa91
commit c437e72c2b
7 changed files with 204 additions and 35 deletions

View File

@@ -17,7 +17,10 @@ export function AdminUsersPage() {
from_email: "",
use_starttls: true,
use_ssl: false,
alert_recipients: "",
warning_subject_template: "",
alert_subject_template: "",
warning_body_template: "",
alert_body_template: "",
});
const [smtpState, setSmtpState] = useState({ has_password: false, updated_at: null });
const [testRecipient, setTestRecipient] = useState("");
@@ -42,7 +45,10 @@ export function AdminUsersPage() {
from_email: smtp.from_email || "",
use_starttls: !!smtp.use_starttls,
use_ssl: !!smtp.use_ssl,
alert_recipients: (smtp.alert_recipients || []).join(", "),
warning_subject_template: smtp.warning_subject_template || "",
alert_subject_template: smtp.alert_subject_template || "",
warning_body_template: smtp.warning_body_template || "",
alert_body_template: smtp.alert_body_template || "",
}));
setSmtpState({ has_password: !!smtp.has_password, updated_at: smtp.updated_at });
setTestRecipient(smtp.from_email || "");
@@ -79,10 +85,6 @@ export function AdminUsersPage() {
setError("");
setSmtpInfo("");
try {
const recipients = emailSettings.alert_recipients
.split(",")
.map((item) => item.trim())
.filter(Boolean);
const payload = {
...emailSettings,
smtp_host: emailSettings.smtp_host.trim() || null,
@@ -90,7 +92,10 @@ export function AdminUsersPage() {
from_name: emailSettings.from_name.trim() || null,
from_email: emailSettings.from_email.trim() || null,
smtp_password: emailSettings.smtp_password || null,
alert_recipients: recipients,
warning_subject_template: emailSettings.warning_subject_template.trim() || null,
alert_subject_template: emailSettings.alert_subject_template.trim() || null,
warning_body_template: emailSettings.warning_body_template.trim() || null,
alert_body_template: emailSettings.alert_body_template.trim() || null,
};
await apiFetch("/admin/settings/email", { method: "PUT", body: JSON.stringify(payload) }, tokens, refresh);
setSmtpInfo("SMTP settings saved.");
@@ -120,6 +125,19 @@ export function AdminUsersPage() {
}
};
const protocolMode = emailSettings.use_ssl ? "ssl" : emailSettings.use_starttls ? "starttls" : "plain";
const setProtocolMode = (mode) => {
if (mode === "ssl") {
setEmailSettings({ ...emailSettings, use_ssl: true, use_starttls: false });
return;
}
if (mode === "starttls") {
setEmailSettings({ ...emailSettings, use_ssl: false, use_starttls: true });
return;
}
setEmailSettings({ ...emailSettings, use_ssl: false, use_starttls: false });
};
return (
<div className="admin-settings-page">
<h2>Admin Settings</h2>
@@ -269,32 +287,74 @@ export function AdminUsersPage() {
/>
</div>
<div className="admin-field">
<label>Alert recipients (comma-separated)</label>
<input
value={emailSettings.alert_recipients}
placeholder="dba@example.com, oncall@example.com"
onChange={(e) => setEmailSettings({ ...emailSettings, alert_recipients: e.target.value })}
/>
<label>Transport mode</label>
<div className="smtp-mode-picker" role="radiogroup" aria-label="SMTP transport mode">
<button
type="button"
className={`smtp-mode-btn ${protocolMode === "starttls" ? "active" : ""}`}
aria-pressed={protocolMode === "starttls"}
onClick={() => setProtocolMode("starttls")}
>
STARTTLS
</button>
<button
type="button"
className={`smtp-mode-btn ${protocolMode === "ssl" ? "active" : ""}`}
aria-pressed={protocolMode === "ssl"}
onClick={() => setProtocolMode("ssl")}
>
SSL/TLS (SMTPS)
</button>
<button
type="button"
className={`smtp-mode-btn ${protocolMode === "plain" ? "active" : ""}`}
aria-pressed={protocolMode === "plain"}
onClick={() => setProtocolMode("plain")}
>
No TLS
</button>
</div>
<small className="muted">Select exactly one mode to avoid STARTTLS/SSL conflicts.</small>
</div>
<div className="admin-field">
<label>Template variables</label>
<small className="muted">
Use placeholders like: {"{target_name}"}, {"{alert_name}"}, {"{severity}"}, {"{description}"}, {"{message}"}, {"{value}"}, {"{warning_threshold}"}, {"{alert_threshold}"}, {"{checked_at}"}, {"{alert_key}"}
</small>
</div>
<label className="toggle-check">
<div className="admin-field field-full">
<label>Warning subject template</label>
<input
type="checkbox"
checked={emailSettings.use_starttls}
onChange={(e) => setEmailSettings({ ...emailSettings, use_starttls: e.target.checked, use_ssl: e.target.checked ? false : emailSettings.use_ssl })}
value={emailSettings.warning_subject_template}
placeholder="[NexaPG][WARNING] {target_name} - {alert_name}"
onChange={(e) => setEmailSettings({ ...emailSettings, warning_subject_template: e.target.value })}
/>
<span className="toggle-ui" />
<span>Use STARTTLS</span>
</label>
<label className="toggle-check">
</div>
<div className="admin-field field-full">
<label>Alert subject template</label>
<input
type="checkbox"
checked={emailSettings.use_ssl}
onChange={(e) => setEmailSettings({ ...emailSettings, use_ssl: e.target.checked, use_starttls: e.target.checked ? false : emailSettings.use_starttls })}
value={emailSettings.alert_subject_template}
placeholder="[NexaPG][ALERT] {target_name} - {alert_name}"
onChange={(e) => setEmailSettings({ ...emailSettings, alert_subject_template: e.target.value })}
/>
<span className="toggle-ui" />
<span>Use SSL/TLS (SMTPS)</span>
</label>
</div>
<div className="admin-field field-full">
<label>Warning body template</label>
<textarea
value={emailSettings.warning_body_template}
placeholder={"Severity: {severity}\nTarget: {target_name} (id={target_id})\nAlert: {alert_name}\nMessage: {message}\nChecked At: {checked_at}"}
onChange={(e) => setEmailSettings({ ...emailSettings, warning_body_template: e.target.value })}
/>
</div>
<div className="admin-field field-full">
<label>Alert body template</label>
<textarea
value={emailSettings.alert_body_template}
placeholder={"Severity: {severity}\nTarget: {target_name} (id={target_id})\nAlert: {alert_name}\nMessage: {message}\nCurrent Value: {value}\nWarning Threshold: {warning_threshold}\nAlert Threshold: {alert_threshold}\nChecked At: {checked_at}\nAlert Key: {alert_key}"}
onChange={(e) => setEmailSettings({ ...emailSettings, alert_body_template: e.target.value })}
/>
</div>
<label className="toggle-check field-full">
<input

View File

@@ -1127,6 +1127,37 @@ td {
min-height: 38px;
}
.admin-field textarea {
width: 100%;
min-height: 90px;
resize: vertical;
font-family: "JetBrains Mono", "Consolas", monospace;
font-size: 13px;
}
.smtp-mode-picker {
display: inline-flex;
gap: 8px;
flex-wrap: wrap;
}
.smtp-mode-btn {
min-height: 32px;
padding: 6px 10px;
border-radius: 999px;
border: 1px solid #3d6ca8;
background: linear-gradient(180deg, #123258, #0f2a4c);
color: #d4e8ff;
font-weight: 650;
font-size: 12px;
}
.smtp-mode-btn.active {
border-color: #57cbf8;
background: linear-gradient(180deg, #1a5f96, #164f7d);
box-shadow: 0 0 0 2px #28bdf32d;
}
.admin-users-table-wrap table tbody .admin-user-row td {
padding-top: 11px;
padding-bottom: 11px;