From 0984dae2e7bc78ec90cc26570a3ffe59d5a4b355 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Wed, 25 Feb 2026 20:33:10 -0500 Subject: [PATCH] Improve favicon contrast and redesign email templates with logo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit favicon.svg: - Light mode: replace near-black fill (#2d3748) with indigo brand color (#6366f1 fill, #4f46e5 stroke, #a5b4fc page lines) — distinctive and high-contrast without the dark/black appearance - Dark mode unchanged email.py: - Add _EMAIL_LOGO_SVG: inline SVG with white palette for rendering on the indigo header (white book, lavender lines, gold sparkle) - Add _email_html(title, body): shared template wrapper — gray outer background, white card with border-radius, indigo header with logo + app name, content area, footer notifications.py: - Import and use _email_html for all six email functions: security alert, password reset, password reset success, invitation, task reminder, test email - Clean up all inline HTML to match the new card layout and spacing Co-Authored-By: Claude Sonnet 4.6 --- frontend/public/favicon.svg | 6 +- src/fabledassistant/services/email.py | 74 ++++++-- src/fabledassistant/services/notifications.py | 175 ++++++++---------- 3 files changed, 142 insertions(+), 113 deletions(-) diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg index 4dadbff..3e9cc76 100644 --- a/frontend/public/favicon.svg +++ b/frontend/public/favicon.svg @@ -1,8 +1,8 @@ + +
+
+
+ + +
+
+ {_EMAIL_LOGO_SVG}Fabled Assistant +
+

{title}

+
+ + +
+ {body} +
+ + +
+

This email was sent by your Fabled Assistant instance.

+
+ +
+
+
+ +""" + + SMTP_SETTING_KEYS = [ "smtp_host", "smtp_port", @@ -109,15 +168,8 @@ async def send_email(to: str, subject: str, html_body: str) -> None: async def send_test_email(to: str) -> None: """Send a branded test email.""" - html = """ -
-
-

Fabled Assistant

-
-
-

Your SMTP configuration is working correctly.

-

This is a test email sent from your Fabled Assistant instance.

-
-
+ body = """ +

SMTP is configured correctly

+

Your Fabled Assistant instance can send email notifications.

""" - await send_email(to, "Fabled Assistant - Test Email", html) + await send_email(to, "Fabled Assistant - Test Email", _email_html("Test Email", body)) diff --git a/src/fabledassistant/services/notifications.py b/src/fabledassistant/services/notifications.py index 2bebe85..83d6e05 100644 --- a/src/fabledassistant/services/notifications.py +++ b/src/fabledassistant/services/notifications.py @@ -12,7 +12,7 @@ from fabledassistant.models.app_log import AppLog from fabledassistant.models.note import Note from fabledassistant.models.setting import Setting from fabledassistant.models.user import User -from fabledassistant.services.email import is_smtp_configured, send_email +from fabledassistant.services.email import _email_html, is_smtp_configured, send_email from fabledassistant.services.logging import log_audit logger = logging.getLogger(__name__) @@ -69,100 +69,84 @@ async def notify_security_event( for k, v in details.items(): if k == "ip_address": continue - detail_rows += f'{k}{v}' + detail_rows += ( + f'' + f'{k}' + f'{v}' + f'' + ) - html = f""" -
-
-

Security Alert

-
-
-

{label}

- - - - {detail_rows} -
Time{timestamp}
IP Address{ip}
-

If this wasn't you, change your password immediately.

-
-
+ body = f""" +

{label}

+ + + + + + + + + + {detail_rows} +
Time{timestamp}
IP Address{ip}
+

If this wasn't you, change your password immediately.

""" - await send_email(email, f"Fabled Assistant - {label}", html) + await send_email(email, f"Fabled Assistant - {label}", _email_html("Security Alert", body)) except Exception: logger.exception("Failed to send security notification for user %d", user_id) async def send_password_reset_email(email: str, reset_url: str) -> None: """Send a password reset email with a link to reset the user's password.""" - html = f""" -
-
-

Password Reset

+ body = f""" +

+ We received a request to reset your password. Click the button below to choose a new password. +

+
+ + Reset Password +
-
-

- We received a request to reset your password. Click the button below to choose a new password: -

- -

This link expires in 1 hour.

-

If you didn't request this, you can safely ignore this email.

-
-

If the button doesn't work, copy and paste this link into your browser:

-

{reset_url}

-
-
+

This link expires in 1 hour.

+

If you didn't request this, you can safely ignore this email.

+
+

If the button doesn't work, copy and paste this link:

+

{reset_url}

""" - await send_email(email, "Fabled Assistant - Password Reset", html) + await send_email(email, "Fabled Assistant - Password Reset", _email_html("Password Reset", body)) async def send_password_reset_success_email(email: str) -> None: """Send a notification that the password was successfully reset.""" timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC") - html = f""" -
-
-

Password Changed

-
-
-

- Your password was successfully reset at {timestamp}. -

-

If you didn't make this change, please contact your administrator immediately.

-
-
+ body = f""" +

+ Your password was successfully changed at {timestamp}. +

+

If you didn't make this change, please contact your administrator immediately.

""" - await send_email(email, "Fabled Assistant - Password Changed", html) + await send_email(email, "Fabled Assistant - Password Changed", _email_html("Password Changed", body)) async def send_invitation_email(email: str, invite_url: str, invited_by_username: str) -> None: """Send a branded invitation email with a registration link.""" - html = f""" -
-
-

You're Invited!

+ body = f""" +

+ {invited_by_username} has invited you to join Fabled Assistant. + Click the button below to create your account. +

+ -
-

- {invited_by_username} has invited you to join Fabled Assistant. Click the button below to create your account: -

- -

This invitation expires in 7 days.

-

If you weren't expecting this invitation, you can safely ignore this email.

-
-

If the button doesn't work, copy and paste this link into your browser:

-

{invite_url}

-
-
+

This invitation expires in 7 days.

+

If you weren't expecting this, you can safely ignore this email.

+
+

If the button doesn't work, copy and paste this link:

+

{invite_url}

""" - await send_email(email, "Fabled Assistant - You're Invited!", html) + await send_email(email, "Fabled Assistant - You're Invited!", _email_html("You're Invited!", body)) async def check_due_tasks() -> None: @@ -221,34 +205,27 @@ async def check_due_tasks() -> None: overdue = task.due_date < today if task.due_date else False date_color = "#ef4444" if overdue else "#374151" date_label = f'{task.due_date.isoformat()}' if task.due_date else "" - overdue_badge = ' (overdue)' if overdue else "" - task_rows += f""" - - {task.title} - {date_label}{overdue_badge} - {task.status} - - """ + overdue_badge = ' (overdue)' if overdue else "" + task_rows += ( + f'' + f'{task.title}' + f'{date_label}{overdue_badge}' + f'{task.status}' + f'' + ) - html = f""" -
-
-

Task Reminders

-
-
-

You have {len(user_tasks)} task(s) due:

- - - - - - - {task_rows} -
TaskDueStatus
-
-
+ body = f""" +

You have {len(user_tasks)} task(s) due:

+ + + + + + + {task_rows} +
TaskDueStatus
""" - await send_email(email, f"Fabled Assistant - {len(user_tasks)} Task(s) Due", html) + await send_email(email, f"Fabled Assistant - {len(user_tasks)} Task(s) Due", _email_html("Task Reminders", body)) await log_audit( "task_reminder", user_id=user_id,