Ключ был объявлен дважды: как подзаголовок страницы списка методов
и как подпись поля формы. Побеждало последнее определение, поэтому
на странице «Платёжные методы» вместо «Настройка порядка и условий
отображения» показывалось «Описание».
Подзаголовок переименован в subtitle, пара description/descriptionHint
осталась за полем формы. В zh/fa подписи поля не было вовсе — там под
description лежал текст подзаголовка, поэтому форма показывала не то;
добавлены недостающие переводы вместо русского фолбэка.
Заодно форматирование AdminPaymentMethodEdit.tsx: шаг format:check
в CI не доходил до выполнения, пока падал lint.
Чинит красный lint в CI ветки dev.
Регистрация нового провайдера пополнения (api.cispay.app, карта и СБП)
во фронтенде: метка в METHOD_LABELS, иконка, узел настроек
payments_cispay и локали ru/en/zh/fa.
Бэкенд отдаёт метод динамически, поэтому страницы статистики и транзакций
подхватывают его автоматически через METHOD_LABELS и PaymentMethodIcon.
Со времён редизайна со скоуп-селектором граф строится только для
выбранных кампаний, партнёров или пользователей, и посмотреть сеть
целиком стало нечем. Бэкенд для этого уже есть — GET /cabinet/admin/
referral-network/ отдаёт полный граф и остался неиспользуемым после
того, как фронт перешёл на /scoped.
Возвращаем к нему доступ: в дропдауне селектора появляется закреплённый
пункт «Вся сеть», который грузит граф целиком. Режим взаимоисключающий
с выбором чипов — полный граф и так содержит все сущности.
White-label (per-method display_name + description in admin) was defeated on
balance/top-up pages: render used `translatedName || method.name` and
`translatedDesc || method.description`, so the bundled locale always won and
the admin override was ignored.
- Flip priority: admin-configured value first, locale as fallback
(TopUpMethodSelect, Balance)
- Add missing Description field to payment-method edit page
(mirrors Display name: description / reset_description)
- Add description to admin PaymentMethodConfig type
- Add description/descriptionHint i18n keys (ru, en)
Форма создания промокода переведена с жёсткого выбора одного типа на режим
«Набор бонусов»: галочками выбирается, что входит в промокод — бонус на
баланс, дни подписки, промогруппа — в любой комбинации. Тип для бэкенда
выводится из выбора (balance / subscription_days / balance_and_days /
promo_group), промогруппа едет через promo_group_id при любом типе (бэкенд
назначает её при активации независимо от типа).
Триал и процентная скидка остаются отдельными режимами: скидка
переиспользует те же колонки с другой семантикой (процент/часы) и не
комбинируется без миграции.
- список и статистика: лейбл/цвет нового типа balance_and_days, у составных
кодов видны оба значения (+руб и +дни)
- edit-режим восстанавливает чекбоксы из типа и promo_group_id
- унифицирована валидация формы (isValid = список ошибок пуст; для скидки
0 часов = «до первой покупки» — разрешено, как и было в isValid)
- локали ru/en/fa/zh
Парный PR бэкенда: remnawave-bedolaga-telegram-bot (тип BALANCE_AND_DAYS,
порядок эффектов дни→баланс, валидации, бот-визард).
Паритет с бот-кнопкой «✉️ Отправить сообщение»: в блоке действий карточки
юзера появляется кнопка (RBAC users:send_message), открывающая модалку с
текстом (HTML, лимит 4096) — бот отправляет юзеру прямое сообщение через
POST /admin/users/{id}/send-message.
Для email-only юзеров без telegram_id кнопка задизейблена с подсказкой;
коды ошибок бэкенда (no_telegram_id / forbidden / bad_request) маппятся
в честные локализованные сообщения. Локали ru/en/fa/zh.
Два новых типа записей таймлайна: button_click (нажатия кнопок в боте,
с текстом кнопки) и cabinet_action (мутационные действия юзера в
кабинете, метод + нормализованный путь). Чип-фильтр «Клики», иконки
BotIcon/BoltIcon, переводы во всех 4 локалях.
biome check, type-check и build проходят (pre-commit пропущен: biome
не установлен локально).
Единый таймлайн действий юзера в боте и кабинете
(GET /cabinet/admin/users/{id}/activity): вертикальная лента с
иконками-кружками по типу записи, бейджи подтипов, суммы со знаком
(+зелёный / −красный), относительное время (абсолютное — в title),
чипы-фильтры по категориям (платежи, события, промо, тикеты, подарки,
рефералы, входы), счётчик StatCard и постраничная догрузка по house
load-more паттерну. Компонент самодостаточный (ActivityTab), как
TicketsTab. Переводы во всех 4 локалях; время переиспользует
admin.auditLog.time.*.
biome check, type-check и build проходят (pre-commit пропущен: biome
не установлен локально — бинарь есть только в CI).
Парный к боту #3049. В /admin/legal-pages под полем политики/оферты/правил:
постоянный счётчик символов + предупреждение с оценкой числа сообщений бота
(estimateTelegramParts зеркалит split_telegram_text, порог 3500). Скрыто там,
где документ в боте не показывается (recurrent-payments, web-only); счётчик
остаётся. Переводы ru/en с плюральными формами, прочие локали — en fallback.
Валидация: JSON-ключи, tsc, biome lint/format, build — чисто.
Кабинет, в отличие от админки бота (лимит 4000 символов), позволяет
сохранять юридические тексты любой длины — и до парного фикса в боте
(BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot#3049) текст длиннее
~4096 символов ронял показ правил и политики при /start
(MESSAGE_TOO_LONG).
Теперь под текстовым полем политики, оферты и правил постоянно
показывается счётчик символов, а для текстов длиннее одного сообщения —
предупреждение с оценкой, на сколько сообщений бот разобьёт текст.
Оценка зеркалит жадную упаковку абзацев из split_telegram_text бота
(порог 3500 символов на кусок).
Предупреждение не показывается там, где оно неуместно:
- «Рекуррентные платежи» — документ не отображается в боте вовсе;
- режим отображения «Только веб» — бот документ не показывает.
Переводы добавлены в ru/en (с плюральными формами); остальные локали
используют английский fallback через defaultValue.
UI для купонов из BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot#3042
(API: PR #3044 бота):
- /admin/coupons — список партий с погашениями и offset-пагинацией;
- /admin/coupons/create — мастер партии (тариф, дни, количество,
учётная оптовая цена, срок), после создания — экран со ссылками
(копирование всех, скачивание .txt);
- /admin/coupons/:id — карточка со статистикой, экспорт актуальных
ссылок, отзыв непогашенных за PermissionGate coupons:edit
с подтверждением и тостом;
- /coupon/:token — публичная страница купона: тариф/срок, кнопка
активации в боте, для залогиненных — активация прямо в кабинете
с маппингом структурированных кодов ошибок {code, message};
- пункт меню в разделе тарифов (гейт coupons:read), лениво
загружаемые роуты, локали ru/en (zh/fa падают на ru-фолбэк).
Backend now serves a dedicated recurring-payments legal document, so:
- Add adminLegalPagesApi.get/updateRecurrentPayments and infoApi.getRecurrentPayments
(+ InfoVisibility.recurrent).
- AdminLegalPages: new 'Рекуррентные платежи' editor tab (DocumentEditor now driven
by a DOCUMENT_API dispatch map instead of privacy/offer ternaries).
- PublicLegal: add 'recurrent' doc; add public /recurrent-payments route.
- Re-add the recurrent link to the login LegalFooter (was removed pending backend).
- i18n: admin.legalPages.tabs.recurrent for ru/en/fa/zh (footer.recurrent already existed).
type-check, eslint, prettier and build all pass.
The login-page footer linked to /offer, /privacy and /recurrent-payments with
target=_blank, but none of those routes existed, so opening them hit the SPA
catch-all, which redirects unauthenticated users to /login — every footer link
just opened another login tab.
- Add a public PublicLegal page that fetches the offer / privacy document from the
existing public /cabinet/info endpoints and renders sanitized HTML (DOMPurify via
the shared formatContent helper).
- Register public /offer and /privacy routes.
- Extract sanitizeHtml/formatContent into src/utils/legalContent.ts and reuse from
Info.tsx (was duplicated).
- Drop the /recurrent-payments footer link: there is no recurring-payments legal
document or endpoint in the backend, so it had no real destination. Re-add once
such a document exists.
type-check, eslint, prettier and build all pass.
Adds a legal footer (Public Offer / Privacy / Recurring Payments) under the
login card, gated by CABINET_FOOTER_ENABLED (backend endpoint added in bot
PR #3011), plus an admin toggle in BrandingTab and ru/en/fa/zh i18n.
Footer-only subset of PR #464 — excludes the unrelated storePartnerClickId
helper and the version/CHANGELOG churn.
Co-authored-by: smediainfo <dev@matrixvpn.top>
- useFeatureFlags: кэш флагов в localStorage — нижняя навигация больше
не прыгает на холодном старте (таб «Рефералы»/«Колесо» появлялся после
загрузки terms/config)
- applyThemeColors: частичный/битый ответ /branding/colors добивается
дефолтами вместо падения всего приложения в ErrorBoundary
- карточка «Связаться с поддержкой»: кнопка больше не дублирует заголовок
(«Написать», без переноса)
- «Мои подписки»: заголовок и «Купить ещё» в одну строку на 390px
- StatCard: подписи плиток переносятся на две строки вместо обрезки
многоточием («Бонус новому польз…»)
Companion to the bot-side change: the variables hint now renders two
groups — type-specific vars and common ones ({service_name},
{cabinet_url}, {support_username}, {username}, {email}, {date}) that
work in every template. The block is visible even for types with no
vars of their own (previously it was hidden entirely, looking like
placeholders didn't exist). Common chips insert at cursor the same way.
Companion to the bot-side fix: defaults now arrive with {placeholders}
instead of baked-in sample values (example.com links).
- inline preview tab (iframe srcDoc) — works on mobile and in Telegram;
the separate desktop-only preview page and its route are removed
- preview/test render the CURRENT editor content with sample values
- test email: recipient input + detailed backend error messages
- variable chips insert at cursor instead of copying to clipboard
- 'insert default template' button to migrate broken saved overrides
- fa added to language tabs
- health probe: tolerant timeout (12s) + retry before flagging the backend down. A
hardcoded 5s probe racing auth bootstrap falsely showed ServiceUnavailableScreen on
slow devices / cold mobile connections while the 30s API requests would have
succeeded ("works on one device, not another on the same Wi-Fi"). The recovery poll
self-reschedules with the tolerant timeout so slow devices auto-recover.
- TopUpAmount: fetch payment methods with a real query (fixes the infinite spinner on a
cold cache / browser-back) and use canonical RUB for quick-amount chips so FX rounding
can't reject a min-amount selection in non-RUB locales.
- settings UI: render secret values as a masked password input (pairs with the backend
secret masking); leaving the field empty keeps the stored secret.
- deps: npm audit fix (18 -> 5 advisories).
Also bundles in-progress settings env-lock UI work.
The hint claimed the provider opens 'inside the MiniApp/tab' — that is the broken behavior that caused #654272 (a same-WebView navigation to the provider can't hand off to a bank app / SBP). After the fix the link opens in the EXTERNAL browser inside Telegram regardless of the toggle. Rewrote the hint in all four locales: ON = open immediately (no button panel), OFF = show an 'Open payment' button; both open externally in Telegram so bank-app/SBP payments work; Stars/CryptoBot (t.me/ links) always open natively and ignore the flag.
Note: the external-browser open is unconditional (both toggle states, all methods incl. WATA) — the toggle only controls auto-open vs a button tap.
Map promocode errors by stable backend code (detail.code) instead of brittle English
substring matching, so previously-unmapped codes (active_discount_exists, daily_limit,
no_subscription_for_days, trial_*, etc.) show the right message instead of 'server error';
added the missing keys to all 4 locales.
valid_until now anchors to end-of-day in the admin's LOCAL timezone on both create AND
edit read-back, so codes no longer expire at the start of the day and editing no longer
drifts the date forward for negative-offset admins.
Allow 0-hour discount ('until first purchase'). Route user-facing discount deactivation
to the endpoint that rolls back promocode usage when the discount came from a promocode,
and parse the structured {code,message} error.
Statistics tab now uses the shared StatCard + BreakdownList (ranked bars +
share %) and the currency-aware formatter — matching the rest of the admin
stats instead of ad-hoc 'card p-4 text-center' / text-3xl tiles — and gains
proper loading, error and empty states.
Also surface backend-supported config the form couldn't reach (forcing DB edits):
- promo_validity_days (model + schema-bounded 1-365) is now editable in Settings.
- per-prize manual_probability override (0-1) now has an input in the prize form.
When linking an email that belongs to another account, the backend now mails a
one-time code to it (merge_verification='email_code') instead of asking for that
account's password. Add a 6-digit code step: on the email_code response show the
code input, verify via authApi.verifyEmailMerge → on success redirect to /merge.
Preview/execute already go through apiClient (JWT), so they satisfy the new
initiator-bound merge endpoints. i18n in ru/en/zh/fa.
The backend account-takeover fix now requires the existing account's password
before merging an email that already belongs to another account. Show a clear
message (profile.emailMergePasswordRequired) on that response instead of the
generic "already registered" dead-end, so the legitimate owner knows to enter
that account's password (in both password fields) to complete the merge. i18n
in ru/en/zh/fa.
Two Telegram-Mini-App follow-ups to the backend-unavailable screen:
- Reliable "Close" button (Telegram only) that actually EXITS the Mini App
instead of routing back. closeTelegramApp() tries the legacy
window.Telegram.WebApp.close() global (telegram-web-app.js), then the SDK
closeMiniApp(), then the raw postEvent('web_app_close') — all emit the same
close event, so it can't silently fail. Guarded so the first path never
silently no-ops.
- Eliminate the flash of the /login page before the outage screen. On the
bootstrap path (the app has never reached the backend) reportPossibleBackendDown()
now flips the screen IMMEDIATELY and synchronously instead of waiting on the
confirm probe, so the overlay is up before isLoading flips and /login can never
paint uncovered. Add an eager checkBackendOnStartup() liveness ping at launch
(parallel with auth) so even the no-stored-token / fresh-Telegram path that
makes no early request shows the screen at once. The confirm-probe still guards
already-loaded sessions from a one-off blip.
i18n close key added in ru/en/zh/fa.