Ключ был объявлен дважды: как подзаголовок страницы списка методов
и как подпись поля формы. Побеждало последнее определение, поэтому
на странице «Платёжные методы» вместо «Настройка порядка и условий
отображения» показывалось «Описание».
Подзаголовок переименован в 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.
Поверх #491: #489 уже в dev — записи resetPassword.*/expiredDate_trial и
placeholder-исключение serverAccess из allowlist удалены (по инструкции
самого теста); последняя дыра admin.remnawave.traffic.realtimeTitle закрыта
переводом — проверка синхронности теперь строгая, без известных исключений
Со времён редизайна со скоуп-селектором граф строится только для
выбранных кампаний, партнёров или пользователей, и посмотреть сеть
целиком стало нечем. Бэкенд для этого уже есть — 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.
The bot backend now rejects prorated tariff switches from a free (0RUB)
tariff with 400 free_tariff_cannot_switch + use_purchase_flow=true
(TARIFF_SWITCH_RESET_FREE_DAYS: the free remainder must reset, not be
prorated over hundreds of accumulated days and carried to a paid
tariff). Handle it like the existing trial_cannot_switch contract:
- SwitchTariffSheet: accept free_tariff_cannot_switch in
shouldUsePurchaseFlow so the sheet hands off to the purchase form
- TariffPickerGrid: new subscription_on_free_tariff purchase-options
flag disables the switch CTA up front and shows purchase instead
Companion to BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot#3056.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Два новых типа записей таймлайна: 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).
Кнопка «Активировать триал» в rich-меню бота при платном триале ведёт в
миниапп: startapp=trial открывает дашборд, где TrialOfferCard показывает
цену и flow оплаты с баланса.
biome check, type-check и build проходят (pre-commit пропущен: biome не
установлен локально — бинарь есть только в CI).
StartParamNavigator обобщён до таблицы маршрутов: помимо admin_ticket_<id>
теперь разбираются renew_<id> → /subscriptions/<id>/renew и subscriptions →
/subscriptions — их шлют ссылки «Продлить» у истёкших подписок в rich-меню
бота (t.me/<bot>/<app>?startapp=…). Доступ по-прежнему ограничен гвардами
самих маршрутов.
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 — чисто.
framer-motion stagger раздаёт детям команду показаться один раз при монтировании
родителя; дети, смонтированные позже (когда resolve-ится react-query), оставались
в initial (opacity:0) — пустые страницы после F5/прямого захода.
- ConnectedAccounts, SavedCards: key={isLoading?...} перемонтирует контейнер
после загрузки → stagger проигрывается со всеми детьми;
- Balance (2), Profile, Support: явные initial/animate на условных staggerItem —
элемент самоанимируется при mount, вне оркестрации родителя;
- Wheel: исправлены несуществующие варианты hidden/show → initial/animate
(stagger истории спинов молча не работал).
Валидация на смердженном дереве: tsc, biome lint/format, build — чисто.
Правки #477 и #478 в общих файлах (ConnectedAccounts/Profile) сосуществуют.
FastAPI/Pydantic на 422 кладёт в detail массив объектов {type,loc,msg,...};
компоненты рендерили его в JSX как есть → React падал в ErrorBoundary
(Objects are not valid as a React child), пользователь не видел причину.
Все такие обработчики (Login, Profile, ConnectedAccounts, admin AnalyticsTab
и MenuEditorTab) переведены на существующий getApiErrorMessage — строковый
detail возвращается как есть (ветки .includes работают), Pydantic-массив
разворачивается в строку field: msg; ..., любой формат безопасен для рендера.
Валидация на смердженном дереве: tsc, biome lint/format, build — чисто.