From caa6aa76d6ec85b0568b6c40c586a28910dfe8a1 Mon Sep 17 00:00:00 2001 From: Rephl3x Date: Tue, 3 Mar 2026 17:20:19 +1300 Subject: [PATCH] Fix email template rendering for Outlook-safe branded content --- .build_number | 2 +- backend/app/build_info.py | 4 +- backend/app/services/invite_email.py | 105 ++++++++++++++------------- frontend/app/admin/SettingsPage.tsx | 16 ++-- frontend/package-lock.json | 4 +- frontend/package.json | 2 +- 6 files changed, 67 insertions(+), 66 deletions(-) diff --git a/.build_number b/.build_number index 5e67e6d..e41d14f 100644 --- a/.build_number +++ b/.build_number @@ -1 +1 @@ -0303261702 +0303261719 diff --git a/backend/app/build_info.py b/backend/app/build_info.py index af83eb2..3f5a023 100644 --- a/backend/app/build_info.py +++ b/backend/app/build_info.py @@ -1,2 +1,2 @@ -BUILD_NUMBER = "0303261702" -CHANGELOG = '2026-03-03|Add branded HTML email templates\n2026-03-03|Add SMTP receipt logging for Exchange relay tracing\n2026-03-03|Fix shared request access and Jellyfin-ready pipeline status\n2026-03-03|Process 1 build 0303261507\n2026-03-03|Improve SQLite batching and diagnostics visibility\n2026-03-03|Add login page visibility controls\n2026-03-03|Hotfix: expand landing-page search to all requests\n2026-03-02|Hotfix: add logged-out password reset flow\n2026-03-02|Process 1 build 0203261953\n2026-03-02|Process 1 build 0203261610\n2026-03-02|Process 1 build 0203261608\n2026-03-02|Add dedicated profile invites page and fix mobile admin layout\n2026-03-01|Persist Seerr media failure suppression and reduce sync error noise\n2026-03-01|Add repository line ending policy\n2026-03-01|Finalize diagnostics, logging controls, and email test support\n2026-03-01|Add invite email templates and delivery workflow\n2026-02-28|Finalize dev-1.3 upgrades and Seerr updates\n2026-02-27|admin docs and layout refresh, build 2702261314\n2026-02-27|Build 2702261153: fix jellyfin sync user visibility\n2026-02-26|Build 2602262241: live request page updates\n2026-02-26|Build 2602262204\n2026-02-26|Build 2602262159: restore jellyfin-first user source\n2026-02-26|Build 2602262049: split magent settings and harden local login\n2026-02-26|Build 2602262030: add magent settings and hardening\n2026-02-26|Build 2602261731: fix user resync after nuclear wipe\n2026-02-26|Build 2602261717: master invite policy and self-service invite controls\n2026-02-26|Build 2602261636: self-service invites and count fixes\n2026-02-26|Build 2602261605: invite trace and cross-system user lifecycle\n2026-02-26|Build 2602261536: refine invite layouts and tighten UI\n2026-02-26|Build 2602261523: live updates, invite cleanup and nuclear resync\n2026-02-26|Build 2602261442: tidy users and invite layouts\n2026-02-26|Build 2602261409: unify invite management controls\n2026-02-26|Build 2602260214: invites profiles and expiry admin controls\n2026-02-26|Build 2602260022: enterprise UI refresh and users bulk auto-search\n2026-02-25|Build 2502262321: fix auto-search quality and per-user toggle\n2026-02-02|Build 0202261541: allow FQDN service URLs\n2026-01-30|Build 3001262148: single container\n2026-01-29|Build 2901262244: format changelog\n2026-01-29|Build 2901262240: cache users\n2026-01-29|Tidy full changelog\n2026-01-29|Update full changelog\n2026-01-29|Bake build number and changelog\n2026-01-29|Hardcode build number in backend\n2026-01-29|release: 2901262102\n2026-01-29|release: 2901262044\n2026-01-29|release: 2901262036\n2026-01-27|Hydrate missing artwork from Jellyseerr (build 271261539)\n2026-01-27|Fallback to TMDB when artwork cache fails (build 271261524)\n2026-01-27|Add service test buttons (build 271261335)\n2026-01-27|Bump build number (process 2) 271261322\n2026-01-27|Add cache load spinner (build 271261238)\n2026-01-27|Fix snapshot title fallback (build 271261228)\n2026-01-27|Fix request titles in snapshots (build 271261219)\n2026-01-27|Bump build number to 271261202\n2026-01-27|Clarify request sync settings (build 271261159)\n2026-01-27|Fix backend cache stats import (build 271261149)\n2026-01-27|Improve cache stats performance (build 271261145)\n2026-01-27|Add cache control artwork stats\n2026-01-26|Fix sync progress bar animation\n2026-01-26|Fix cache title hydration\n2026-01-25|Build 2501262041\n2026-01-25|Harden request cache titles and cache-only reads\n2026-01-25|Serve bundled branding assets by default\n2026-01-25|Seed branding logo from bundled assets\n2026-01-25|Tidy request sync controls\n2026-01-25|Add Jellyfin login cache and admin-only stats\n2026-01-25|Add user stats and activity tracking\n2026-01-25|Move account actions into avatar menu\n2026-01-25|Improve mobile header layout\n2026-01-25|Automate build number tagging and sync\n2026-01-25|Add site banner, build number, and changelog\n2026-01-24|Improve request handling and qBittorrent categories\n2026-01-24|Map Prowlarr releases to Arr indexers for manual grab\n2026-01-24|Clarify how-it-works steps and fixes\n2026-01-24|Document fix buttons in how-it-works\n2026-01-24|Route grabs through Sonarr/Radarr only\n2026-01-23|Use backend branding assets for logo and favicon\n2026-01-23|Copy public assets into frontend image\n2026-01-23|Fix backend Dockerfile paths for root context\n2026-01-23|Add Docker Hub compose override\n2026-01-23|Remove password fields from users page\n2026-01-23|Use bundled branding assets\n2026-01-23|Add default branding assets when missing\n2026-01-23|Show available status on landing when in Jellyfin\n2026-01-23|Fix cache titles and move feedback link\n2026-01-23|Add feedback form and webhook\n2026-01-23|Hide header actions when signed out\n2026-01-23|Fallback manual grab to qBittorrent\n2026-01-23|Split search actions and improve download options\n2026-01-23|Fix cache titles via Jellyseerr media lookup\n2026-01-22|Update README with Docker-first guide\n2026-01-22|Update README\n2026-01-22|Ignore build artifacts\n2026-01-22|Initial commit' +BUILD_NUMBER = "0303261719" +CHANGELOG = '2026-03-03|Update all email templates with uniform branded graphics\n2026-03-03|Add branded HTML email templates\n2026-03-03|Add SMTP receipt logging for Exchange relay tracing\n2026-03-03|Fix shared request access and Jellyfin-ready pipeline status\n2026-03-03|Process 1 build 0303261507\n2026-03-03|Improve SQLite batching and diagnostics visibility\n2026-03-03|Add login page visibility controls\n2026-03-03|Hotfix: expand landing-page search to all requests\n2026-03-02|Hotfix: add logged-out password reset flow\n2026-03-02|Process 1 build 0203261953\n2026-03-02|Process 1 build 0203261610\n2026-03-02|Process 1 build 0203261608\n2026-03-02|Add dedicated profile invites page and fix mobile admin layout\n2026-03-01|Persist Seerr media failure suppression and reduce sync error noise\n2026-03-01|Add repository line ending policy\n2026-03-01|Finalize diagnostics, logging controls, and email test support\n2026-03-01|Add invite email templates and delivery workflow\n2026-02-28|Finalize dev-1.3 upgrades and Seerr updates\n2026-02-27|admin docs and layout refresh, build 2702261314\n2026-02-27|Build 2702261153: fix jellyfin sync user visibility\n2026-02-26|Build 2602262241: live request page updates\n2026-02-26|Build 2602262204\n2026-02-26|Build 2602262159: restore jellyfin-first user source\n2026-02-26|Build 2602262049: split magent settings and harden local login\n2026-02-26|Build 2602262030: add magent settings and hardening\n2026-02-26|Build 2602261731: fix user resync after nuclear wipe\n2026-02-26|Build 2602261717: master invite policy and self-service invite controls\n2026-02-26|Build 2602261636: self-service invites and count fixes\n2026-02-26|Build 2602261605: invite trace and cross-system user lifecycle\n2026-02-26|Build 2602261536: refine invite layouts and tighten UI\n2026-02-26|Build 2602261523: live updates, invite cleanup and nuclear resync\n2026-02-26|Build 2602261442: tidy users and invite layouts\n2026-02-26|Build 2602261409: unify invite management controls\n2026-02-26|Build 2602260214: invites profiles and expiry admin controls\n2026-02-26|Build 2602260022: enterprise UI refresh and users bulk auto-search\n2026-02-25|Build 2502262321: fix auto-search quality and per-user toggle\n2026-02-02|Build 0202261541: allow FQDN service URLs\n2026-01-30|Build 3001262148: single container\n2026-01-29|Build 2901262244: format changelog\n2026-01-29|Build 2901262240: cache users\n2026-01-29|Tidy full changelog\n2026-01-29|Update full changelog\n2026-01-29|Bake build number and changelog\n2026-01-29|Hardcode build number in backend\n2026-01-29|release: 2901262102\n2026-01-29|release: 2901262044\n2026-01-29|release: 2901262036\n2026-01-27|Hydrate missing artwork from Jellyseerr (build 271261539)\n2026-01-27|Fallback to TMDB when artwork cache fails (build 271261524)\n2026-01-27|Add service test buttons (build 271261335)\n2026-01-27|Bump build number (process 2) 271261322\n2026-01-27|Add cache load spinner (build 271261238)\n2026-01-27|Fix snapshot title fallback (build 271261228)\n2026-01-27|Fix request titles in snapshots (build 271261219)\n2026-01-27|Bump build number to 271261202\n2026-01-27|Clarify request sync settings (build 271261159)\n2026-01-27|Fix backend cache stats import (build 271261149)\n2026-01-27|Improve cache stats performance (build 271261145)\n2026-01-27|Add cache control artwork stats\n2026-01-26|Fix sync progress bar animation\n2026-01-26|Fix cache title hydration\n2026-01-25|Build 2501262041\n2026-01-25|Harden request cache titles and cache-only reads\n2026-01-25|Serve bundled branding assets by default\n2026-01-25|Seed branding logo from bundled assets\n2026-01-25|Tidy request sync controls\n2026-01-25|Add Jellyfin login cache and admin-only stats\n2026-01-25|Add user stats and activity tracking\n2026-01-25|Move account actions into avatar menu\n2026-01-25|Improve mobile header layout\n2026-01-25|Automate build number tagging and sync\n2026-01-25|Add site banner, build number, and changelog\n2026-01-24|Improve request handling and qBittorrent categories\n2026-01-24|Map Prowlarr releases to Arr indexers for manual grab\n2026-01-24|Clarify how-it-works steps and fixes\n2026-01-24|Document fix buttons in how-it-works\n2026-01-24|Route grabs through Sonarr/Radarr only\n2026-01-23|Use backend branding assets for logo and favicon\n2026-01-23|Copy public assets into frontend image\n2026-01-23|Fix backend Dockerfile paths for root context\n2026-01-23|Add Docker Hub compose override\n2026-01-23|Remove password fields from users page\n2026-01-23|Use bundled branding assets\n2026-01-23|Add default branding assets when missing\n2026-01-23|Show available status on landing when in Jellyfin\n2026-01-23|Fix cache titles and move feedback link\n2026-01-23|Add feedback form and webhook\n2026-01-23|Hide header actions when signed out\n2026-01-23|Fallback manual grab to qBittorrent\n2026-01-23|Split search actions and improve download options\n2026-01-23|Fix cache titles via Jellyseerr media lookup\n2026-01-22|Update README with Docker-first guide\n2026-01-22|Update README\n2026-01-22|Ignore build artifacts\n2026-01-22|Initial commit' diff --git a/backend/app/services/invite_email.py b/backend/app/services/invite_email.py index 53e0312..1cfb4d6 100644 --- a/backend/app/services/invite_email.py +++ b/backend/app/services/invite_email.py @@ -139,17 +139,17 @@ TEMPLATE_PRESENTATION: Dict[str, Dict[str, str]] = { def _build_email_stat_card(label: str, value: str, detail: str = "") -> str: detail_html = ( - f"
" + f"
" f"{html.escape(detail)}
" if detail else "" ) return ( - "
" - f"
" + f"
{html.escape(label)}
" - f"
" + f"
" f"{html.escape(value)}
" f"{detail_html}" "
" @@ -184,7 +184,7 @@ def _build_email_list(items: list[str], *, ordered: bool = False) -> str: f"
  • {html.escape(item)}
  • " for item in items if item ) return ( - f"<{tag} style=\"margin:0; {marker} color:#dbe5ff; line-height:1.8; font-size:14px;\">" + f"<{tag} style=\"margin:0; {marker} color:#132033; line-height:1.8; font-size:14px;\">" f"{rendered_items}" f"" ) @@ -193,40 +193,40 @@ def _build_email_list(items: list[str], *, ordered: bool = False) -> str: def _build_email_panel(title: str, body_html: str, *, variant: str = "neutral") -> str: styles = { "neutral": { - "background": "#101726", - "border": "rgba(255,255,255,0.08)", - "eyebrow": "#9aa3b8", - "text": "#dbe5ff", + "background": "#f8fafc", + "border": "#d9e2ef", + "eyebrow": "#6b778c", + "text": "#132033", }, "brand": { - "background": "#101726", - "border": "rgba(59,130,246,0.22)", - "eyebrow": "#9dbfff", - "text": "#dbe5ff", + "background": "#eef4ff", + "border": "#bfd2ff", + "eyebrow": "#2754b6", + "text": "#132033", }, "success": { - "background": "#122016", - "border": "rgba(34,197,94,0.24)", - "eyebrow": "#9de7b5", - "text": "#d9f9e4", + "background": "#edf9f0", + "border": "#bfe4c6", + "eyebrow": "#1f7a3f", + "text": "#132033", }, "warning": { - "background": "#241814", - "border": "rgba(251,146,60,0.34)", - "eyebrow": "#fbbd7b", - "text": "#ffe0ba", + "background": "#fff5ea", + "border": "#ffd5a8", + "eyebrow": "#c46a10", + "text": "#132033", }, "danger": { - "background": "#251418", - "border": "rgba(239,68,68,0.32)", - "eyebrow": "#ff9b9b", - "text": "#ffd0d0", + "background": "#fff0f0", + "border": "#f3c1c1", + "eyebrow": "#bb2d2d", + "text": "#132033", }, }.get(variant, { - "background": "#101726", - "border": "rgba(255,255,255,0.08)", - "eyebrow": "#9aa3b8", - "text": "#dbe5ff", + "background": "#f8fafc", + "border": "#d9e2ef", + "eyebrow": "#6b778c", + "text": "#132033", }) return ( f"
    " + "
    " "A new invitation has been prepared for {{recipient_email}}. Use the details below to sign up." "
    " + _build_email_stat_grid( @@ -305,7 +305,7 @@ DEFAULT_TEMPLATES: Dict[str, Dict[str, str]] = { "{{message}}\n" ), "body_html": ( - "
    " + "
    " "Your account is live and ready to use. Everything below mirrors the current site behavior." "
    " + _build_email_stat_grid( @@ -345,7 +345,7 @@ DEFAULT_TEMPLATES: Dict[str, Dict[str, str]] = { "If you need help, contact the admin.\n" ), "body_html": ( - "
    " + "
    " "Please review this account notice carefully. This message was sent by an administrator." "
    " + _build_email_stat_grid( @@ -388,7 +388,7 @@ DEFAULT_TEMPLATES: Dict[str, Dict[str, str]] = { "{{message}}\n" ), "body_html": ( - "
    " + "
    " "Your account access has changed. Review the details below." "
    " + _build_email_stat_grid( @@ -507,13 +507,14 @@ def _looks_like_full_html_document(value: str) -> bool: def _build_email_action_button(label: str, url: str, *, primary: bool) -> str: - background = "linear-gradient(135deg, #ff6b2b 0%, #1c6bff 100%)" if primary else "#151c2d" - border = "1px solid rgba(59, 130, 246, 0.32)" if primary else "1px solid rgba(255, 255, 255, 0.12)" - color = "#ffffff" + background = "linear-gradient(135deg, #ff6b2b 0%, #1c6bff 100%)" if primary else "#ffffff" + fallback = "#1c6bff" if primary else "#ffffff" + border = "1px solid rgba(28, 107, 255, 0.28)" if primary else "1px solid #d5deed" + color = "#ffffff" if primary else "#132033" return ( f"{html.escape(label)}" ) @@ -560,39 +561,39 @@ def _wrap_email_html( return ( "" - "" + "" "
    " f"{html.escape(title)} - {html.escape(subtitle)}" "
    " "" - "
    " + "style=\"width:100%; border-collapse:collapse; background:#eef2f7;\" bgcolor=\"#eef2f7\">" + "
    " "" "
    " - f"
    " + f"
    " "" "" f"" "" "" "
    {logo_block}" - f"
    {html.escape(app_name)}
    " - f"
    {html.escape(title)}
    " - f"
    {html.escape(subtitle or EMAIL_TAGLINE)}
    " + f"
    {html.escape(app_name)}
    " + f"
    {html.escape(title)}
    " + f"
    {html.escape(subtitle or EMAIL_TAGLINE)}
    " "
    " - f"
    " + f"
    " f"
    " f"{html.escape(EMAIL_TAGLINE)}
    " - f"
    {body_html}
    " + f"
    {body_html}
    " f"
    {actions_html}
    " - "
    " + "
    " "" "" - f"" - f"" + f"" + f"" "" "
    {html.escape(footer)}Build {html.escape(build_number)}{html.escape(footer)}Build {html.escape(build_number)}
    " "
    " @@ -1014,7 +1015,7 @@ async def send_test_email(recipient_email: Optional[str] = None) -> Dict[str, st subtitle="This confirms Magent can generate and hand off branded mail.", tone="brand", body_html=( - "
    " + "
    " "This is a live test email from Magent. If this renders correctly, the HTML template shell and SMTP handoff are both working." "
    " + _build_email_stat_grid( @@ -1110,7 +1111,7 @@ async def send_password_reset_email( subtitle=f"This will update the credentials used for {provider_label}.", tone="brand", body_html=( - f"
    " + f"
    " f"A password reset was requested for {html.escape(username)}." "
    " + _build_email_stat_grid( diff --git a/frontend/app/admin/SettingsPage.tsx b/frontend/app/admin/SettingsPage.tsx index 274805b..0e0673e 100644 --- a/frontend/app/admin/SettingsPage.tsx +++ b/frontend/app/admin/SettingsPage.tsx @@ -2296,14 +2296,6 @@ export default function SettingsPage({ section }: SettingsPageProps) { /> ) : null} - {getSectionTestLabel(sectionGroup.key) ? (
    ))} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 811a7b7..4ddbf15 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "magent-frontend", - "version": "0303261702", + "version": "0303261719", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "magent-frontend", - "version": "0303261702", + "version": "0303261719", "dependencies": { "next": "16.1.6", "react": "19.2.4", diff --git a/frontend/package.json b/frontend/package.json index 1868c03..3271914 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "magent-frontend", "private": true, - "version": "0303261702", + "version": "0303261719", "scripts": { "dev": "next dev", "build": "next build",