Commit Graph

300 Commits

Author SHA1 Message Date
Fringg
e840ed2b60 feat(admin): панельная идентичность под Remnawave 3.0.0
Remnawave 3.0.0 удалил `uuid` из модели пользователя — панельный аккаунт
адресуется числовым `id`, и бот переименовал соответствующие поля своего
API. Без этих правок вкладка Sync показывала бы «не привязан» у ВСЕХ
пользователей: оба операнда стали бы undefined, причём без ошибки TS —
интерфейсы продолжали бы декларировать несуществующие поля.

Типы: `UserDetailResponse.remnawave_uuid` → `remnawave_id: number | null`,
`PanelSyncStatusResponse.remnawave_uuid` → `remnawave_id`,
`SyncToPanelResponse.panel_uuid` → `panel_user_id`,
`PanelUserInfo.uuid` → `id: number` (бэкенд отдаёт его обязательным).

В SyncTab используется `??`, а не `||`: идентификатор числовой, и `0`
нужно отличать от отсутствия значения. Ярлык — «Remnawave ID».

Локаль: ключ настройки бота переименован в TRAFFIC_EXCLUDED_USER_IDS,
подпись обновлена — значения теперь числовые id, а не UUID.
2026-08-02 19:38:11 +03:00
Fringg
0f21629ba5 feat(auth): чекбоксы согласия с офертой и политикой при первой авторизации
Новый пользователь попадал в кабинет молча — никаких документов ему не показывали.
Теперь при создании аккаунта он подтверждает, что ознакомился с офертой и политикой.

Набор документов и сам факт включения гейта приходят с бэка
(GET /cabinet/info/legal-consent): там же учитывается, заполнены ли документы и
видны ли они в вебе. Фронт знает только ключи и адреса публичных страниц /offer
и /privacy — тех же, что уже линкует футер логина.

Два пути входа устроены по-разному, поэтому и UI разный:
- регистрация по email — чекбоксы прямо в форме, кнопка заблокирована до галочек;
- Telegram-вход происходит сам собой, спрашивать заранее не у чего. Поэтому ловим
  428 от бэка и показываем отдельный экран согласия, а замыкание помнит, какой
  именно вход повторить после простановки галочек. Тот же приём страхует и
  email-форму: конфиг мог протухнуть, если админ включил гейт между загрузкой
  страницы и отправкой.

Неизвестный ключ документа всё равно рисуем (без ссылки): бэк его требует, и молча
спрятанный чекбокс превратился бы в «кнопка не работает и непонятно почему».

Локали ru/en/zh/fa.
2026-07-30 00:31:47 +03:00
Fringg
390da33914 feat(coupons): лимит на пользователя и удаление партии в кабинете
Парная фронтовая часть к бэкенду (бот: 82609441).

- Поле «Лимит на пользователя» в форме создания партии: 0 — без
  ограничения, для раздач и конкурсов ставится 1. Значение показывается в
  карточке партии, если задано.
- Кнопка «Удалить партию» с подтверждением: отдельно от отзыва, который
  лишь гасит ссылки. Если в партии есть погашенные купоны, подтверждение
  предупреждает о потере истории активаций (выданные подписки не
  отзываются). После удаления — возврат к списку.
- API: max_per_user в типах партии и запросе создания, deleteBatch.

Локали ru/en — раздел admin.coupons в zh/fa отсутствует целиком (купоны там
никогда не переводились), поэтому новые ключи добавлены в том же объёме.
2026-07-29 22:02:07 +03:00
Fringg
c9afea7a50 feat(branding): загрузка видео стартового меню бота
Парная фронтовая часть к /cabinet/branding/bot-start-video (бот: 524960c3).
В «Брендинге» появилась карточка: загрузить/заменить/удалить видео, которое
бот прикрепляет к сообщению /start вместо картинки-логотипа, и статус
текущего состояния. Файл разово уходит в Telegram ради file_id — на нашей
стороне не хранится.

Видео в рассылках уже работало (кнопка в боте, загрузка в кабинете,
доставка send_video) и не затронуто. Локали ru/en/zh/fa.
2026-07-29 21:28:48 +03:00
Fringg
a28f1167f7 feat(lava): покупка привязкой, продукт тарифа в админке, CTA
Фронтовые хвосты автопродления Lava (бэкенд: de3518b7).

- Поле «Продукт Lava» в форме создания/редактирования тарифа: раньше
  lava_product_id можно было задать только через API, из-за чего фичу нельзя
  было включить из интерфейса. Пустая строка отвязывает тариф от продукта.
