Add in-app Settings page for secrets; move Gmail creds out of source

- settings.py: JSON-on-volume store with env fallback; secrets never rendered.
- /settings page (⚙️ in nav) to set Gmail address + app password + reminder prefs;
  password fields are write-only (blank keeps existing).
- reminders.py reads Gmail creds from Settings instead of a hardcoded password
  (resolves #2 in code — note the old password is still in git history; rotate it).
- mac_notification no longer crashes off-macOS; FLASK_DEBUG gates debug (#7).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-06 16:38:20 -05:00
parent be74ca1eb7
commit 74d5e196c7
5 changed files with 159 additions and 8 deletions

View File

@@ -7,26 +7,41 @@ from email.mime.multipart import MIMEMultipart
from datetime import date, timedelta
import calendar as cal_module
from bills import load_bills
from settings import get_setting
EMAIL = "bonna61@gmail.com"
APP_PASSWORD = "qowu zcff dbtk kqbs"
def _gmail():
"""Resolve Gmail creds + recipient from Settings (env-var fallback)."""
email = get_setting("gmail_address", env="GMAIL_ADDRESS", default="")
pw = get_setting("gmail_app_password", env="GMAIL_APP_PASSWORD", default="")
recipient = get_setting("reminder_recipient", default="") or email
return email, pw, recipient
def send_email(subject, body_html, body_text):
email, pw, recipient = _gmail()
if not (email and pw):
print("Gmail not configured in Settings — skipping email reminder.")
return
msg = MIMEMultipart("alternative")
msg["Subject"] = subject
msg["From"] = f"Moon Household Budget <{EMAIL}>"
msg["To"] = EMAIL
msg["From"] = f"Moon Household Budget <{email}>"
msg["To"] = recipient
msg.attach(MIMEText(body_text, "plain"))
msg.attach(MIMEText(body_html, "html"))
with smtplib.SMTP_SSL("smtp.gmail.com", 465) as server:
server.login(EMAIL, APP_PASSWORD)
server.sendmail(EMAIL, EMAIL, msg.as_string())
server.login(email, pw)
server.sendmail(email, recipient, msg.as_string())
def mac_notification(title, message):
# macOS-only; no-op (don't crash) when osascript isn't available, e.g. in
# the Linux container.
script = f'display notification "{message}" with title "{title}" sound name "Default"'
subprocess.run(["osascript", "-e", script])
try:
subprocess.run(["osascript", "-e", script], check=False)
except (FileNotFoundError, OSError):
pass
def bills_due_in(days):