Add experimental server alerts and webhook notifications
Release rolling / release (push) Successful in 7m30s

Server-side alerting for Docker/container/stack health with Discord,
Slack, Teams, ntfy, Gotify, Telegram, and generic webhooks. Settings
tab, client cache, and backup/restore coverage; marked experimental.
This commit is contained in:
Raven Scott
2026-07-17 18:10:43 -04:00
parent 366333e6bd
commit 5b8fb142bb
18 changed files with 2886 additions and 19 deletions
+261 -5
View File
@@ -2682,7 +2682,7 @@ services:
<button type="button" class="nav-link" data-settings-tab="jobs" role="tab">Jobs tray</button>
</li>
<li class="nav-item" role="presentation">
<button type="button" class="nav-link" data-settings-tab="notifications" role="tab">Notifications</button>
<button type="button" class="nav-link" data-settings-tab="alerts" role="tab">Alerts &amp; Notifications <span class="badge text-bg-secondary ms-1" style="font-size:0.65em;vertical-align:middle;">Experimental</span></button>
</li>
<li class="nav-item" role="presentation">
<button type="button" class="nav-link" data-settings-tab="tunnels" role="tab">Tunnels</button>
@@ -2915,10 +2915,11 @@ services:
</div>
</div>
<!-- Notifications -->
<div class="settings-panel hidden" id="settings-panel-notifications" data-settings-panel="notifications">
<!-- Alerts & Notifications (local bell + server webhooks) -->
<div class="settings-panel hidden" id="settings-panel-alerts" data-settings-panel="alerts">
<div class="settings-section">
<h3>Bell &amp; history</h3>
<h3>Local bell &amp; history</h3>
<p class="small text-muted mb-3">In-app notification center on this client (not webhooks).</p>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label" for="settings-badge-mode">Bell badge</label>
@@ -2947,6 +2948,255 @@ services:
</button>
</div>
</div>
<div class="settings-section mt-4">
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mb-2">
<div>
<h3 class="mb-0">Server alerts &amp; webhooks <span class="badge text-bg-secondary">Experimental</span></h3>
<p class="small text-muted mb-0">
<strong>Experimental.</strong> Runs on the PearDock <strong>server</strong> — Discord, Slack, Teams, ntfy, Gotify, Telegram, or generic HTTPS.
Fires even when this UI is closed. Requires an admin session. Channels, rules, and secrets are backed up in client packages when included.
</p>
</div>
<div class="d-flex align-items-center gap-2">
<span id="alerts-engine-status" class="small text-muted"></span>
<button type="button" class="btn btn-outline-secondary btn-sm" id="alerts-refresh" title="Reload from server">
<i class="fas fa-sync-alt"></i>
</button>
</div>
</div>
<div id="alerts-offline-hint" class="alert alert-warning py-2 small hidden">
Connect to a server to configure webhook channels and rules.
</div>
<div class="row g-3 mt-1">
<div class="col-md-3">
<label class="form-label" for="alerts-enabled">Alert engine</label>
<select id="alerts-enabled" class="form-select bg-dark text-white">
<option value="1">Enabled</option>
<option value="0">Disabled</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label" for="alerts-min-severity">Global min severity</label>
<select id="alerts-min-severity" class="form-select bg-dark text-white">
<option value="info">Info+</option>
<option value="warning">Warning+</option>
<option value="critical">Critical only</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label" for="alerts-poll-ms">Health poll (sec)</label>
<input type="number" id="alerts-poll-ms" class="form-control bg-dark text-white" min="15" max="3600" value="60">
</div>
<div class="col-md-2">
<label class="form-label" for="alerts-rate-limit">Max / minute</label>
<input type="number" id="alerts-rate-limit" class="form-control bg-dark text-white" min="1" max="300" value="40">
</div>
<div class="col-md-2">
<label class="form-label" for="alerts-include-hostname">Include hostname</label>
<select id="alerts-include-hostname" class="form-select bg-dark text-white">
<option value="1">Yes</option>
<option value="0">No</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label" for="alerts-quiet-enabled">Quiet hours</label>
<select id="alerts-quiet-enabled" class="form-select bg-dark text-white">
<option value="0">Off</option>
<option value="1">On (critical still fires)</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label" for="alerts-quiet-start">Quiet start</label>
<input type="text" id="alerts-quiet-start" class="form-control bg-dark text-white" placeholder="22:00" value="22:00">
</div>
<div class="col-md-2">
<label class="form-label" for="alerts-quiet-end">Quiet end</label>
<input type="text" id="alerts-quiet-end" class="form-control bg-dark text-white" placeholder="07:00" value="07:00">
</div>
<div class="col-md-3 d-flex align-items-end">
<button type="button" class="btn btn-primary btn-sm" id="alerts-save-global" data-min-role="admin">
<i class="fas fa-save me-1"></i>Save engine settings
</button>
</div>
</div>
<div id="alerts-status" class="small text-muted mt-2" role="status"></div>
</div>
<div class="settings-section mt-4">
<h3>Webhook channels</h3>
<p class="small text-muted">Delivery targets. Secrets are stored only on the server (redacted in the UI after save).</p>
<div id="alerts-channels-list" class="list-group mb-3"></div>
<div class="card bg-dark border-secondary">
<div class="card-body">
<h4 class="h6 card-title">Add / edit channel</h4>
<input type="hidden" id="alert-ch-id" value="">
<div class="row g-2">
<div class="col-md-4">
<label class="form-label" for="alert-ch-name">Name</label>
<input type="text" id="alert-ch-name" class="form-control bg-dark text-white" placeholder="Ops Discord">
</div>
<div class="col-md-3">
<label class="form-label" for="alert-ch-type">Type</label>
<select id="alert-ch-type" class="form-select bg-dark text-white">
<option value="discord">Discord</option>
<option value="slack">Slack</option>
<option value="teams">Microsoft Teams</option>
<option value="generic">Generic webhook</option>
<option value="ntfy">ntfy</option>
<option value="gotify">Gotify</option>
<option value="telegram">Telegram</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label" for="alert-ch-enabled">Enabled</label>
<select id="alert-ch-enabled" class="form-select bg-dark text-white">
<option value="1">Yes</option>
<option value="0">No</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label" for="alert-ch-min-sev">Min severity</label>
<select id="alert-ch-min-sev" class="form-select bg-dark text-white">
<option value="info">Info+</option>
<option value="warning">Warning+</option>
<option value="critical">Critical only</option>
</select>
</div>
<div class="col-md-8">
<label class="form-label" for="alert-ch-url">Webhook / server URL</label>
<input type="url" id="alert-ch-url" class="form-control bg-dark text-white" placeholder="https://…">
<div class="form-text" id="alert-ch-url-help"></div>
</div>
<div class="col-md-4">
<label class="form-label" for="alert-ch-username">Display name</label>
<input type="text" id="alert-ch-username" class="form-control bg-dark text-white" value="PearDock">
</div>
<div class="col-md-4 hidden" id="alert-ch-token-row">
<label class="form-label" for="alert-ch-token">Token</label>
<input type="password" id="alert-ch-token" class="form-control bg-dark text-white" autocomplete="off">
</div>
<div class="col-md-4 hidden" id="alert-ch-topic-row">
<label class="form-label" for="alert-ch-topic">ntfy topic</label>
<input type="text" id="alert-ch-topic" class="form-control bg-dark text-white" placeholder="peardock">
</div>
<div class="col-12 hidden" id="alert-ch-telegram-row">
<div class="row g-2">
<div class="col-md-6">
<label class="form-label" for="alert-ch-bot-token">Telegram bot token</label>
<input type="password" id="alert-ch-bot-token" class="form-control bg-dark text-white" autocomplete="off">
</div>
<div class="col-md-6">
<label class="form-label" for="alert-ch-chat-id">Telegram chat ID</label>
<input type="text" id="alert-ch-chat-id" class="form-control bg-dark text-white">
</div>
</div>
</div>
</div>
<div class="mt-3 d-flex gap-2">
<button type="button" class="btn btn-primary btn-sm" id="alert-ch-save" data-min-role="admin">Save channel</button>
<button type="button" class="btn btn-outline-secondary btn-sm" id="alert-ch-reset">Clear form</button>
</div>
</div>
</div>
</div>
<div class="settings-section mt-4">
<h3>Alert rules</h3>
<p class="small text-muted">
What triggers a notification. Built-in rules cover container die/OOM, healthchecks, Docker daemon down,
compose stack degradation, restarts, and optional disk pressure. Fully editable.
</p>
<div id="alerts-rules-list" class="list-group mb-3"></div>
<div class="card bg-dark border-secondary">
<div class="card-body">
<h4 class="h6 card-title">Add / edit rule</h4>
<input type="hidden" id="alert-rule-id" value="">
<div class="row g-2">
<div class="col-md-4">
<label class="form-label" for="alert-rule-name">Name</label>
<input type="text" id="alert-rule-name" class="form-control bg-dark text-white" placeholder="Container died">
</div>
<div class="col-md-3">
<label class="form-label" for="alert-rule-kind">Kind</label>
<select id="alert-rule-kind" class="form-select bg-dark text-white">
<option value="docker_event">Docker event</option>
<option value="container_health">Container health</option>
<option value="docker_daemon">Docker daemon</option>
<option value="stack_health">Stack / compose health</option>
<option value="resource">Host resource (disk)</option>
<option value="peardock">Manual / PearDock</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label" for="alert-rule-severity">Severity</label>
<select id="alert-rule-severity" class="form-select bg-dark text-white">
<option value="info">Info</option>
<option value="warning">Warning</option>
<option value="critical">Critical</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label" for="alert-rule-enabled">Enabled</label>
<select id="alert-rule-enabled" class="form-select bg-dark text-white">
<option value="1">Yes</option>
<option value="0">No</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label" for="alert-rule-types">Event types</label>
<input type="text" id="alert-rule-types" class="form-control bg-dark text-white" placeholder="container" value="container">
</div>
<div class="col-md-3">
<label class="form-label" for="alert-rule-actions">Actions</label>
<input type="text" id="alert-rule-actions" class="form-control bg-dark text-white" placeholder="die, oom, kill">
</div>
<div class="col-md-3">
<label class="form-label" for="alert-rule-health">Health statuses</label>
<input type="text" id="alert-rule-health" class="form-control bg-dark text-white" placeholder="unhealthy">
</div>
<div class="col-md-3">
<label class="form-label" for="alert-rule-disk">Disk % threshold</label>
<input type="number" id="alert-rule-disk" class="form-control bg-dark text-white" min="1" max="100" placeholder="90">
</div>
<div class="col-md-3">
<label class="form-label" for="alert-rule-cooldown">Cooldown (sec)</label>
<input type="number" id="alert-rule-cooldown" class="form-control bg-dark text-white" min="0" value="300">
</div>
<div class="col-md-3">
<label class="form-label" for="alert-rule-recover">Notify on recover</label>
<select id="alert-rule-recover" class="form-select bg-dark text-white">
<option value="1">Yes</option>
<option value="0">No</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label" for="alert-rule-name-regex">Name regex filter</label>
<input type="text" id="alert-rule-name-regex" class="form-control bg-dark text-white" placeholder="^prod-">
</div>
<div class="col-md-4">
<label class="form-label" for="alert-rule-name-include">Name must include</label>
<input type="text" id="alert-rule-name-include" class="form-control bg-dark text-white" placeholder="api, worker">
</div>
<div class="col-md-4">
<label class="form-label" for="alert-rule-name-exclude">Name must exclude</label>
<input type="text" id="alert-rule-name-exclude" class="form-control bg-dark text-white" placeholder="dev-, test-">
</div>
</div>
<div class="mt-3 d-flex gap-2">
<button type="button" class="btn btn-primary btn-sm" id="alert-rule-save" data-min-role="admin">Save rule</button>
<button type="button" class="btn btn-outline-secondary btn-sm" id="alert-rule-reset">Clear form</button>
</div>
</div>
</div>
</div>
<div class="settings-section mt-4">
<h3>Recent deliveries</h3>
<p class="small text-muted">Last alerts fired by this server (success and failures).</p>
<div id="alerts-history-list" class="list-group" style="max-height: 22rem; overflow: auto;"></div>
</div>
</div>
<!-- Tunnels -->
@@ -3194,9 +3444,15 @@ services:
<label class="form-check-label" for="settings-backup-inc-backupPolicy">Backup retention policy</label>
</div>
</div>
<div class="col-md-4 col-sm-6">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="settings-backup-inc-alerts" data-backup-section="alerts" checked>
<label class="form-check-label" for="settings-backup-inc-alerts">Server alerts &amp; webhooks</label>
</div>
</div>
</div>
<p class="form-text text-muted small mt-2 mb-0">
Packages include settings (theme, filters, columns, fleet tags, …), peers, templates, notifications,
Packages include settings, peers, templates, notifications, alerts/webhooks (channels, rules, secrets),
and optionally the client DHT identity seed. Store backup files securely.
</p>
</div>