Add SMTP email: password reset + per-frame battery-threshold alerts
Build and push server image / build-and-push (push) Successful in 40s

Admin-configured SMTP (server/port/username/password/from address/
STARTTLS, a singleton server_settings row set from /admin -- not env
vars, since it's operator infrastructure a household admin sets up
once through the UI) powers two features, both requiring the relevant
user to have an email set in their own Settings:

- "Forgot password?" on /login emails a one-hour single-use reset link
  (password_reset_tokens table). The endpoint always returns the same
  generic "check your email" response regardless of whether the address
  matched an account, so it can't be used to enumerate registered users.
- A frame's Configuration tab can set a battery-alert threshold
  (Frame.battery_alert_threshold_pct, -1 = disabled); POST /frame/battery
  emails the owner the first time a report drops to or below it, then
  stays quiet for the rest of that discharge cycle (battery_alert_sent,
  reset alongside battery_history whenever the existing recharge-jump
  detection fires) -- not once per wake.

New app/mail.py wraps stdlib smtplib (no new dependency); send_email()
never raises, so a broken mail server can't 500 a battery report or a
password-reset request. Schema migration v2 adds users.email and the
two frame columns via ALTER TABLE (safe against the live, already-
populated database) plus the two new tables via the existing
create_all-based migration runner.

Verified against a real (already-migrated, real user/frame data)
database: the v1->v2 migration, admin SMTP config + test-email button,
full forgot/reset-password roundtrip (including single-use token
invalidation and the no-enumeration response), and the battery alert
firing exactly once per crossing against a hand-rolled fake SMTP
server -- all via curl end-to-end, plus the standing legacy-device
curl suite to confirm the device protocol is untouched.
This commit is contained in:
2026-07-22 00:51:54 -04:00
parent a45444ab4b
commit 8e10ca540e
15 changed files with 456 additions and 8 deletions
+20
View File
@@ -66,6 +66,26 @@ async function loadControl() {
document.getElementById('take-control').addEventListener('click', takeControl);
// ---- Battery alerts card ----
document.getElementById('battery-alert-save').addEventListener('click', async () => {
const raw = document.getElementById('battery_alert_threshold_pct').value.trim();
const body = new URLSearchParams({
battery_alert_threshold_pct: raw === '' ? '-1' : raw,
});
try {
const resp = await fetch(`${window.FRAME_API}/config`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body,
});
if (!resp.ok) throw new Error(await apiError(resp));
showStatus(true, 'Saved.');
} catch (e) {
showStatus(false, e.message);
}
});
// ---- Firmware card ----
document.getElementById('firmware-upload').addEventListener('click', async () => {