Add a daily Docker image-update notification

Dockhand's pending-update counts were only visible if you happened to
open the Docker page. New "Docker image update available" toggle
under Settings -> Notifications, sharing the same daily
time/timezone as the secret and Tailscale key expiry reminders (same
node-schedule reschedule-on-settings-change pattern as those two).
Reads each enabled Dockhand integration's already-cached update-check
results via listContainers() rather than triggering a fresh
per-container registry lookup, so it costs nothing extra beyond what
the Docker page itself already fetches, and lists every container
with an update pending across all environments/integrations in one
notification.

Verified end-to-end against a fake local Dockhand server (one
environment, two containers, one flagged with a pending update) and a
fake webhook receiver on an isolated scratch database: the check
correctly found only the flagged container (with its newerVersion),
sent exactly one notification with the right title/content, excluded
the up-to-date container, and sent nothing at all when the setting
was toggled off despite still finding the same pending update.
Confirmed the real dev database's mtime was untouched throughout.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
bobbanandClaude Sonnet 5 committed 2026-09-20 23:44:47 +02:00
1 parent 9c07718d2d
commit 1cae35a59e
7 files changed
+107 -3

No files matched your search

@@ -42,6 +42,7 @@ const DEFAULT_NOTIFICATIONS: NotificationEvents = {
dnsDelete: true,
secretCheck: true,
tailscaleKeyCheck: true,
dockerUpdateCheck: true,
secretCheckTime: "08:00",
timezone: "UTC",
integrationFailureAlerts: true,
@@ -491,6 +492,7 @@ export default function NotificationSettings() {
{ key: "dnsDelete" as const, label: "DNS record deleted" },
{ key: "secretCheck" as const, label: "Secret expiry reminder" },
{ key: "tailscaleKeyCheck" as const, label: "Tailscale key expiry reminder" },
{ key: "dockerUpdateCheck" as const, label: "Docker image update available" },
{ key: "integrationFailureAlerts" as const, label: "Integration/DNS provider failing repeatedly" },
].map(({ key, label }) => (
<label key={key} className="form-check mb-2">
@@ -524,7 +526,7 @@ export default function NotificationSettings() {
</div>
</div>
{(() => {
const dailyChecksEnabled = notifications.secretCheck || notifications.tailscaleKeyCheck;
const dailyChecksEnabled = notifications.secretCheck || notifications.tailscaleKeyCheck || notifications.dockerUpdateCheck;
return (
<div className="row g-2 mt-2">
<div className="col-6">
@@ -536,7 +538,7 @@ export default function NotificationSettings() {
disabled={!dailyChecksEnabled}
onChange={(e) => setNotifications((n) => ({ ...n, secretCheckTime: e.target.value }))}
/>
<div className="form-hint">Shared by the secret and Tailscale key expiry reminders above.</div>
<div className="form-hint">Shared by the secret, Tailscale key, and Docker update reminders above.</div>
</div>
<div className="col-6">
<label className="form-label">Timezone</label>