From 1ff54690fcf180a1c206472e0801a9ed6633c918 Mon Sep 17 00:00:00 2001 From: Rephl3x Date: Tue, 3 Mar 2026 16:30:02 +1300 Subject: [PATCH] Add branded HTML email templates --- .build_number | 2 +- backend/app/build_info.py | 4 +- backend/app/services/invite_email.py | 384 ++++++++++++++++++++++++--- frontend/package-lock.json | 4 +- frontend/package.json | 2 +- 5 files changed, 346 insertions(+), 50 deletions(-) diff --git a/.build_number b/.build_number index c630262..a17e58e 100644 --- a/.build_number +++ b/.build_number @@ -1 +1 @@ -0303261611 +0303261629 diff --git a/backend/app/build_info.py b/backend/app/build_info.py index 0118f36..887a7e6 100644 --- a/backend/app/build_info.py +++ b/backend/app/build_info.py @@ -1,2 +1,2 @@ -BUILD_NUMBER = "0303261611" -CHANGELOG = '2026-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 = "0303261629" +CHANGELOG = '2026-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 f02e8dd..ff9b81e 100644 --- a/backend/app/services/invite_email.py +++ b/backend/app/services/invite_email.py @@ -64,6 +64,78 @@ TEMPLATE_PLACEHOLDERS = [ "username", ] +EMAIL_TAGLINE = "Find and fix media requests fast." + +EMAIL_TONE_STYLES: Dict[str, Dict[str, str]] = { + "brand": { + "chip_bg": "rgba(255, 107, 43, 0.16)", + "chip_border": "rgba(255, 107, 43, 0.38)", + "chip_text": "#ffd2bf", + "accent_a": "#ff6b2b", + "accent_b": "#1c6bff", + }, + "success": { + "chip_bg": "rgba(34, 197, 94, 0.16)", + "chip_border": "rgba(34, 197, 94, 0.38)", + "chip_text": "#c7f9d7", + "accent_a": "#22c55e", + "accent_b": "#1c6bff", + }, + "warning": { + "chip_bg": "rgba(251, 146, 60, 0.16)", + "chip_border": "rgba(251, 146, 60, 0.38)", + "chip_text": "#ffe0ba", + "accent_a": "#fb923c", + "accent_b": "#ff6b2b", + }, + "danger": { + "chip_bg": "rgba(248, 113, 113, 0.16)", + "chip_border": "rgba(248, 113, 113, 0.38)", + "chip_text": "#ffd0d0", + "accent_a": "#ef4444", + "accent_b": "#ff6b2b", + }, +} + +TEMPLATE_PRESENTATION: Dict[str, Dict[str, str]] = { + "invited": { + "tone": "brand", + "title": "You have been invited", + "subtitle": "A new account invitation is ready for you.", + "primary_label": "Accept invite", + "primary_url_key": "invite_link", + "secondary_label": "How it works", + "secondary_url_key": "how_it_works_url", + }, + "welcome": { + "tone": "success", + "title": "Welcome to Magent", + "subtitle": "Your account is ready and synced.", + "primary_label": "Open Magent", + "primary_url_key": "app_url", + "secondary_label": "How it works", + "secondary_url_key": "how_it_works_url", + }, + "warning": { + "tone": "warning", + "title": "Account warning", + "subtitle": "Please review the note below.", + "primary_label": "Open Magent", + "primary_url_key": "app_url", + "secondary_label": "How it works", + "secondary_url_key": "how_it_works_url", + }, + "banned": { + "tone": "danger", + "title": "Account status changed", + "subtitle": "Your account has been restricted or removed.", + "primary_label": "How it works", + "primary_url_key": "how_it_works_url", + "secondary_label": "", + "secondary_url_key": "", + }, +} + DEFAULT_TEMPLATES: Dict[str, Dict[str, str]] = { "invited": { "subject": "{{app_name}} invite for {{recipient_email}}", @@ -81,18 +153,34 @@ DEFAULT_TEMPLATES: Dict[str, Dict[str, str]] = { "Build: {{build_number}}\n" ), "body_html": ( - "

You have been invited

" - "

You have been invited to {{app_name}}.

" - "

Invite code: {{invite_code}}
" - "Invited by: {{inviter_username}}
" - "Invite label: {{invite_label}}
" - "Expires: {{invite_expires_at}}
" - "Remaining uses: {{invite_remaining_uses}}

" - "

{{invite_description}}

" - "

{{message}}

" - "

Accept invite and create account

" - "

How it works

" - "

Build {{build_number}}

" + "
" + "A new invitation has been prepared for {{recipient_email}}. Use the details below to sign up." + "
" + "" + "" + "" + "" + "" + "" + "" + "" + "" + "
" + "
Invite code
" + "
{{invite_code}}
" + "
" + "
Invited by
" + "
{{inviter_username}}
" + "
" + "
Invite label
" + "
{{invite_label}}
" + "
" + "
Access window
" + "
{{invite_expires_at}}
" + "
Remaining uses: {{invite_remaining_uses}}
" + "
" + "
{{invite_description}}
" + "
{{message}}
" ), }, "welcome": { @@ -106,12 +194,30 @@ DEFAULT_TEMPLATES: Dict[str, Dict[str, str]] = { "{{message}}\n" ), "body_html": ( - "

Welcome

" - "

Your {{app_name}} account is ready, {{username}}.

" - "

Role: {{role}}

" - "

Open {{app_name}}
" - "Read how it works

" - "

{{message}}

" + "
" + "Your account is live and ready to use. Everything below mirrors the current site behavior." + "
" + "" + "" + "" + "" + "" + "
" + "
Username
" + "
{{username}}
" + "
" + "
Role
" + "
{{role}}
" + "
" + "
" + "
What to do next
" + "
    " + "
  1. Open Magent and sign in using your shared credentials.
  2. " + "
  3. Search or review requests without refreshing every page.
  4. " + "
  5. Use the invite tools in your profile if your account allows it.
  6. " + "
" + "
" + "
{{message}}
" ), }, "warning": { @@ -124,12 +230,15 @@ DEFAULT_TEMPLATES: Dict[str, Dict[str, str]] = { "If you need help, contact the admin.\n" ), "body_html": ( - "

Account warning

" - "

Hello {{username}},

" - "

This is a warning regarding your {{app_name}} account.

" - "

Reason: {{reason}}

" - "

{{message}}

" - "

If you need help, contact the admin.

" + "
" + "Please review this account notice carefully. This message was sent by an administrator." + "
" + "
" + "
Reason
" + "
{{reason}}
" + "
" + "
{{message}}
" + "
If you need help or think this was sent in error, contact the site administrator.
" ), }, "banned": { @@ -141,11 +250,18 @@ DEFAULT_TEMPLATES: Dict[str, Dict[str, str]] = { "{{message}}\n" ), "body_html": ( - "

Account status changed

" - "

Hello {{username}},

" - "

Your {{app_name}} account has been banned or removed.

" - "

Reason: {{reason}}

" - "

{{message}}

" + "
" + "Your account access has changed. Review the details below." + "
" + "" + "" + "" + "" + "
" + "
Reason
" + "
{{reason}}
" + "
" + "
{{message}}
" ), }, } @@ -227,6 +343,108 @@ def _build_default_base_url() -> str: return f"http://localhost:{port}" +def _looks_like_full_html_document(value: str) -> bool: + probe = value.lstrip().lower() + return probe.startswith(" 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" + return ( + f"{html.escape(label)}" + ) + + +def _wrap_email_html( + *, + app_name: str, + app_url: str, + build_number: str, + title: str, + subtitle: str, + tone: str, + body_html: str, + primary_label: str = "", + primary_url: str = "", + secondary_label: str = "", + secondary_url: str = "", + footer_note: str = "", +) -> str: + styles = EMAIL_TONE_STYLES.get(tone, EMAIL_TONE_STYLES["brand"]) + logo_url = "" + if app_url.lower().startswith("http://") or app_url.lower().startswith("https://"): + logo_url = f"{app_url.rstrip('/')}/branding/logo.png" + + actions = [] + if primary_label and primary_url: + actions.append(_build_email_action_button(primary_label, primary_url, primary=True)) + if secondary_label and secondary_url: + actions.append(_build_email_action_button(secondary_label, secondary_url, primary=False)) + actions_html = "".join(actions) + + footer = footer_note or "This email was generated automatically by Magent." + logo_block = ( + f"\"{html.escape(app_name)}\"" + if logo_url + else ( + "
M
" + ) + ) + + return ( + "" + "" + "
" + f"{html.escape(title)} - {html.escape(subtitle)}" + "
" + "" + "
" + "" + "
" + f"
" + "" + "" + f"" + "" + "" + "
{logo_block}" + f"
{html.escape(app_name)}
" + f"
{html.escape(title)}
" + f"
{html.escape(subtitle or EMAIL_TAGLINE)}
" + "
" + f"
" + f"
" + f"{html.escape(EMAIL_TAGLINE)}
" + f"
{body_html}
" + f"
{actions_html}
" + "
" + "" + "" + f"" + f"" + "" + "
{html.escape(footer)}Build {html.escape(build_number)}
" + "
" + "
" + "
" + "
" + "" + ) + + def build_invite_email_context( *, invite: Optional[Dict[str, Any]] = None, @@ -348,11 +566,35 @@ def render_invite_email_template( reason=reason, overrides=overrides, ) - body_html = _render_template_string(template["body_html"], context, escape_html=True) + raw_body_html = _render_template_string(template["body_html"], context, escape_html=True) body_text = _render_template_string(template["body_text"], context, escape_html=False) - if not body_text.strip() and body_html.strip(): - body_text = _strip_html_for_text(body_html) + if not body_text.strip() and raw_body_html.strip(): + body_text = _strip_html_for_text(raw_body_html) subject = _render_template_string(template["subject"], context, escape_html=False) + presentation = TEMPLATE_PRESENTATION.get(template_key, TEMPLATE_PRESENTATION["invited"]) + primary_url = _normalize_display_text(context.get(presentation["primary_url_key"], "")) + secondary_url = _normalize_display_text(context.get(presentation["secondary_url_key"], "")) + if _looks_like_full_html_document(raw_body_html): + body_html = raw_body_html.strip() + else: + body_html = _wrap_email_html( + app_name=_normalize_display_text(context.get("app_name"), env_settings.app_name), + app_url=_normalize_display_text(context.get("app_url"), _build_default_base_url()), + build_number=_normalize_display_text(context.get("build_number"), BUILD_NUMBER), + title=_normalize_display_text(context.get("title"), presentation["title"]), + subtitle=_normalize_display_text(context.get("subtitle"), presentation["subtitle"]), + tone=_normalize_display_text(context.get("tone"), presentation["tone"]), + body_html=raw_body_html.strip(), + primary_label=_normalize_display_text( + context.get("primary_label"), presentation["primary_label"] + ), + primary_url=primary_url, + secondary_label=_normalize_display_text( + context.get("secondary_label"), presentation["secondary_label"] + ), + secondary_url=secondary_url, + footer_note=_normalize_display_text(context.get("footer_note"), ""), + ).strip() return { "subject": subject.strip(), "body_text": body_text.strip(), @@ -592,17 +834,43 @@ async def send_test_email(recipient_email: Optional[str] = None) -> Dict[str, st raise RuntimeError("No valid recipient email is configured for the test message.") application_url = _normalize_display_text(runtime.magent_application_url, "Not configured") + primary_url = application_url if application_url.lower().startswith(("http://", "https://")) else "" subject = f"{env_settings.app_name} email test" body_text = ( f"This is a test email from {env_settings.app_name}.\n\n" f"Build: {BUILD_NUMBER}\n" f"Application URL: {application_url}\n" ) - body_html = ( - f"

{html.escape(env_settings.app_name)} email test

" - f"

This is a test email from {html.escape(env_settings.app_name)}.

" - f"

Build: {html.escape(BUILD_NUMBER)}
" - f"Application URL: {html.escape(application_url)}

" + body_html = _wrap_email_html( + app_name=env_settings.app_name, + app_url=_build_default_base_url(), + build_number=BUILD_NUMBER, + title="Email delivery test", + 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
" + f"
{html.escape(BUILD_NUMBER)}
" + "
" + "
Application URL
" + f"
{html.escape(application_url)}
" + "
" + "
" + "Use this test when changing SMTP settings, relay targets, or branding." + "
" + ), + primary_label="Open Magent" if primary_url else "", + primary_url=primary_url, + footer_note="SMTP test email generated by Magent.", ) receipt = await asyncio.to_thread( @@ -654,13 +922,41 @@ async def send_password_reset_email( f"Expires: {expires_at}\n\n" "If you did not request this reset, you can ignore this email.\n" ) - body_html = ( - f"

{html.escape(env_settings.app_name)} password reset

" - f"

A password reset was requested for {html.escape(username)}.

" - f"

This link will reset the password used for {html.escape(provider_label)}.

" - f"

Reset password

" - f"

Expires: {html.escape(expires_at)}

" - "

If you did not request this reset, you can ignore this email.

" + body_html = _wrap_email_html( + app_name=env_settings.app_name, + app_url=app_url, + build_number=BUILD_NUMBER, + title="Reset your password", + subtitle=f"This will update the credentials used for {provider_label}.", + tone="brand", + body_html=( + f"
" + f"A password reset was requested for {html.escape(username)}." + "
" + "" + "" + "" + "" + "" + "
" + "
Account
" + f"
{html.escape(username)}
" + "
" + "
Expires
" + f"
{html.escape(expires_at)}
" + "
" + f"
" + f"This reset will update the password used for {html.escape(provider_label)}." + "
" + "
" + "If you did not request this reset, ignore this email. No changes will be applied until the reset link is opened and completed." + "
" + ), + primary_label="Reset password", + primary_url=reset_url, + secondary_label="Open Magent", + secondary_url=app_url, + footer_note="Password reset email generated by Magent.", ) receipt = await asyncio.to_thread( diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 0478a7f..01e43f0 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "magent-frontend", - "version": "0303261611", + "version": "0303261629", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "magent-frontend", - "version": "0303261611", + "version": "0303261629", "dependencies": { "next": "16.1.6", "react": "19.2.4", diff --git a/frontend/package.json b/frontend/package.json index 1433a17..39bf8de 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "magent-frontend", "private": true, - "version": "0303261611", + "version": "0303261629", "scripts": { "dev": "next dev", "build": "next build",