From 5d1fd3304b9ff06ee03144424c798769be4e7199 Mon Sep 17 00:00:00 2001 From: 3252a8 <3252a8@proton.me> Date: Wed, 3 Jun 2026 11:20:50 +0300 Subject: [PATCH] docs: refresh branded email previews --- docs-site/scripts/generate-email-previews.py | 46 +++++++++++++++++++- docs-site/src/lib/emailPreviews.mjs | 1 + docs/features/admin-panel.md | 2 + docs/features/email-login.md | 6 +++ docs/features/notifications.md | 2 + docs/features/webapp-themes.md | 2 + tests/test_email_localization.py | 2 + 7 files changed, 59 insertions(+), 2 deletions(-) diff --git a/docs-site/scripts/generate-email-previews.py b/docs-site/scripts/generate-email-previews.py index ac36473..facbe3e 100644 --- a/docs-site/scripts/generate-email-previews.py +++ b/docs-site/scripts/generate-email-previews.py @@ -1,5 +1,8 @@ +import base64 +import hashlib import json import sys +import tempfile from pathlib import Path from types import SimpleNamespace @@ -9,6 +12,7 @@ sys.path.insert(0, str(BACKEND_ROOT)) if hasattr(sys.stdout, "reconfigure"): sys.stdout.reconfigure(encoding="utf-8") +from bot.services import email_templates as email_templates_module # noqa: E402 from bot.services.email_templates import ( # noqa: E402 render_account_merged, render_login_code, @@ -23,6 +27,34 @@ from bot.services.email_templates import ( # noqa: E402 ) LANGUAGE = "ru" +PREVIEW_LOGO_FILE = ( + REPO_ROOT + / "backend" + / "bot" + / "app" + / "web" + / "templates" + / "default-brand" + / "favicons" + / "19b2a242e5b7bc2d" + / "icon-180.png" +) +PREVIEW_LOGO_TEMP_DIR = tempfile.TemporaryDirectory() + + +def prepare_preview_logo_url() -> str: + if not PREVIEW_LOGO_FILE.exists(): + return "" + body = PREVIEW_LOGO_FILE.read_bytes() + digest = hashlib.sha256(body).hexdigest()[:16] + filename = f"logo-{digest}.png" + logo_dir = Path(PREVIEW_LOGO_TEMP_DIR.name) + (logo_dir / filename).write_bytes(body) + email_templates_module._WEBAPP_UPLOADED_LOGO_DIR = logo_dir + return f"/webapp-uploaded-logo/{filename}" + + +PREVIEW_LOGO_URL = prepare_preview_logo_url() class PreviewI18n: @@ -63,7 +95,7 @@ def settings(): return SimpleNamespace( DEFAULT_LANGUAGE=LANGUAGE, EMAIL_CODE_TTL_SECONDS=600, - WEBAPP_LOGO_URL="", + WEBAPP_LOGO_URL=PREVIEW_LOGO_URL, WEBAPP_PRIMARY_COLOR="#00fe7a", WEBAPP_TITLE="remnawave-minishop", ) @@ -94,10 +126,20 @@ def preview(item_id: str, category: str, title: str, content): "category": category, "title": title, "subject": content.subject, - "html": content.html, + "html": preview_html(content), } +def preview_html(content): + rendered = content.html + for image in content.inline_images: + data_url = ( + f"data:{image.content_type};base64,{base64.b64encode(image.data).decode('ascii')}" + ) + rendered = rendered.replace(f"cid:{image.content_id}", data_url) + return rendered + + def payment_preview( item_id: str, title: str, diff --git a/docs-site/src/lib/emailPreviews.mjs b/docs-site/src/lib/emailPreviews.mjs index 3a0d6b8..1822f42 100644 --- a/docs-site/src/lib/emailPreviews.mjs +++ b/docs-site/src/lib/emailPreviews.mjs @@ -31,6 +31,7 @@ for (const command of pythonCommands) { const result = spawnSync(command, [generatorPath], { cwd: repoRoot, encoding: "utf8", + maxBuffer: 20 * 1024 * 1024, env: { ...process.env, PYTHONIOENCODING: "utf-8", diff --git a/docs/features/admin-panel.md b/docs/features/admin-panel.md index afe87d4..a964ec4 100644 --- a/docs/features/admin-panel.md +++ b/docs/features/admin-panel.md @@ -104,6 +104,8 @@ Раздел **Внешний вид** объединяет настройки бренда и темы Web App. Логотип можно загрузить файлом или по HTTPS-ссылке; backend сохраняет файл в `data/webapp-logo/uploads` и подставляет локальный URL. Если логотип не задан, показывается логотип проекта по умолчанию. Favicon генерируется из логотипа или загружается отдельно. +Этот же бренд используется в HTML-письмах. Загруженный локальный логотип встраивается в письмо как inline image (`cid:webapp-logo`), поэтому email-клиенту не нужен прямой доступ к `/webapp-uploaded-logo/...`. Логотип по публичной HTTPS-ссылке остается внешней картинкой в письме. + В блоке тем админка читает каталог из `WEBAPP_THEMES_DIR`, показывает встроенные и кастомные темы, позволяет выбрать текущую тему, изменить accent, включить или выключить тему для админки и настроить масштаб логотипа на главной и экране входа. Кнопка предпросмотра открывает `/home?theme_preview=` и не меняет глобальную тему до сохранения. Подробный формат `theme.json`, CSS/asset-роуты и пошаговый пайплайн создания новой темы описаны в [webapp-themes.md](webapp-themes.md). diff --git a/docs/features/email-login.md b/docs/features/email-login.md index c200518..0a30fa1 100644 --- a/docs/features/email-login.md +++ b/docs/features/email-login.md @@ -44,6 +44,12 @@ BRUTE_FORCE_WINDOW_SECONDS=900 BRUTE_FORCE_LOCK_SECONDS=900 ``` +## Брендинг писем + +HTML-письма используют тот же бренд, что и Mini App: название из `WEBAPP_TITLE`, accent из внешнего вида и логотип из раздела **Внешний вид**. Если логотип загружен через админку файлом, backend прикладывает его к письму как inline image (`cid:webapp-logo`), поэтому получателю не нужен доступ к внутреннему `/webapp-uploaded-logo/...`. + +Если в качестве логотипа задан публичный `https://` URL, письмо использует его как обычный внешний ``. В этом режиме некоторые почтовые клиенты могут скрыть картинку, пока получатель не разрешит загрузку внешних изображений. + Для Brevo обычно подходит порт `587` с STARTTLS. Если основной порт недоступен, приложение пробует порты из `SMTP_FALLBACK_PORTS`; порт `465` используется через SSL wrapper автоматически. `SMTP_FROM_EMAIL` должен быть подтвержден у SMTP-провайдера, иначе письмо часто отклоняется или попадает в спам. `SMTP_FROM_NAME` можно оставить пустым, тогда используется название Web App. diff --git a/docs/features/notifications.md b/docs/features/notifications.md index 1090eac..76864fe 100644 --- a/docs/features/notifications.md +++ b/docs/features/notifications.md @@ -6,6 +6,8 @@ Minishop отправляет уведомления в Telegram и на email. Для уведомлений жизненного цикла подписки есть отдельный флаг `SUBSCRIPTION_EMAIL_NOTIFICATIONS_ENABLED`. Если он включен, пользовательские уведомления об окончании подписки отправляются в Telegram при наличии привязанного Telegram-аккаунта и на email при наличии привязанной почты. +Все HTML-письма используют общий email-шаблон с брендом из Web App: заголовком, accent-цветом и логотипом. Логотип, загруженный через раздел **Внешний вид**, отправляется как inline image, а публичный HTTPS-логотип остается внешней картинкой. + ## Сводная таблица | Событие | Получатель | Telegram | Email | Условия и ограничения | diff --git a/docs/features/webapp-themes.md b/docs/features/webapp-themes.md index 0757939..bb6f102 100644 --- a/docs/features/webapp-themes.md +++ b/docs/features/webapp-themes.md @@ -52,6 +52,8 @@ WEBAPP_DEFAULT_THEME= Важно: `WEBAPP_PRIMARY_COLOR` и `WEBAPP_LOGO_URL` больше не являются рабочим способом первичной настройки через `.env`. Эти значения редактируются в админке и сохраняются как overrides в базе. Тема при этом может использовать сохраненный primary color как fallback accent. +Email-шаблоны берут тот же бренд из настроек внешнего вида. Загруженный логотип добавляется в письма как inline image (`cid:webapp-logo`), а публичный HTTPS-логотип остается внешней картинкой, которую почтовый клиент может скрыть до разрешения загрузки изображений. + ## Контракт `theme.json` Минимальная тема: diff --git a/tests/test_email_localization.py b/tests/test_email_localization.py index 68a2c95..8f872c1 100644 --- a/tests/test_email_localization.py +++ b/tests/test_email_localization.py @@ -326,5 +326,7 @@ def test_docs_email_preview_generator_renders_real_template_html(): assert all(preview["html"].lstrip().startswith("") for preview in previews) assert all('