docs: refresh branded email previews
This commit is contained in:
@@ -1,5 +1,8 @@
|
|||||||
|
import base64
|
||||||
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
|
|
||||||
@@ -9,6 +12,7 @@ sys.path.insert(0, str(BACKEND_ROOT))
|
|||||||
if hasattr(sys.stdout, "reconfigure"):
|
if hasattr(sys.stdout, "reconfigure"):
|
||||||
sys.stdout.reconfigure(encoding="utf-8")
|
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
|
from bot.services.email_templates import ( # noqa: E402
|
||||||
render_account_merged,
|
render_account_merged,
|
||||||
render_login_code,
|
render_login_code,
|
||||||
@@ -23,6 +27,34 @@ from bot.services.email_templates import ( # noqa: E402
|
|||||||
)
|
)
|
||||||
|
|
||||||
LANGUAGE = "ru"
|
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:
|
class PreviewI18n:
|
||||||
@@ -63,7 +95,7 @@ def settings():
|
|||||||
return SimpleNamespace(
|
return SimpleNamespace(
|
||||||
DEFAULT_LANGUAGE=LANGUAGE,
|
DEFAULT_LANGUAGE=LANGUAGE,
|
||||||
EMAIL_CODE_TTL_SECONDS=600,
|
EMAIL_CODE_TTL_SECONDS=600,
|
||||||
WEBAPP_LOGO_URL="",
|
WEBAPP_LOGO_URL=PREVIEW_LOGO_URL,
|
||||||
WEBAPP_PRIMARY_COLOR="#00fe7a",
|
WEBAPP_PRIMARY_COLOR="#00fe7a",
|
||||||
WEBAPP_TITLE="remnawave-minishop",
|
WEBAPP_TITLE="remnawave-minishop",
|
||||||
)
|
)
|
||||||
@@ -94,10 +126,20 @@ def preview(item_id: str, category: str, title: str, content):
|
|||||||
"category": category,
|
"category": category,
|
||||||
"title": title,
|
"title": title,
|
||||||
"subject": content.subject,
|
"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(
|
def payment_preview(
|
||||||
item_id: str,
|
item_id: str,
|
||||||
title: str,
|
title: str,
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ for (const command of pythonCommands) {
|
|||||||
const result = spawnSync(command, [generatorPath], {
|
const result = spawnSync(command, [generatorPath], {
|
||||||
cwd: repoRoot,
|
cwd: repoRoot,
|
||||||
encoding: "utf8",
|
encoding: "utf8",
|
||||||
|
maxBuffer: 20 * 1024 * 1024,
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
PYTHONIOENCODING: "utf-8",
|
PYTHONIOENCODING: "utf-8",
|
||||||
|
|||||||
@@ -104,6 +104,8 @@
|
|||||||
|
|
||||||
Раздел **Внешний вид** объединяет настройки бренда и темы Web App. Логотип можно загрузить файлом или по HTTPS-ссылке; backend сохраняет файл в `data/webapp-logo/uploads` и подставляет локальный URL. Если логотип не задан, показывается логотип проекта по умолчанию. Favicon генерируется из логотипа или загружается отдельно.
|
Раздел **Внешний вид** объединяет настройки бренда и темы 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=<key>` и не меняет глобальную тему до сохранения.
|
В блоке тем админка читает каталог из `WEBAPP_THEMES_DIR`, показывает встроенные и кастомные темы, позволяет выбрать текущую тему, изменить accent, включить или выключить тему для админки и настроить масштаб логотипа на главной и экране входа. Кнопка предпросмотра открывает `/home?theme_preview=<key>` и не меняет глобальную тему до сохранения.
|
||||||
|
|
||||||
Подробный формат `theme.json`, CSS/asset-роуты и пошаговый пайплайн создания новой темы описаны в [webapp-themes.md](webapp-themes.md).
|
Подробный формат `theme.json`, CSS/asset-роуты и пошаговый пайплайн создания новой темы описаны в [webapp-themes.md](webapp-themes.md).
|
||||||
|
|||||||
@@ -44,6 +44,12 @@ BRUTE_FORCE_WINDOW_SECONDS=900
|
|||||||
BRUTE_FORCE_LOCK_SECONDS=900
|
BRUTE_FORCE_LOCK_SECONDS=900
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Брендинг писем
|
||||||
|
|
||||||
|
HTML-письма используют тот же бренд, что и Mini App: название из `WEBAPP_TITLE`, accent из внешнего вида и логотип из раздела **Внешний вид**. Если логотип загружен через админку файлом, backend прикладывает его к письму как inline image (`cid:webapp-logo`), поэтому получателю не нужен доступ к внутреннему `/webapp-uploaded-logo/...`.
|
||||||
|
|
||||||
|
Если в качестве логотипа задан публичный `https://` URL, письмо использует его как обычный внешний `<img>`. В этом режиме некоторые почтовые клиенты могут скрыть картинку, пока получатель не разрешит загрузку внешних изображений.
|
||||||
|
|
||||||
Для Brevo обычно подходит порт `587` с STARTTLS. Если основной порт недоступен, приложение пробует порты из `SMTP_FALLBACK_PORTS`; порт `465` используется через SSL wrapper автоматически.
|
Для Brevo обычно подходит порт `587` с STARTTLS. Если основной порт недоступен, приложение пробует порты из `SMTP_FALLBACK_PORTS`; порт `465` используется через SSL wrapper автоматически.
|
||||||
|
|
||||||
`SMTP_FROM_EMAIL` должен быть подтвержден у SMTP-провайдера, иначе письмо часто отклоняется или попадает в спам. `SMTP_FROM_NAME` можно оставить пустым, тогда используется название Web App.
|
`SMTP_FROM_EMAIL` должен быть подтвержден у SMTP-провайдера, иначе письмо часто отклоняется или попадает в спам. `SMTP_FROM_NAME` можно оставить пустым, тогда используется название Web App.
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ Minishop отправляет уведомления в Telegram и на email.
|
|||||||
|
|
||||||
Для уведомлений жизненного цикла подписки есть отдельный флаг `SUBSCRIPTION_EMAIL_NOTIFICATIONS_ENABLED`. Если он включен, пользовательские уведомления об окончании подписки отправляются в Telegram при наличии привязанного Telegram-аккаунта и на email при наличии привязанной почты.
|
Для уведомлений жизненного цикла подписки есть отдельный флаг `SUBSCRIPTION_EMAIL_NOTIFICATIONS_ENABLED`. Если он включен, пользовательские уведомления об окончании подписки отправляются в Telegram при наличии привязанного Telegram-аккаунта и на email при наличии привязанной почты.
|
||||||
|
|
||||||
|
Все HTML-письма используют общий email-шаблон с брендом из Web App: заголовком, accent-цветом и логотипом. Логотип, загруженный через раздел **Внешний вид**, отправляется как inline image, а публичный HTTPS-логотип остается внешней картинкой.
|
||||||
|
|
||||||
## Сводная таблица
|
## Сводная таблица
|
||||||
|
|
||||||
| Событие | Получатель | Telegram | Email | Условия и ограничения |
|
| Событие | Получатель | Telegram | Email | Условия и ограничения |
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ WEBAPP_DEFAULT_THEME=
|
|||||||
|
|
||||||
Важно: `WEBAPP_PRIMARY_COLOR` и `WEBAPP_LOGO_URL` больше не являются рабочим способом первичной настройки через `.env`. Эти значения редактируются в админке и сохраняются как overrides в базе. Тема при этом может использовать сохраненный primary color как fallback accent.
|
Важно: `WEBAPP_PRIMARY_COLOR` и `WEBAPP_LOGO_URL` больше не являются рабочим способом первичной настройки через `.env`. Эти значения редактируются в админке и сохраняются как overrides в базе. Тема при этом может использовать сохраненный primary color как fallback accent.
|
||||||
|
|
||||||
|
Email-шаблоны берут тот же бренд из настроек внешнего вида. Загруженный логотип добавляется в письма как inline image (`cid:webapp-logo`), а публичный HTTPS-логотип остается внешней картинкой, которую почтовый клиент может скрыть до разрешения загрузки изображений.
|
||||||
|
|
||||||
## Контракт `theme.json`
|
## Контракт `theme.json`
|
||||||
|
|
||||||
Минимальная тема:
|
Минимальная тема:
|
||||||
|
|||||||
@@ -326,5 +326,7 @@ def test_docs_email_preview_generator_renders_real_template_html():
|
|||||||
assert all(preview["html"].lstrip().startswith("<!DOCTYPE html>") for preview in previews)
|
assert all(preview["html"].lstrip().startswith("<!DOCTYPE html>") for preview in previews)
|
||||||
assert all('<html lang="ru"' in preview["html"] for preview in previews)
|
assert all('<html lang="ru"' in preview["html"] for preview in previews)
|
||||||
assert all('role="presentation"' in preview["html"] for preview in previews)
|
assert all('role="presentation"' in preview["html"] for preview in previews)
|
||||||
|
assert all('src="data:image/png;base64,' in preview["html"] for preview in previews)
|
||||||
|
assert all("cid:webapp-logo" not in preview["html"] for preview in previews)
|
||||||
assert all("mail-card" not in preview["html"] for preview in previews)
|
assert all("mail-card" not in preview["html"] for preview in previews)
|
||||||
assert all("email_" not in preview["subject"] + preview["html"] for preview in previews)
|
assert all("email_" not in preview["subject"] + preview["html"] for preview in previews)
|
||||||
|
|||||||
Reference in New Issue
Block a user