Commit Graph

491 Commits

Author SHA1 Message Date
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
5b123f5da0 fix(switch-tariff): route trial & expired subs to the purchase flow (#629889)
The switch sheet's fallback checked detail.error_code, but the backend
sends detail.code — so the expired-fallback was effectively dead. Centralize
it into shouldUsePurchaseFlow(), which accepts either key and now also
handles the new trial_cannot_switch code: a trial has no paid value to
prorate, so the backend rejects the switch and the sheet hands the tariff to
the regular purchase form instead of surfacing a raw error.

The switch button is already hidden for trials via canSwitch; this is the
server-side backstop for races / direct calls, and it un-breaks the expired
fallback at the same time.
2026-06-03 19:02:52 +03:00
c0mrade
ce7a3eaedf fix(connection): Happ TV connect works on Apple TV too (one API for both)
Correction: check.happ.su/sendtv is a single API serving both Android TV and
Apple TV, not Android-only. Re-enable the TV connect widget for Apple TV
(gate on androidTV || appleTV again) and make the code input flexible again
(inputMode text, alphanumeric) since the code isn't guaranteed numeric.
2026-06-03 16:34:20 +03:00
c0mrade
f8ac4895fa fix(connection): keep selected app when switching platform
Switching the platform dropdown always reset the app to the new platform's
featured/first one, discarding the user's choice (e.g. picked Happ → switched
platform → snapped back to the first app). Now it keeps the same app by name if
it exists on the new platform, falling back to featured/first only otherwise.
2026-06-03 16:22:41 +03:00
c0mrade
c91c9e0441 fix(connection): Happ TV connect renders through the active block style (all 4)
The TV quick-connect was rendered as standalone cards inserted between two
sliced block lists, which broke the panel's chosen style (e.g. split the
timeline rail) and dropped a step. Now it renders THROUGH the active renderer:

- Blocks gain an optional customNode; every renderer (cards/timeline/accordion/
  minimal) drops it into the block body, so it inherits that exact style.
- InstallationGuide renders the FULL block list in the active style and injects
  the TV connect widget as customNode on the "add subscription" step (no more
  slicing / dropped steps) — only for the Happ Android TV app.
- TvQuickConnect is now content-only (no one-off cards/titles); the block step is
  the wrapper. Buttons already use the shared blockButtonClass.
2026-06-03 16:09:49 +03:00
c0mrade
ca54cb4732 fix(connection): restrict Happ TV quick-connect to Android TV only
Verified against Happ docs: the sendtv 5-digit-code / Web-Import API
(check.happ.su/sendtv) is Android-TV-only. Apple TV (tvOS) uses a different
mechanism (tv.happ.su temporary code), so POSTing the code to check.happ.su
would hit the wrong endpoint. The block was gated on isTvPlatform (androidTV ||
appleTV); narrow it to androidTV. Apple TV now renders its normal config blocks.
2026-06-03 15:54:54 +03:00
c0mrade
5855a88dc6 fix(connection): Happ TV quick-connect is Happ-only + matches config-block styles
The TV quick-connect block (Подключить TV / Сканировать QR) is a Happ-only
feature (check.happ.su/sendtv), but it rendered for ANY app on a TV platform and
used one-off button styles (solid btn-primary + grey btn-secondary) that clashed
with the outlined-accent buttons that come from the subscription-page config.

- Show TvQuickConnect only for the Happ app (detected by its happ:// deep-link
  scheme, name as fallback).
- Extract the config-block button style into a shared blockButtonClass and use it
  for the TV buttons too, so they adapt to exactly the same visual language as the
  panel-config blocks (no divergent one-off styling).
- Align the code input with the Happ Android TV API doc (5-digit numeric):
  placeholder 12345 + inputMode numeric. The send call already matches the spec
  (POST check.happ.su/sendtv/{code}, body {"data": base64(subscriptionUrl)}).
