[glm-grade=B] fix(notifications): repair UI/API contract drift — SMTP auth, event gate, test button (DC-092)

Three user-facing notification features were silently dead from contract
drift between the settings UI and the backend:

1. SMTP auth never applied: the UI sent email.user/email.pass while the
   manager read username/password. Route now normalizes aliases; legacy
   config files canonicalize at load.
2. Event toggles were cosmetic: UI sent camelCase keys (containerDown),
   the send() gate read kebab-case ('container-down'). EVENT_ALIASES now
   folds every known spelling (manager gate, route store, legacy files);
   UI sends and reads canonical keys.
3. Deploy/auto-restart notifications always dropped: deploy-success/
   deploy-failed/auto-restart were missing from DEFAULT events, and
   send('test') was itself gated -> the Test button was a no-op. Defaults
   added; 'test' bypasses the gate.

Also: strict boolean contract (string 'false' for secure/enabled rejected
— previously coerced truthy, silently forcing TLS), SMTP port bounds,
non-destructive credential merge (blank password no longer clobbers the
stored one), GET /config returns port/secure/to/username + hasPassword
(password never returned), full form prefill + keep-hint placeholder.

Verified live pre-fix: send('test')/'deploymentSuccess'/'auto-restart'
all returned 'not enabled'. Post-fix: +20 tests (route + manager),
full suite 2641/2641 (112 suites).

Judge: GLM-5.3 cold read via delegate_task (deleg_ad765d63), grade B,
ship, zero blockers; judge independently ran the DC-092 suites (38/38).
Polish #1 (canonical event for provider titles) folded in. Remaining
emitters with the same gate-miss class (recipeRemoved, workflow,
ssl-cert-expiry, dns-propagation, drift-detected, dependency-restart-*)
noted for a follow-up tick.
This commit is contained in:
Hermes
2026-08-22 18:16:47 -07:00
parent df55677bd1
commit 1328cfda6b
7 changed files with 620 additions and 92 deletions
+30 -14
View File
@@ -240,9 +240,23 @@
document.getElementById('ntfy-server').value = config.providers.ntfy.serverUrl;
}
// email fields
// email fields — DC-092: prefill the FULL form so a save doesn't
// silently wipe fields the GET response previously omitted. Password
// is never returned; when one is stored the field shows a keep-hint
// and an empty submit preserves the stored credential server-side.
if (config.providers?.email?.host) document.getElementById('email-host').value = config.providers.email.host;
if (config.providers?.email?.from) document.getElementById('email-from').value = config.providers.email.from;
if (config.providers?.email?.to) document.getElementById('email-to').value = config.providers.email.to;
if (config.providers?.email?.port) document.getElementById('email-port').value = config.providers.email.port;
if (config.providers?.email?.secure !== undefined) document.getElementById('email-secure').checked = config.providers.email.secure === true;
if (config.providers?.email?.username) document.getElementById('email-user').value = config.providers.email.username;
const emailPassEl = document.getElementById('email-pass');
if (config.providers?.email?.hasPassword) {
emailPassEl.value = '';
emailPassEl.placeholder = 'saved — leave blank to keep';
} else {
emailPassEl.placeholder = 'app password';
}
// Health check
document.getElementById('health-check-enabled').checked = config.healthCheck?.enabled || false;
@@ -254,12 +268,14 @@
`Last check: ${new Date(config.healthCheck.lastCheck).toLocaleString()}`;
}
// Events
document.getElementById('event-container-down').checked = config.events?.containerDown !== false;
document.getElementById('event-container-up').checked = config.events?.containerUp !== false;
document.getElementById('event-deploy-success').checked = config.events?.deploymentSuccess !== false;
document.getElementById('event-deploy-failed').checked = config.events?.deploymentFailed !== false;
document.getElementById('event-resource-alert').checked = config.events?.resourceAlert !== false;
// Events — canonical kebab-case keys, matching the backend store
// (DC-092: previously read camelCase keys that never existed, so
// every toggle re-rendered as 'checked' regardless of stored state).
document.getElementById('event-container-down').checked = config.events?.['container-down'] !== false;
document.getElementById('event-container-up').checked = config.events?.['container-up'] === true;
document.getElementById('event-deploy-success').checked = config.events?.['deploy-success'] !== false;
document.getElementById('event-deploy-failed').checked = config.events?.['deploy-failed'] !== false;
document.getElementById('event-resource-alert').checked = config.events?.['alert'] !== false;
}
} catch (error) {
errorHandler.logError('[Notifications] Load Config', error, { function: 'loadConfig' });
@@ -325,18 +341,18 @@
host: document.getElementById('email-host').value.trim(),
port: parseInt(document.getElementById('email-port').value) || 587,
secure: document.getElementById('email-secure').checked,
user: document.getElementById('email-user').value.trim(),
pass: document.getElementById('email-pass').value.trim(),
username: document.getElementById('email-user').value.trim(),
password: document.getElementById('email-pass').value.trim(),
from: document.getElementById('email-from').value.trim(),
to: document.getElementById('email-to').value.trim()
}
},
events: {
containerDown: document.getElementById('event-container-down').checked,
containerUp: document.getElementById('event-container-up').checked,
deploymentSuccess: document.getElementById('event-deploy-success').checked,
deploymentFailed: document.getElementById('event-deploy-failed').checked,
resourceAlert: document.getElementById('event-resource-alert').checked
'container-down': document.getElementById('event-container-down').checked,
'container-up': document.getElementById('event-container-up').checked,
'deploy-success': document.getElementById('event-deploy-success').checked,
'deploy-failed': document.getElementById('event-deploy-failed').checked,
'alert': document.getElementById('event-resource-alert').checked
},
healthCheck: {
enabled: document.getElementById('health-check-enabled').checked,