- CTA «Оформить с автооплатой Lava» в форме покупки тарифа — рядом с
  СБП-кнопкой, в обеих ветках рендера; флаг lava_recurrent_enabled из
  purchase-options наконец получил потребителя.
- API-метод purchaseWithLavaRecurring + типы lava_product_id в списке,
  детали и запросах тарифа.
- Локали ru/en/zh/fa: подписи покупки и поля продукта (плейсхолдеры сверены).
2026-07-29 20:23:35 +03:00
Fringg
2dce539da2 feat(lava): автопродление Lava на странице подписки
Парная фронтовая часть к эндпоинтам /cabinet/subscription/lava-recurrent
(бот: 0dea2182). Блок — сиблинг Platega-блока с той же семантикой состояний
(off/pending/active/past_due), поллинг раз в 8с пока привязка PENDING.

Отличие от Platega в подписи: у Lava период задан продуктом в кабинете
провайдера и приезжает числом дней, поэтому канонические значения (30/90/365
и т.д.) показываются словами, а произвольные — как «раз в N дн.».

utils/lavaRecurring.ts: строгое распознавание 403 'Lava recurrent disabled'
(другие 403-guard'ы прятать нельзя), деградация неизвестного статуса в off.
Локали ru/en/zh/fa (+25 строк каждая, плейсхолдеры сверены). 7 тестов утилиты.
2026-07-29 16:34:43 +03:00
kewldan
c6d8c0afe0 feat(promo-offers): охват сегментов и прогресс доставки на странице отправки
Сегмент выбирался вслепую — в списке не было числа пользователей, а после
отправки экран показывал только количество созданных офферов: сколько
человек реально получило предложение и сколько заблокировало бота, узнать
было негде.

В селекте сегментов теперь стоит охват каждого (GET /promo-offers/segments),
под ним — сколько уйдёт по выбранному. После отправки экран результата
показывает прогресс доставки с карточками Всего/Отправлено/Заблокировали/
Ошибки и опрашивает запись рассылки раз в 3 секунды, пока она не завершится.

Прогресс-блок и бейдж статуса вынесены из AdminBroadcastDetail в
BroadcastDeliveryStats и переиспользуются обеими страницами, а условие
«доставка ещё идёт» — в утилиту broadcastStatus с тестами.
2026-07-28 22:24:58 +03:00
Fringg
f448c14994 feat(sbp-recurring): СБП-оформление подписки на форме покупки тарифа
Вторая CTA «Оформить с автооплатой СБП» под кнопкой покупки с баланса
(и в daily-ветке) — показывается по platega_recurrent_enabled из
purchase-options. POST /platega-recurrent/purchase → редирект в банк
(первое списание = подтверждение привязки), инвалидация подписочных и
sbp-запросов, переход в /subscriptions. Локали en/ru/zh/fa.
2026-07-24 19:07:23 +03:00
Fringg
2cd494e949 Merge PR #508: различать устаревшего бота и ненастроенного на логине
404 на /cabinet/branding/telegram-widget и deeplink-роутах = сборка бота
старше v3.24/v3.33 — вместо «бот не настроен» показываем auth.botOutdated
(4 локали). README: фактический минимум v3.33.0. Закрывает #345.
Спасибо @kewldan.
2026-07-24 15:00:36 +03:00
Fringg
9093aec207 feat(subscription): API и утилиты СБП-автопродления Platega 2026-07-24 02:14:41 +03:00
kewldan
b3b07c322d fix(auth): различать устаревшего бота и ненастроенного на экране логина 2026-07-23 04:09:06 +03:00
Egor
6d1513092b Merge PR #496: кнопка построения полной реферальной сети 2026-07-19 16:36:28 +03:00
Egor
424d77455e Merge PR #493: состав промокода чекбоксами — баланс / дни / промогруппа 2026-07-19 16:29:08 +03:00
kewldan
19facf2ba9 feat(referral-network): кнопка построения полной реферальной сети
Со времён редизайна со скоуп-селектором граф строится только для
выбранных кампаний, партнёров или пользователей, и посмотреть сеть
целиком стало нечем. Бэкенд для этого уже есть — GET /cabinet/admin/
referral-network/ отдаёт полный граф и остался неиспользуемым после
того, как фронт перешёл на /scoped.

Возвращаем к нему доступ: в дропдауне селектора появляется закреплённый
пункт «Вся сеть», который грузит граф целиком. Режим взаимоисключающий
с выбором чипов — полный граф и так содержит все сущности.
2026-07-16 15:32:17 +03:00
Case211
9ce838bade feat(promocodes): состав промокода чекбоксами — баланс / дни / промогруппа
Форма создания промокода переведена с жёсткого выбора одного типа на режим
«Набор бонусов»: галочками выбирается, что входит в промокод — бонус на
баланс, дни подписки, промогруппа — в любой комбинации. Тип для бэкенда
выводится из выбора (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,
порядок эффектов дни→баланс, валидации, бот-визард).
2026-07-14 08:52:06 +05:00
Case211
2f743d82db feat(users): кнопка «Отправить сообщение» в карточке пользователя
Паритет с бот-кнопкой «✉️ Отправить сообщение»: в блоке действий карточки
юзера появляется кнопка (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.
2026-07-14 07:28:33 +05:00
Egor
eeb715c095 Merge PR #484: поле icon_custom_emoji_id у кастомных кнопок рассылки
feat(broadcasts): поле icon_custom_emoji_id у кастомных кнопок рассылки
2026-07-13 15:13:22 +03:00
Fringg
7547b49d7a feat(admin): вкладка «Активность» в карточке пользователя
Единый таймлайн действий юзера в боте и кабинете
(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).
2026-07-13 03:34:07 +03:00
kewldan
b4b4bca2cb feat(broadcasts): поле icon_custom_emoji_id для кастомных кнопок рассылки 2026-07-13 02:34:46 +03:00
Fringg
9f840a9a47 Merge PR #474: миграция тулинга с ESLint + Prettier на Biome
Замена ESLint + Prettier (9 dev-зависимостей) на Biome (одна): линт ~10×,
формат-чек ~49× быстрее. Паритет форматирования (single quotes/semi/width
100/trailing/arrowParens/jsxDouble) и Telegram WebView-гвардов (перенесены
GritQL-плагином). Имена npm-скриптов lint/format:check сохранены — CI не тронут.

Ревью-фиксы поверх PR (восстановление точного паритета):
- grit-исключение сужено до якорного src/platform/ (было bare-substring
  "platform", щадившее любой файл с этим словом в имени)
- вернул *.min.js/*.min.css в biome ignore (были в .prettierignore)
- вернул *.css в lint-staged pre-commit
- поправил устаревшую метку CI-шага
2026-07-12 01:36:15 +03:00
kewldan
cf3837163e chore(tooling): миграция с ESLint + Prettier на Biome
Один инструмент вместо девяти dev-зависимостей. Замеры на этом репо
(483 файла, медиана из 3 прогонов, один и тот же компьютер):

- eslint .            ~20.4s  ->  biome lint .    ~2s
- prettier --check .  ~8.8s   ->  biome format .  ~0.2s
- суммарно линт+формат ~29s   ->  biome check .   ~2-3s (~10x)

Паритет с прежними правилами:
- форматирование бинарно совместимо с Prettier-конфигом (biome format
  --write тронул 9 файлов из ~480 — микроразличия);
- запрет window.confirm/alert/open/prompt и navigator.clipboard
  (Telegram WebView) перенесён GritQL-плагином
  biome-plugins/telegram-webview-guards.grit c теми же сообщениями
  и исключениями для src/platform/** и clipboard.ts; плагин сразу
  нашёл window.open в Wheel.tsx под старым eslint-disable —
  переведён на biome-ignore;
- react-hooks-правила: useExhaustiveDependencies/useHookAtTopLevel;
- новые для проекта правила (a11y и пр.) не включались или понижены
  до warning, чтобы миграция не смешивалась с чисткой кода — включать
  можно отдельными PR.

Ограничения (задокументированы): сортировка tailwind-классов
(prettier-plugin-tailwindcss) в Biome пока nursery — не включена;
CSS исключён из Biome (парсер спотыкается о Tailwind-синтаксис
globals.css) — файл один и правится редко.

CI не меняется: имена npm-скриптов lint/format:check сохранены.
lint-staged переведён на biome check --write.
2026-07-12 00:12:15 +03:00
kewldan
799e986d84 feat(coupons): раздел оптовых купонов + публичная страница активации
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-фолбэк).
2026-07-11 23:52:13 +03:00
Fringg
20565b8f59 feat(legal): wire recurrent-payments document + restore footer link
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.
2026-07-09 18:47:24 +03:00
Fringg
984d1b0776 feat(cabinet): legal footer on login + admin toggle
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>
2026-06-30 07:30:59 +03:00
Fringg
fd5500c85b fix: align SubscriptionRequestRecord type with Remnawave 2.8.0
The panel renamed the subscription-request-history field userUuid (uuid)
-> userId (number) in 2.8.0. The field is never read in the UI (render
uses id/requestAt/requestIp/userAgent), so this is a type-accuracy fix
only. tsc --noEmit passes.
2026-06-30 01:43:06 +03:00
Boris Kovalskii
f52360449b feat(admin): overpay certificate upload block
(cherry picked from commit b51b9d7d45db7b4597838d25a475291e453c7a7a)
2026-06-16 06:31:46 +03:00
Fringg
6721e25df6 fix(auth): send Bearer token for account merge endpoints
Remove '/cabinet/auth/merge/' from AUTH_ENDPOINTS so the request
interceptor attaches the Authorization header. The backend merge
preview/execute endpoints require an authenticated initiator
(get_current_cabinet_user); omitting the token produced
401 'Authentication required' and merges never completed after
Telegram confirmation or email-code verification.
2026-06-16 02:00:18 +03:00
Boris Kovalskii
515c3d73b8 fix(admin): harden legal pages editor state handling 2026-06-11 11:14:56 +10:00
Boris Kovalskii
04cbbb5e51 feat: add display mode selector to info page editor 2026-06-11 11:14:56 +10:00
Boris Kovalskii
e979aa8863 feat: add admin legal pages api client and display mode types 2026-06-11 11:14:56 +10:00
c0mrade
8b2501d908 feat(admin): show common placeholders group in email template editor
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.
2026-06-10 23:37:31 +03:00
c0mrade
ac75a806d9 fix(admin): rework email template editor (#667043)
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
2026-06-10 22:53:08 +03:00
c0mrade
16fad9f4fe fix(cabinet): recover "Сервис недоступен" false-positive + top-up fixes
- 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.
2026-06-10 16:15:55 +03:00
c0mrade
73ee42a0ef fix(wheel): stop showing fake wins in the browser
After a Stars payment the wheel matched its landing sector to the won prize
by display_name/emoji and, on any miss, span to Math.random()*360 — which
often pointed at a winning slot. The poll result also carried the SPIN id in
prize_id (not the WheelPrize id), so it never matched and always hit that
random fallback. In a backgrounded browser tab the poll often times out,
so the wheel 'landed' on месяц/50₽ while the real result was Ничего; the
Mini App was fine because it gets the result inline.

- Match the sector by prize_id (exact), with name/emoji only as a defensive
  fallback for old payloads.
- Never land on a random angle: when the prize can't be resolved (poll
  timeout/error) land on the neutral 'Nothing' sector instead.
- Carry the real WheelPrize id (now exposed by the bot history endpoint)
  through the poll result.
2026-06-04 22:15:43 +03:00
c0mrade
3224320095 feat(connected-accounts): email-merge confirmation via emailed one-time code
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.
2026-06-04 19:06:54 +03:00
c0mrade
c6fc167d80 fix(security): use signed media tokens for ticket attachments
Backend now requires a signed, expiring token to download media (a raw file_id
404s). Carry the per-attachment `token` (and the legacy `media_token`) from the
ticket response through MessageMediaGrid and append it in ticketsApi.getMediaUrl,
so attachments keep rendering while leaked URLs expire. Must deploy together with
the backend change.
2026-06-04 17:45:43 +03:00
c0mrade
7413837faf feat(cabinet): bulletproof Close button + kill the /login flash on the service-unavailable screen
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.
2026-06-04 13:06:05 +03:00
c0mrade
ac8a0fc41a feat(cabinet): recoverable "service unavailable" screen when the backend is unreachable
When the backend was down the cabinet got stuck on a blank loader: the bootstrap
token refresh used bare axios with no timeout (hung forever), and every
interceptor guard required an HTTP response — so a transport-level failure had
zero handling and no error UI ever appeared.

Add a full-screen ServiceUnavailableScreen (extends the existing blocking-store
pattern) shown whenever the backend is unreachable, that auto-recovers when it
returns:
- New 'backend_unavailable' blocking type + BlockingOverlay branch; mirrors
  MaintenanceScreen, CloudWarningIcon, i18n in ru/en/zh/fa.
- api/health.ts: pingBackend() probes the root /health/unified (bypasses the
  /api baseURL + interceptor); URL derived from the origin for remote/sub-path
  deploys; 502/503/504 count as down. reportPossibleBackendDown() confirms an
  outage with a liveness probe before flipping, so a one-off blip never blanks a
  loaded app.
- Detected from the response interceptor (no-response) and the bootstrap refresh
  path; doRefresh gets a timeout so it can no longer hang.
- A transport failure during refresh now PRESERVES the session (distinguished
  from a rejected token via lastFailureWasTransport) instead of logging the user
  out, so recovery actually resumes.
- Recovery lifts the overlay + refetches for an already-loaded session, and only
  hard-reloads when the initial bootstrap never reached the backend — no lost
  form state. Manual retry + 5s auto-poll. Dev proxy for /health.
2026-06-04 12:44:27 +03:00
c0mrade
78c633ff54 feat(landing-stats): rebuild charts on shared components + add funnel & breakdowns
The landing stats page rendered raw recharts by hand plus a bespoke progress-bar
list, looking nothing like the Sales/Remnawave stats. Rebuild it on the shared
chart components and add the visualizations the data warrants:

- Daily: MultiSeriesAreaChart (created/paid) + SimpleAreaChart (revenue),
  replacing the hand-rolled area chart and the custom progress-bar list.
- Tariff & traffic sources: BreakdownList; payment methods & gift composition:
  DonutChart — all the same components Sales stats uses.
- New conversion Funnel (created → paid) and a Gift activation card
  (claimed/sent + rate), backed by the new backend breakdowns.
- Containers use the bento-card surface; i18n added in ru/en/zh/fa.
2026-06-03 15:11:47 +03:00
c0mrade
b9521f1aa7 fix(gift): cabinet GiftResult shows claim link for directed gifts + review fixes
Pairs with the backend cabinet-unification. Directed cabinet gifts now stay
PAID/claimable, so the buyer's GiftResult must show the share link for them too.

- GiftResult.tsx: render the share state (and stop polling) for ANY claimable
  paid gift via is_claimable, not just is_code_only.
- api/gift.ts: GiftPurchaseStatus gains is_claimable.
- GiftClaim.tsx: explicit failed/expired state (was falling into the 'almost
  ready' spinner forever).
- PurchaseSuccess.tsx: a paid gift is always claimable — stop polling and show
  the share block on `paid && is_gift` (don't gate on is_claimable, which could
  strand the buyer on a spinner).
- i18n: landing.giftClaim.failedTitle/failedDesc in ru/en/zh/fa.
2026-06-03 14:34:19 +03:00
c0mrade
db31d395cd feat(gift): transferable gift claim — buyer share link + recipient claim page
Pairs with the bot's unified claimable-gift model. A landing gift now lands on
the buyer's success page as PAID/claimable (deferred) instead of being delivered
to a phantom recipient, so the buyer gets a transferable link to forward and the
recipient claims it themselves — guaranteeing delivery regardless of channel or
whether they pre-exist.

- api/landings.ts: PurchaseStatus gains is_claimable/claim_url/bot_claim_link;
  add GiftClaimResult + getGiftClaim()/claimGift().
- PurchaseSuccess.tsx: GiftLinkShareState — buyer sees the claim link + Telegram
  link + copy-message when a gift is PAID/claimable; stop polling for that state
  (it stays PAID until claimed) instead of spinning forever.
- GiftClaim.tsx (new) + /buy/gift/:token route: public recipient claim page with
  a Telegram arm (GIFT_ deep link) and a web/email arm (binds + one-click login),
  polling while the payment settles, idempotent "already claimed" + 404 states.
- i18n: landing.giftClaim.* / landing.giftLink.* in ru/en/zh/fa.
2026-06-03 13:46:00 +03:00
c0mrade
5a6e4589c8 fix(landing): заголовок «Loading...» и пустая иконка вкладки на лендингах
Публичный лендинг (QuickPurchase) рендерится вне AppShell, а useBranding
с заголовком/иконкой завязан на авторизацию — поэтому на лендинге вкладка
оставалась с дефолтным «Loading...» из index.html (тайтл обновлялся только при
заданном meta_title) и без favicon (href="data:,").

- index.html: нейтральная дефолтная иконка-монограмма вместо пустого data-URI
  и тайтл «VPN» вместо «Loading...».
- QuickPurchase: тайтл берётся из meta_title || title лендинга; favicon
  ставится из брендинга (кастомный лого-блоб или буквенная монограмма), брендинг
  тянется из публичного эндпоинта.
- favicon-хелперы вынесены в utils/favicon.ts; useBranding переведён на них
  (DRY) + добавлен буквенный фолбэк, чтобы иконка была всегда.
- Тип LandingTariff дополнен is_daily/daily_price_kopeks (синхронно с бэком).
2026-06-03 11:54:23 +03:00
c0mrade
3adda6fa03 feat(sales-stats): конкретика в сводке
- «Активные подписки» теперь с % (нетто-изменение за период: новые
  платные − истёкшие; обычная дельта снимка была бы всегда 0).
- Новая карточка «Новые платные подписки» (с дельтой период-к-периоду).
- «Доход» больше не включает ручные пополнения (бэк отвязал).
- Убрал «Средняя длит.» из триалов — была вечный 0 и непонятна.
- Сводка 9 карточек: сетка 2/3 с orphan-fill на мобайле, без пустот.
2026-06-02 19:38:38 +03:00
c0mrade
436d9f860c feat(sales-stats): вкладка «Оплаты» — success-rate и неудачные покупки
Новая вкладка в статистике продаж:
- success-rate платежей (оплачено/создано) — общий + по каждому шлюзу с
  цветными барами (зелёный ≥90%, жёлтый ≥70%, красный ниже);
- кол-во попыток оплат;
- «Неудачные покупки» — внутренние откаты при ошибке покупки (сигнал
  надёжности, не возвраты).
Данные из нового эндпоинта /payment-health. Шлюзы без активности не
показываются. Карточки 1/3 (без сирот), адаптив. 4 локали.
2026-06-02 18:46:42 +03:00
c0mrade
ec23618a56 revert(sales-stats): убрать карточки возвратов из сводки
Возвраты для оператора всегда 0 (реальные возвраты только Apple/Tribute,
которых нет) — постоянно пустые карточки только путают. Сводка вернулась
к 8 карточкам (grid 2/4). Убрал также неиспользуемый invertDelta из
StatCard и ключи refunds из локалей.
2026-06-02 18:01:33 +03:00
c0mrade
bd12113294 feat(sales-stats): карточки возвратов в сводке
Две карточки — количество и сумма возвратов за период (красный тон).
Сетку сводки перевёл на 2/5 (10 карточек делятся ровно, без пустых
ячеек). StatCard получил invertDelta: для «меньше — лучше» метрик рост
красится в красный, падение в зелёный (стрелка по факту). 4 локали.
2026-06-02 16:53:30 +03:00
c0mrade
0d024aec58 fix(i18n): spell the brand "Remnawave" instead of camelCase "RemnaWave"
Replace the user-facing 'RemnaWave' spelling with 'Remnawave' across all locale values (en/ru/zh/fa, 67 strings), t() display fallbacks, and code comments. Translation keys (refreshRemnaWave, ...) and code identifiers (RemnaWaveService, setRemnaWaveUuid, ...) are left untouched.
2026-06-01 13:31:12 +03:00
c0mrade
9309643b7e feat(admin-remnawave): provider favicon, mobile node card, meaningful icons
- Render the provider favicon in the node badge (new provider_favicon / provider_name fields), with graceful fallback.
- Mobile-friendly node card: stop the versions block from shifting right on wrapped rows, truncate long addresses, cap the provider badge.
- Give node-summary cards (total/online/offline/disabled) and traffic-period cards (2d/7d/30d/month/year) distinct, meaningful icons instead of all-the-same globe / bar-chart.
- Show traffic as 'used / limit' (e.g. 2.35 TB / ∞).
- Route every icon through the central barrel (drop the direct react-icons/pi import).
2026-06-01 13:08:30 +03:00
c0mrade
b8be98939a feat(admin-remnawave): enrich panel stats UI and fix nodes-online display
- Show nodes as "online / total" (fixes "56 online" when only 5 nodes exist)
- Add recap, devices (+top users by device count), top-consumers, panel
  health and subscription-request widgets, with profile-specific Phosphor
  metric icons (CPU/RAM/uptime/devices/event-loop)
- Add the matching adminRemnawave API types and methods
2026-05-31 18:51:12 +03:00
c0mrade
574da67ca0 feat(yandex-conv): forward CID through remaining paid API methods
Pair with the bot's #558449 follow-up. Pass getYandexCid() through
the four cabinet API methods that previously omitted it:

  - switchTraffic (PUT /traffic)
  - activateTrial (POST /trial — body added, was previously empty POST)
  - updateCountries (POST /countries)
  - switchTariff (POST /tariff/switch)

Without these the backend now-typed yandex_cid field would silently
stay None for these paid flows even when the user had a CID cached.
2026-05-28 16:43:22 +03:00