2026-06-03 15:49:58 +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
56d8e1ffa7 fix(sales-stats): адаптив сетки Допов и сводки
Допы (5 карточек) рисовали 4+1: orphan-fill из двух arbitrary-вариантов
конфликтовал по специфичности — правило :last-child:nth-child(odd) бьёт
ресет :last-child, поэтому 5-я карточка тянулась на 2 колонки и на
десктопе. Заменил на один mobile-вариант max-lg:, как в AdminRemnawave:
десктоп — 5 в ряд, планшет/мобайл — 2 колонки + последняя на всю ширину.
Сводку тоже перевёл на lg:grid-cols-5 (было sm) — на планшете 5 колонок
давали ~115px и резали суммы; теперь до десктопа 2 колонки.
2026-06-02 17:15:18 +03:00
c0mrade
bd12113294 feat(sales-stats): карточки возвратов в сводке
Две карточки — количество и сумма возвратов за период (красный тон).
Сетку сводки перевёл на 2/5 (10 карточек делятся ровно, без пустых
ячеек). StatCard получил invertDelta: для «меньше — лучше» метрик рост
красится в красный, падение в зелёный (стрелка по факту). 4 локали.
2026-06-02 16:53:30 +03:00
c0mrade
c880517169 fix(datepicker): убрать скачки и поправить адаптив
- Числовой формат даты (01.06.2026) — постоянная ширина в любой локали,
  раньше короткое имя месяца («мая» vs «сент.») меняло ширину кнопки.
- min-w + justify-start + truncate на триггере: плейсхолдер и выбранная
  дата дают одинаковую ширину — поле не прыгает по горизонтали.
- fixedWeeks + showOutsideDays: всегда 6 недель, высота календаря не
  скачет при листании месяцев (5↔6 рядов).
- collisionPadding + max-w-[calc(100vw-1rem)]: поповер не вылезает за край
  экрана на мобайле.
- Контейнеры диапазона дат (продажи, трафик) переведены на flex-wrap /
  full-width на мобайле — два поля больше не вылезают за ширину.
2026-06-02 16:38:56 +03:00
c0mrade
54998c8195 fix(sales-stats): убрать пустые ячейки в сетках карточек
Сводка (8 карточек) была 2/3/4 колонки — на 3-колоночном брейкпоинте
последний ряд оставлял пустую ячейку. Перевёл на 2/4 (8 делится ровно).
Допы (5 карточек) тянули сироту на 2- и 3-колоночном — теперь 2/5 с
orphan-fill: последняя нечётная карточка занимает всю ширину на мобайле
(паттерн как в новостях/подписках).
2026-06-02 16:32:25 +03:00
c0mrade
016da9f638 feat(ui): заменить все нативные date-инпуты на DateField
Тёмный DateField теперь везде, где был браузерный <input type="date">: фильтр
периода в Трафике, диапазон дат в Платежах и Аудит-логе, срок действия промокода.
Единый вид во всём кабинете. (datetime-local не трогал — там нужно ещё и время.)
2026-06-02 16:17:10 +03:00
c0mrade
edd2d25024 feat(ui): тёмный DateField на react-day-picker вместо нативного input
Нативный <input type="date"> выглядел чужеродно (браузерный светлый календарь).
Новый общий компонент DateField: react-day-picker внутри Radix Popover, тёмная
тема через CSS-переменные .rdp-dark, навигация — наши иконки из барреля,
локализация месяцев/дней по языку кабинета (date-fns). Принимает/отдаёт
'YYYY-MM-DD' — drop-in замена нативного инпута. Подключён в селекторе периода
статистики (свой период). Остальные места — следующими шагами.
2026-06-02 16:10:48 +03:00
c0mrade
9a05f23a04 feat(sales-stats): пресет «Этот месяц» (с 1-го числа) и он же по умолчанию
Статистика по умолчанию открывается за текущий месяц с 1-го числа до сегодня
(month-to-date), а не за фиксированные 30 дней. Добавлена кнопка «Этот месяц»
в селектор периода; util getMonthToDateRange/isMonthToDate.
2026-06-02 15:55:10 +03:00
c0mrade
80ac63102b feat(sales-stats): иконки на карточках во всех вкладках
Доделал душевный вид: карточки внутри вкладок (Триалы/Продажи/Продления/Допы/
Депозиты) тоже получили иконки в цветных чипах с тонами — единый стиль со сводкой
(регистрации/триалы/конверсия/длительность, продажи/выручка/чек/топ-тариф,
продления/%, допы/GB/устройства, пополнения/сумма/средний). Крупные суммы — без
копеек.
2026-06-02 15:45:40 +03:00
c0mrade
622ec318da fix(sales-stats): ровные иконки в карточках + не дублировать иконку
- StatCard: лейбл сверху, чип+значение в одной строке (чип центрируется ровно по
  значению, дельта ушла под строку) — иконки больше не «плавают» по вертикали
  из-за разной высоты карточек.
