Notify on expiring Tailscale device keys
The Tailscale page already showed per-device key expiry; extend the existing daily-reminder infrastructure (currently only for Secrets) to push it out through the configured notification channels too, the same way expiring secrets already are. New tailscaleKeyExpiryScheduler.ts mirrors secretExpiryScheduler.ts: runs once at startup (skipped if already run today) and daily thereafter, checking every enabled Tailscale integration's devices for keys expiring within the warning window and calling notify() with the results. Reuses the exact same daily time/timezone setting as the secret-expiry check (one "Daily reminder time" control, two independent on/off toggles) rather than adding a second schedule for users to configure. Centralized the expiring-soon threshold and check (previously only duplicated in the /synology and /tailscale route summaries) into adapter.ts as `KEY_EXPIRY_WARN_DAYS` / `isKeyExpiringSoon()`, and updated the devices route to use it instead of its own inline copy. New `tailscaleKeyCheck` notification-event toggle (default on) in settings, alongside the existing secret-expiry one. Verified end-to-end against a temp SQLite DB + real migrations: a tailscale integration pointed at a mock Tailscale API (one device expiring in 10 days, one with key-expiry disabled) with the webhook channel enabled and pointed at a mock receiver — confirmed the scheduler's startup check queries the DB correctly, decrypts the integration's credential, calls the adapter, filters out the disabled-expiry device, and delivers a webhook payload naming only the expiring device. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c0af546d08
commit
1ff59afb40
10 files changed
+147
-39
No files matched your search
@@ -41,6 +41,7 @@ const DEFAULT_NOTIFICATIONS: NotificationEvents = {
|
||||
dnsUpdate: true,
|
||||
dnsDelete: true,
|
||||
secretCheck: true,
|
||||
tailscaleKeyCheck: true,
|
||||
secretCheckTime: "08:00",
|
||||
timezone: "UTC",
|
||||
};
|
||||
@@ -487,6 +488,7 @@ export default function NotificationSettings() {
|
||||
{ key: "dnsUpdate" as const, label: "DNS record updated" },
|
||||
{ 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" },
|
||||
].map(({ key, label }) => (
|
||||
<label key={key} className="form-check form-check-single mb-2">
|
||||
<input
|
||||
@@ -498,33 +500,39 @@ export default function NotificationSettings() {
|
||||
<span className="form-check-label">{label}</span>
|
||||
</label>
|
||||
))}
|
||||
<div className="row g-2 mt-2">
|
||||
<div className="col-6">
|
||||
<label className="form-label">Daily reminder time</label>
|
||||
<input
|
||||
type="time"
|
||||
className="form-control"
|
||||
value={notifications.secretCheckTime}
|
||||
disabled={!notifications.secretCheck}
|
||||
onChange={(e) => setNotifications((n) => ({ ...n, secretCheckTime: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-6">
|
||||
<label className="form-label">Timezone</label>
|
||||
<select
|
||||
className="form-select"
|
||||
value={notifications.timezone}
|
||||
disabled={!notifications.secretCheck}
|
||||
onChange={(e) => setNotifications((n) => ({ ...n, timezone: e.target.value }))}
|
||||
>
|
||||
{TIMEZONES.map((tz) => (
|
||||
<option key={tz} value={tz}>
|
||||
{tz}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{(() => {
|
||||
const dailyChecksEnabled = notifications.secretCheck || notifications.tailscaleKeyCheck;
|
||||
return (
|
||||
<div className="row g-2 mt-2">
|
||||
<div className="col-6">
|
||||
<label className="form-label">Daily reminder time</label>
|
||||
<input
|
||||
type="time"
|
||||
className="form-control"
|
||||
value={notifications.secretCheckTime}
|
||||
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>
|
||||
<div className="col-6">
|
||||
<label className="form-label">Timezone</label>
|
||||
<select
|
||||
className="form-select"
|
||||
value={notifications.timezone}
|
||||
disabled={!dailyChecksEnabled}
|
||||
onChange={(e) => setNotifications((n) => ({ ...n, timezone: e.target.value }))}
|
||||
>
|
||||
{TIMEZONES.map((tz) => (
|
||||
<option key={tz} value={tz}>
|
||||
{tz}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user