- «Активные подписки»: TicketIcon вместо SubscriptionIcon (та визуально как искра
  и дублировала «Новые триалы»).
2026-06-02 15:39:25 +03:00
c0mrade
dfc7768a40 feat(sales-stats): душевные карточки с иконками + фикс скачка при смене периода
- StatCard: иконка в мягком цветном чипе (tone success/accent/warning/…) в духе
  статистики Remnawave; дельта под значением. Обратно совместимо (valueClassName).
- Карточки сводки получили осмысленные иконки (доход/подписки/триалы/конверсия/
  продления/допы/пополнения) и цветовые тоны.
- Скачок при переключении периодов убран: на все запросы статистики добавлен
  placeholderData: keepPreviousData — старые данные остаются на экране, пока
  грузятся новые, без вспышки скелетона и прыжка вёрстки.
2026-06-02 15:32:53 +03:00
c0mrade
631e922f78 fix(sales-stats): не резать значения в карточках сводки (адаптив)
Карточки сводки резали суммы («252…», «100.0…»): на десктопе их было 8 в ряд
(слишком узко), а дельта стояла в той же строке и доедала место.
- Дельта переехала ПОД значение — значение получает всю ширину карточки.
- Сетка сводки: убрал xl:grid-cols-8, максимум 4 в ряд (карточки шире).
2026-06-02 15:22:33 +03:00
c0mrade
bb97ee8430 feat(sales-stats): разбивка допов по пакетам тоже на BreakdownList
Для консистентности: «Допы → по пакетам» теперь список с долями % и мини-баром,
как остальные категорийные разбивки, вместо отдельного bar-графика.
2026-06-02 15:17:34 +03:00
c0mrade
a685e542aa feat(sales-stats): дельты период-к-периоду на карточках сводки
StatCard получил опциональную дельту (стрелка + %), а страница статистики тянет
сводку ещё и за предыдущий равный период и считает рост/падение для метрик
периода: выручка, новые триалы, продления, доход с допов, ручные пополнения.
Для «всего времени» сравнение не показывается (не с чем сравнивать).
2026-06-02 15:15:34 +03:00
c0mrade
a75f42d056 feat(sales-stats): читаемые разбивки и stacked-бары, иконки платёжек, выручка
Полировка статистики продаж (читаемее/информативнее):
- BreakdownList — категорийные разбивки списком (иконка/цвет + значение + доля % +
  пропорциональный мини-бар) вместо bar-графика. В «Депозиты → по методам» теперь
  с иконками платёжек (PaymentMethodIcon), в «Продажи → по тарифам» — с долями.
- StackedBarChart — «по дням разбивкой» теперь stacked-бар вместо наслаивающихся
  полупрозрачных area (читалось плохо); тултип по дню показывает все серии.
- Продажи: добавлена карточка «Выручка» (total_revenue бэк отдавал, но не показывался).
2026-06-02 15:10:06 +03:00
c0mrade
bc8503469d fix(wheel): remove the spin-only sparkle lights entirely
The extra lights that appear during a spin (and bunched into a line through the centre) were the SPARKLE_POSITIONS effect — yellow animate-ping dots rendered only while isSpinning. Moving/scattering them never satisfied; they're unwanted during the spin. Remove them outright: while spinning, only the rim LEDs remain — no extra lights, no centre line.
2026-06-01 18:12:55 +03:00
c0mrade
2ce966cece fix(wheel): remove CSS filters from the spinning SVG (kill GPU-composite streak)
The center streak doesn't appear in screen recordings, which is the signature of a GPU-compositing artifact — CSS filters on SVG elements mis-render when the wheel group is promoted to a layer during the spin. Replace the two SVG CSS filters with compositing-safe equivalents: LED-glow blur -> radial gradient fill, emoji drop-shadow -> SVG-native feDropShadow. (HTML background blur left as-is; it's not on the SVG and isn't a light.)
2026-06-01 18:00:00 +03:00
c0mrade
6c5d1ee0b1 fix(wheel): keep spin sparkles in the outer ring, never near the centre
Confirmed from the user's spin video: the sparkles bunched into the middle during a spin because they were placed at 16-40% from centre (some right by the hub). Push them all to a 34-43% radius (golden-angle distributed) so they ring the outer wheel and the centre stays clear — no sparkles crossing or near the centre.
2026-06-01 17:46:36 +03:00
c0mrade
7716e32eec fix(wheel): scatter spin sparkles so they don't streak through the centre
The real cause of the center line: SPARKLE_POSITIONS (yellow dots shown only while spinning) used `20 + (i*10)%60` / `15 + (i*13)%70`. For the first ~6 sparkles the modulo never wraps, so top/left increment linearly and the dots land on a straight diagonal through the centre — the 'line of lights' during the spin. Scatter them on a golden-angle spiral inside the wheel instead.

Reverts the earlier LED-glow change (it wasn't the cause): the rim-light blur is restored to the original.
2026-06-01 16:45:35 +03:00
c0mrade
8a41aa7781 fix(wheel): kill the center-line LED artifact during spin (drop CSS blur filter)
The rim lights are static cx/cy SVG circles; their glow used a CSS filter:blur(2px). During the spin the wheel's transform transition promotes the SVG to a GPU layer, and Chrome mis-renders CSS filters on geometry-positioned SVG elements — the blurs streak toward the view-box origin, drawing a line of lights through the centre. Replace the blur with a soft radial-gradient fill (no filter, so the artefact can't occur).

Also drive both the chase speed and the per-dot delay from one --led-dur var so the chase stays in order at the faster spin speed instead of scrambling (the delay was a fixed 6s value that broke once the duration dropped to 2s).
2026-06-01 16:24:02 +03:00
c0mrade
7b7ff53672 fix(news): fill the orphan card so the grid has no empty cell
On sm+ the regular news cards sit in a 2-col grid; an odd count left an empty cell. Move the featured card out of the grid (it was col-span-full anyway, which also skewed nth-child parity) and stretch the last regular card to span both columns when it's the lone one in its row — same orphan-fill used for the admin stat cards.
2026-06-01 15:49:29 +03:00
c0mrade
1b1046bb5c feat(admin-remnawave): add node/xray version icons like the panel
The panel marks each node version with its product logo. Add an XrayIcon (the Xray core brand pinwheel, custom SVG like RemnawaveIcon) and put RemnawaveIcon before the remnanode version + XrayIcon before the xray version, matching the original node rows. Drop the redundant 'xray' word since the logo conveys it.
2026-06-01 14:43:11 +03:00
c0mrade
872f73157f feat(icons): add node/gift glyphs to the central barrel
Add Key, Inbox, Export, WarningCircle, Heartbeat, CalendarBlank, CalendarStar, ChartPie, ChartDonut, Cpu, Memory, Pulse as Phosphor Regular wrappers so feature pages stop hand-writing SVGs or importing react-icons/pi directly.
2026-06-01 13:08:06 +03:00
c0mrade
9aae399ce8 fix(cabinet-icons): correct two mismatched glyphs from the svg migration
A personal glyph-by-glyph re-review of the migration (the "steering wheel"
guard) found two wrong matches:
- MaintenanceScreen: a wrench/maintenance svg had been mapped to AdjustmentsIcon
  (sliders) → use a proper WrenchIcon (PiWrench).
- InstallationGuide tutorial button: an open-book svg had been mapped to
  DocumentIcon (file) → use BookOpenIcon (PiBookOpen).

Add WrenchIcon and BookOpenIcon to the central barrel. All other replacements
were verified correct (paper-plane→Send, server→HardDrives, warning-triangle→
Warning, etc.).
2026-05-31 23:43:32 +03:00
c0mrade
b5088c70a1 refactor(cabinet): migrate inline SVG icons to the central react-icons set
Audit-driven sweep: replaced 168 hand-written inline <svg> icons across 77
files with the central Phosphor (react-icons/pi) components from
@/components/icons, preserving each icon's size classes and colour (dynamic
stroke colours via the parent's currentColor, RefreshIcon's spinning state,
conditional rotate-180 chevrons).

Verified: tsc + vite build + eslint clean; an adversarial diff review of all
changed files found the replacements correct (70 files clean, 0 blockers).
Remaining inline <svg> dropped from 262 to ~95 — the survivors are legitimate
non-icons (brand/provider logos, loading spinners & framer-motion animations,
charts/sparklines, background decoration) plus a few ambiguous glyphs.
2026-05-31 23:37:12 +03:00
c0mrade
8613cb20aa fix(subscription): use the nav sparkle icon on the renew CTA button
PurchaseCTAButton (the "Продлить подписку" CTA) rendered a single inline
sparkle; swap it for the SubscriptionIcon component so it matches the two-star
sparkle in the navigation. Colour is inherited from the badge container.
2026-05-31 19:43:54 +03:00
c0mrade
5340f81a95 fix(subscription): use the nav SubscriptionIcon on the quick-renew button
The inline single sparkle didn't match the two-star sparkle shown in the nav.
Render the same SubscriptionIcon component so the button matches the nav exactly.
2026-05-31 19:38:57 +03:00
c0mrade
e18db02e42 fix(subscription): use the sparkle icon on the quick-renew button
The expired-card quick-renew button showed a lightning bolt, inconsistent with
the sparkle used on every other subscription CTA. Match the subscription CTA.
2026-05-31 19:24:43 +03:00
c0mrade
0a89838926 style(cabinet-icons): use regular Phosphor weight and switch wheel to dice
- Drop the Duotone variants (their translucent secondary fill looked murky on
  the dark theme) in favour of the clean regular weight across all icons
- WheelIcon: PiSteeringWheel → PiDiceFive (a steering wheel made no sense for
  the fortune-wheel feature)
2026-05-31 19:24:29 +03:00
c0mrade
d0e0b6b7e3 feat(cabinet): migrate all icons to the panel's Phosphor set
Replace the cabinet's hand-written heroicons-style SVG icon components with
the Remnawave panel's own icon family — Phosphor via react-icons/pi (Duotone).
All icons now live in a single central barrel (src/components/icons:
index + extended-icons + editor-icons, 144 icons); every feature file imports
from it instead of redefining inline SVGs (~440 icon defs removed).

- Add react-icons dependency
- Desktop nav (AppShell) now uses the central Phosphor icons, removing a
  stroke-vs-duotone inconsistency the partial migration introduced
- Icons with custom props (SortIcon's direction, expandable chevrons) kept as
  thin Phosphor wrappers; the RemnawaveIcon brand logo and animated SVGs
  (checkmarks, spinners) are intentionally left as-is
- Restore the original per-call-site icon sizes wherever the centralized
  default differed (211 call sites)
2026-05-31 18:50:50 +03:00
c0mrade
e1f401c333 fix(i18n): backfill 120 missing keys + close 2 typo regressions from purchase refactor
Surface: users (and especially Серёжа в bugs topic) were seeing raw i18n keys
like `subscription.promoGroup.title` rendered as literal text on the renewal
screen. Audit found 118 keys referenced in code that were absent from all 4
locale files, plus 140 keys missing only in zh/fa (silently falling back to
Russian for those users).

Root causes:
- cf52999 (extract TariffPickerGrid) introduced `subscription.promoGroup.title`
  that never existed — should have been `subscription.promoGroup.yourGroup`.
- Subscription.tsx used `subscription.autopay.title` (object path) while
  SubscriptionListCard.tsx used `subscription.autopay` (leaf) — i18next can't
  hold both shapes for the same key.
- Many user-flow keys (subscription.notFound, subscriptions.empty, common.ok,
  successNotification.*, etc.) were never added when their components landed.
- Historical drift had ~140 keys in ru/en that never got zh/fa translations.

Fix:
- Rename TariffPickerGrid usage `promoGroup.title` → `promoGroup.yourGroup`.
- Rename Subscription.tsx usage `autopay.title` → `autopay`.
- Add 116 brand-new keys across ru/en/zh/fa (464 entries).
- Add 8 partially-missing keys (en+ru gaps) for landing periods, dashboard
  campaign/referrer stats, and promo offer failure counts.
- Translate 280 entries to close the zh/fa drift.

Final state: 0 keys missing in any of the 4 locale files for any code-referenced
translation key. Build + tsc green.
2026-05-28 18:47:58 +03:00
c0mrade
0aaf58c8b8 refactor(admin-traffic): extract 5 filter components
Third and final step in the AdminTrafficUsage decomp. Move the
filter dropdowns + PeriodSelector into
src/components/admin/trafficUsage/filters/:

- PeriodSelector.tsx (103 lines): fixed period tabs (1/3/7/14/30
  days) + custom date-range mode. Exports the PERIODS constant
  so the parent's prefetch effect iterates the same canonical
  list.

- TariffFilter.tsx (133 lines): pop-over multi-select for tariff
  names.

- StatusFilter.tsx (147 lines): pop-over multi-select for
  subscription statuses. Exports STATUS_COLORS alongside.

- NodeFilter.tsx (135 lines): pop-over multi-select for traffic
  nodes (with flag emoji + traffic-share numbers).

- CountryFilter.tsx (132 lines): pop-over multi-select for
  source countries.

All five are pure controlled components — parent owns every
Set<string> filter state and feeds them via {available, selected,
onChange}. The parent imports them and drops 9 now-unused icon
imports (FilterIcon, ChevronDownIcon, ServerIcon, CalendarIcon,
StatusIcon, GlobeIcon — they live inside the filter files now).

AdminTrafficUsage.tsx: 1593 → 977 lines (-616).
Cumulative across the 3-step admin-traffic decomp:
1904 → 977 (-927, -49%).
2026-05-27 09:45:29 +03:00
c0mrade
c94bd3d593 refactor(admin-traffic): extract TrafficIcons barrel
Second step in the AdminTrafficUsage decomp. Move the 15 inline SVG
icons (Search/Chevron/Refresh/Download/Sort/Filter/Server/Calendar/X/
Status/Globe/Shield/ServerSmall) into a single barrel at
src/components/admin/trafficUsage/TrafficIcons.tsx. Pure SVG, no
behavioural change.

AdminTrafficUsage.tsx: 1734 → 1593 lines (-141).
2026-05-27 09:40:01 +03:00
c0mrade
4c2af82d91 refactor(admin-traffic): extract helpers + RiskBadge
First step in the AdminTrafficUsage decomp. Two new files under
src/components/admin/trafficUsage/:

- trafficUsageHelpers.ts (166 lines): the TanStack Table module
  augmentation (sticky/align/bold meta), formatters (formatBytes,
  formatCurrency, formatShortDate, formatGbPerDay,
  toBackendSortField, getFlagEmoji wrapper), risk-assessment helpers
  (bytesToGbPerDay, getRatio, getRowBgColor, getNodeTextColor,
  getRiskLevel, getCompositeRisk), and the RISK_STYLES constant.

- RiskBadge.tsx (36 lines): the small per-row risk indicator
  (dot + GB/d value + mini progress bar). Pure presentational.

Parent imports everything from the new modules; the inline
'TanStack Table module augmentation' lives in the helpers file
so the declaration merge still picks up across the codebase
once the helpers are imported.

AdminTrafficUsage.tsx: 1904 → 1734 lines (-170).
2026-05-27 09:38:18 +03:00
c0mrade
c11bfaf012 refactor(bulk-actions): extract SubscriptionSubRow + StatusBadge
Fourth and final step in the AdminBulkActions decomp.

SubscriptionSubRow: the expanded per-user secondary <tr> shown when
the operator drills into multi-subscription users (tariff, status,
days remaining, traffic bar, devices). Pure presentational.

StatusBadge: the pill used both inside the sub-row and the primary
user-table column (subscription_status). Co-located here because
both consumers want the same visual contract.

AdminBulkActions.tsx: 1367 → 1182 lines (-185).
Cumulative across the 4-step admin decomp: 2539 → 1182 (-1357, -53%).
2026-05-27 09:35:37 +03:00
c0mrade
8d25e5201a refactor(bulk-actions): extract MultiSelectDropdown
Third step in the AdminBulkActions decomp. Move the 154-line
pop-over multi-select used by the tariff / status / node filters
into src/components/admin/bulkActions/MultiSelectDropdown.tsx.

Pure controlled primitive: closes on outside click, provides
select-all / deselect-all helpers, uses the shared ChevronDownIcon
from DropdownSelect.tsx. Exports MultiSelectOption type alongside.

Parent drops its inline ChevronDownIcon import (now only referenced
by the moved sibling) and imports MultiSelectDropdown + the option
type from the new module.

AdminBulkActions.tsx: 1517 → 1367 lines (-150).
2026-05-27 09:32:54 +03:00
c0mrade
f398d9e95c refactor(bulk-actions): extract FloatingActionBar + actionTargets
Second step in the AdminBulkActions decomp. Two new files:

- actionTargets.ts (27 lines): SUBSCRIPTION_LEVEL_ACTIONS set +
  isSubscriptionLevelAction(). The classification is the source of
  truth shared between FloatingActionBar (renders two grouped menus
  when multi-tariff is on) and the parent page (gates selection-count
  semantics + the active-paid count in delete-subscription).

- FloatingActionBar.tsx (304 lines): the bottom-dock selection
  toolbar. Pure presentational — parent owns selection state and
  supplies onAction (opens the modal with the picked action type)
  + onToggleAllSubscriptions. The full 11-item action config (icons
  + label keys + colour classes) lives inside the file as a private
  ActionConfig array.

Parent now imports FloatingActionBar + isSubscriptionLevelAction
from the new modules; the inline ActionConfig interface is gone.

AdminBulkActions.tsx: 1819 → 1517 lines (-302).
Cumulative across the 2-step admin decomp: 2539 → 1517 (-1022, -40%).
2026-05-27 09:31:10 +03:00
c0mrade
217d60ab19 refactor(bulk-actions): extract ActionModal + DropdownSelect
First and largest step in the AdminBulkActions decomp. Two new
files under src/components/admin/bulkActions/:

- DropdownSelect.tsx (47 lines): the small native-<select> wrapper
  used both by the filter row and the ActionModal's tariff/promo
  picker. Exports DropdownSelect + the shared ChevronDownIcon +
  DropdownOption type.

- ActionModal.tsx (712 lines): the per-action configuration +
  execution modal. Wraps three internal views — ProgressView (live
  SSE log + counters while a job streams), ErrorDetails (collapsible
  per-row error list after completion), and the per-action form
  (days / tariff / traffic / balance / promo-group / grant /
  set-devices / delete-* confirmations). Exports the ModalState
  and ProgressState types alongside.

Self-contained: parent feeds modal state + reference data (tariffs,
promo groups, users for active-paid counting) and receives
onExecute(params) when the operator confirms. No queries or
mutations here — execution flow stays in the parent's
handleExecuteAction.

Parent keeps a local CheckIcon (still used by MultiSelectDropdown
checkboxes + the user-table column defs) and re-imports
ChevronDownIcon / DropdownOption from the new file.

AdminBulkActions.tsx: 2539 → 1796 lines (-743).
2026-05-27 09:28:39 +03:00
c0mrade
72c325f8cc refactor(purchase): extract ClassicPurchaseWizard from god page
The largest and last structural extraction in the SubscriptionPurchase
decomp. Moves the classic-mode purchase flow (period → traffic →
servers → devices → confirm) into a self-contained step wizard at
src/components/subscription/purchase/ClassicPurchaseWizard.tsx.

The wizard self-owns:
- the preview query (gated on confirm step + form open)
- the submitPurchase mutation
- all six pieces of wizard state (currentStep, selectedPeriod,
  selectedTraffic, selectedServers, selectedDevices, showPurchaseForm)
- the steps memo + currentSelection memo + currentStepIndex/isFirst/isLast
- the init useEffect (seeds from classicOptions.selection)
- toggleServer / goToNextStep / goToPrevStep / resetPurchase / getStepLabel
- its own useCloseOnSuccessNotification handler (closes form + resets
  step on global success notification) — replaces the parent's
  handleCloseAllModals dependency for the classic flow

Parent shrinks to a pure orchestrator: route params, three top-level
queries (subscription, purchase-options, subscriptions-list), mode
detection, two pieces of tariffs-mode state, the success-notification
handler for the remaining tariff modals, and JSX that delegates to
the four extracted sub-components.

SubscriptionPurchase.tsx: 908 → 370 lines (-538).
Cumulative across the 5-step decomp: 2054 → 370 (-1684, -82%).
Drops 11 parent imports (useEffect, useMemo, useCallback, useMutation,
useQueryClient, useNavigate, PurchaseSelection, PeriodOption,
InsufficientBalancePrompt, CheckIcon, Twemoji, useCurrency,
usePromoDiscount, getErrorMessage, PurchaseStep).
2026-05-27 00:02:31 +03:00
c0mrade
cf5299973a refactor(purchase): extract TariffPickerGrid from god page
Move the tariff selection surface (promo-group banner, all-purchased
empty state, and the 1/2-col tariff cards grid with per-tariff CTAs)
into src/components/subscription/purchase/TariffPickerGrid.tsx.

The grid is pure presentation — it owns no queries or mutations.
Two callbacks back into the parent:
- onSelectTariff(tariff)  → parent sets selectedTariff + showForm
- onSwitchTariff(tariffId) → parent sets switchTariffId

Internal helpers (formatPrice via useCurrency, applyPromoDiscount
via usePromoDiscount, glass tokens via useTheme + getGlassColors,
navigate for the 'all purchased' back-to-list button) all derive
inside the component — nothing threaded as props beyond data.

SubscriptionPurchase.tsx: 1202 → 908 lines (-294).
2026-05-26 23:55:23 +03:00