Commit Graph

530 Commits

Author SHA1 Message Date
Chara-Freedom
255777e7d1 fix(subscription): fall back to purchase flow for free-tariff switches
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>
2026-07-13 05:36:12 +03:00
Fringg
5fc8e7f97a feat(admin): клики кнопок бота и действия в кабинете во вкладке «Активность»
Два новых типа записей таймлайна: button_click (нажатия кнопок в боте,
с текстом кнопки) и cabinet_action (мутационные действия юзера в
кабинете, метод + нормализованный путь). Чип-фильтр «Клики», иконки
BotIcon/BoltIcon, переводы во всех 4 локалях.

biome check, type-check и build проходят (pre-commit пропущен: biome
не установлен локально).
2026-07-13 03:46:53 +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
Fringg
0e6f80c090 Merge PR #477: нормализовать detail из ответов API перед рендером (краш на 422)
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 — чисто.
2026-07-12 23:22:26 +03:00
kewldan
9b972931cf fix(ui): нормализовать detail из ответов API перед рендером — краш на 422
Что: все обработчики ошибок, которые клали сырой response.data.detail в
состояние и затем в JSX, переведены на существующий хелпер
getApiErrorMessage (src/utils/api-error.ts):

- Login.tsx — Telegram-auth, вход/регистрация по email, восстановление пароля
- Profile.tsx — смена email (запрос и подтверждение кода), повторная
  отправка письма верификации
- ConnectedAccounts.tsx — привязка email к аккаунту
- admin/AnalyticsTab.tsx, admin/MenuEditorTab.tsx — сохранение настроек

Почему: на невалидный по схеме запрос FastAPI/Pydantic отвечает 422, где
detail — не строка, а массив объектов {type, loc, msg, input, ctx}.
Компоненты рендерили его как есть, React падал с «Objects are not valid
as a React child», и вся страница (включая логин) уходила в ErrorBoundary
«Something went wrong» — пользователь не видел причину отказа вообще.

Зачем: getApiErrorMessage сводит Pydantic-массив к строке
«field: message; ...», поэтому вместо краша форма показывает настоящий
текст ошибки валидации. Ветвления по detail.includes(...) сохранены и
работают по нормализованной строке — поведение прежнее для строковых
detail, но безопасное для любой формы ответа.
2026-07-12 18:37:04 +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
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
37a53e40ea fix(auth): make login footer legal links open real pages, not the login tab
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.
2026-07-09 18:14:49 +03:00
Fringg
8f31311112 fix(connection): keep the Happ add-subscription button working on Remnawave 2.8.0
Panel 2.8.0 removed the server-side happ-encrypt endpoint, so for users
without a stored crypto link the backend leaves {{HAPP_CRYPT4_LINK}}
unresolved and the subscriptionLink button silently disappeared
(isValidDeepLink requires '://').

- BlockButtons: resolve button templates client-side at render (with username,
  like the panel's own subpage) instead of dropping the button; collapse
  double-prefixed happ://crypt4/happ://cryptN/... resolvedUrl from old backends
- templateEngine: collapse a hardcoded happ://cryptN/ prefix before
  {{HAPP_CRYPT[34]_LINK}}; cache crypt-link generation (jsencrypt RSA-4096 is
  slow on weak devices and re-randomizes every call)
- Connection: in happ_cryptolink mode force the crypt link only when the button
  fell back to the plain subscription link or kept unresolved templates - an
  explicit Subpage link (e.g. happ://add/...) now wins, so Subpage edits apply
2026-07-02 16:12:18 +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
c0mrade
9456c7f54e fix(ui): finish design-audit follow-ups
- useFeatureFlags: кэш флагов в localStorage — нижняя навигация больше
  не прыгает на холодном старте (таб «Рефералы»/«Колесо» появлялся после
  загрузки terms/config)
- applyThemeColors: частичный/битый ответ /branding/colors добивается
  дефолтами вместо падения всего приложения в ErrorBoundary
- карточка «Связаться с поддержкой»: кнопка больше не дублирует заголовок
  («Написать», без переноса)
- «Мои подписки»: заголовок и «Купить ещё» в одну строку на 390px
- StatCard: подписи плиток переносятся на две строки вместо обрезки
  многоточием («Бонус новому польз…»)
2026-06-11 16:00:19 +03:00
c0mrade
7f68e2cd5e fix(theme): readable text on any operator palette + admin consistency pass
Контраст-аудит (WCAG-обход всех текстовых узлов, 4 палитры × юзер+админ
страницы) показал системные провалы читаемости: вторичный текст 1.5-2.5
на светлой/кастомных темах, статусные цвета 1.3-1.6, белый текст на
светлых акцентах 1.9.

Корневые фиксы:
- useThemeColors: контраст-кламп производных серых токенов — dark-400/
  champagne-600 (вторичный текст) держат >=5.0 к поверхности, dark-500/
  champagne-500 (подсказки) >=3.8; палитры с достаточным контрастом
  рендерятся байт-в-байт как раньше
- --color-on-accent/-success/-warning/-error: чёрный или белый текст
  по фактической яркости цвета оператора; text-white на accent-заливках
  заменён на text-on-accent (135 мест + variant примитива Button +
  .btn-primary)
- .light: статусные текстовые оттенки *-300/400 ремапятся на тёмные
  *-700 (жёлтая «Админка» 1.26 -> 3.8+, зелёные суммы 1.4 -> 3.4+,
  бейджи состояний)

Замер после: операторская тёмная палитра — 0 нарушений (всё >=4.5),
худшие точки остальных подняты с 1.26-1.6 до 3.2+.

Консистентность:
- заголовки админ-страниц приведены к text-xl font-bold text-dark-100
  (были 18/20/24/30px и 600/700 вперемешку)
- «Создать FAQ»: warning-500/80+белый -> warning-500+text-on-warning
- formatTraffic: единицы из i18n (убран микс «GB»/«ГБ» на одном экране)
- categoryCanvas -> categoryCANVAS в локалях (битый ключ на настройках)
- «Управление сквадами» -> «Управление локациями» (юзерский экран)
- мин-макс сумм на выборе метода: dark-600 -> dark-400
- неактивные пункты нижнего нава: dark-500 -> dark-400
2026-06-11 15:49:17 +03:00
Boris Kovalskii
9e6372afba fix: address review feedback on admin pages and backgrounds
- guard config.default_quick_amounts with ?? [] so the edit page survives a
  cabinet deployed ahead of the bot
- roll back the first faq reorder request when the second fails so both
  questions don't end up with the same display_order
- stop overriding the aurora palette in render for the legacy seed colors
- add the missing legalPages / displayMode / quickAmounts keys to fa and zh
2026-06-11 20:00:52 +10:00
Boris Kovalskii
acd0d7c69e fix(backgrounds): keep constellation particles in bounds on resize 2026-06-11 11:14:57 +10:00
Boris Kovalskii
510e4bd738 feat(backgrounds): add constellation background 2026-06-11 11:14:57 +10:00
Boris Kovalskii
71160c6700 feat(backgrounds): add liquid-gradient background 2026-06-11 11:14:57 +10:00
Boris Kovalskii
ab91c86f81 feat(backgrounds): add matrix-rain background 2026-06-11 11:14:57 +10:00
Boris Kovalskii
3df2ef0f39 fix(backgrounds): live speed and size updates for fireflies 2026-06-11 11:14:57 +10:00
Boris Kovalskii
3742f2f88d feat(backgrounds): add starfield background with depth projection 2026-06-11 11:14:57 +10:00
Boris Kovalskii
e3dbc4e663 feat(backgrounds): add snowfall background 2026-06-11 11:14:57 +10:00
Boris Kovalskii
1d00ca97e5 feat(backgrounds): add fireflies background 2026-06-11 11:14:57 +10:00
Boris Kovalskii
caf499a824 fix(backgrounds): legacy config compatibility and locale sync 2026-06-11 11:14:57 +10:00
Boris Kovalskii
725bf8df39 fix(backgrounds): configurable wave colors and boxes grid color with explicit multicolor mode 2026-06-11 11:14:56 +10:00
Boris Kovalskii
acbccc8af8 fix(backgrounds): make beams and beams-collision colors configurable 2026-06-11 11:14:56 +10:00
Boris Kovalskii
f1f4281e11 fix(backgrounds): expose remaining hardcoded colors in shooting-stars, meteors, gradient-animation 2026-06-11 11:14:56 +10:00
Boris Kovalskii
66c08b5d6d fix(backgrounds): read aurora gradient colors and speed from settings 2026-06-11 11:14:56 +10: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
35428cc27d fix(promocode): correct activation UX, error mapping and validity dates
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.
2026-06-08 00:23:31 +03:00
c0mrade
1a3236f650 fix(cabinet): equal-height StatCards so dashboard balance/referral align
In a grid, the wrapping <Link> stretched to the row height but the
StatCard inside only took content height, so the referral card (which has
a subValue line) rendered taller than the balance card ('один толще
другого'). Make StatCard fill its cell with h-full and stretch the
StatsGrid links. Verified by rendering both cards against the built CSS:
balance 86px->94px, now equal to referral's 94px.
2026-06-06 21:44:07 +03:00
c0mrade
3b48abbb7b refactor(cabinet): unify all statistics onto the shared StatCard
Bring every stat strip across the admin panel and user cabinet onto the
canonical StatCard (icon chip + label-on-top + tone-coloured value),
matching the sales/Remnawave style the rest of the app already used.

- Convert ~20 hand-rolled stat strips across 21 pages/components
- Remove 4 divergent local StatCard components (Dashboard, Users,
  BanSystem, Payments) and migrate Remnawave's local StatCard (33 cards)
- Extend StatCard with optional subValue, loading skeleton, trailing slot,
  and icon-size normalisation (all backward-compatible)
- StatsGrid nav tiles now use StatCard while keeping link/chevron/loading
- Preserve all data, i18n keys, conditionals and interactive filters;
  fix the dead 'info' colour by mapping it to accent
2026-06-06 21:27:57 +03:00
c0mrade
9fc681b56d fix(desktop-header): stop layout jumping via 3-column grid
The header used flex with the capsule centered by mx-auto, so the capsule's
auto-margins recomputed whenever anything in the row changed (and compounded
with framer-motion's shared-layout measurement on route change) — making the
whole row, incl. the language button, twitch when navigating (notably into
/admin).

Switch the inner row to grid grid-cols-[1fr_auto_1fr]: the capsule is anchored
to the true viewport center by the symmetric side columns, and the actions are
pinned to the right column (justify-self-end). A width change in one zone can no
longer shift another, so nothing jumps.
2026-06-05 17:29:55 +03:00
c0mrade
7de9b3b51b feat(desktop-header): group nav into a centered segmented capsule
Wrap the always-visible nav items in a single rounded track (segmented-control
look): rounded-full border + subtle bg + backdrop blur, centered between the
logo and the right-side actions via mx-auto (fills the previously empty middle).

- All items stay visible with icon + label — no scroll, no shrink, no collapse,
  no hover-reveal. Purely a visual grouping.
- Items become rounded-full pills; the framer-motion active indicator slides
  within the capsule and is lifted above the track (bg + ring + shadow).
- Admin link stays inside the capsule after a thin divider, warning-tinted.
- Right-side actions sit at the edge (dropped ml-auto since nav now centers).
2026-06-05 17:21:21 +03:00
c0mrade
864aeaf600 fix(desktop-header): full-width row so all nav items fit (no overflow/overlap)
The nav was centered inside a max-w-6xl grid column; 8 icon+label items were
wider than that column, so the centered overflow spilled LEFT over the logo
(ZeroPing's tail poked out from under 'Главная'). Switch to a full-width flex
row — logo (shrink-0) | nav | controls (shrink-0, pinned right via ml-auto) —
widen the container cap to 1600px, slightly compact the items, and drop the
invisible theme toggle from layout when it's not toggleable. Icons + labels
all stay visible: no scroll, no shrink, no overlap.
2026-06-05 00:32:32 +03:00
c0mrade
e4cd975d9c refactor(desktop-header): elegant labeled nav, drop hover-reveal + dead sidebar
The desktop top-bar nav rendered icon-only buttons whose label only slid out
on hover (max-w-0 -> group-hover:max-w-40) — mystery-meat, jumpy, hard to
scan. Replace with always-visible 'icon + label' links and a single
framer-motion pill (shared layoutId) that smoothly slides to the active item;
compact spacing, refined active/hover states.

Also delete DesktopSidebar.tsx — dead code since the cabinet's inception
(never imported anywhere, only re-exported from the barrel).
2026-06-05 00:16:01 +03:00
c0mrade
800daf7506 fix(cabinet): sweep mobile flex-overflow across all pages + components
Full scan of every page/component for the Connected-Accounts overflow class
(long user data with no truncation pushing siblings off the card on mobile),
adversarially verified. Applied min-w-0 + truncate (break-all for copyable
values, break-words for titles) on text columns and shrink-0 on fixed
siblings across ~23 files: AppHeader, dashboard/SubscriptionCardActive,
subscription purchase/sheets (TariffPurchaseForm, ClassicPurchaseWizard,
SwitchTariffSheet), SuccessNotificationModal, PurchaseSuccess, Wheel sub
picker, ReferralNetwork UserDetailPanel, and the admin pages (AnalyticsTab,
BrandingTab, SubscriptionSubRow, user-detail Info/Referrals tabs,
ApplicationReview, PartnerCampaignAssign, Partners, PromocodeStats,
PromoOfferSend, RemnawaveSquadDetail, TariffCreate, Tickets, Withdrawals).
2026-06-04 23:32:59 +03:00
c0mrade
63863a048b fix(cabinet): stop long user data overflowing flex rows on mobile
Same flex-overflow class as the Connected-Accounts card — a long
user-controlled string with no truncation pushes its sibling badge/button
out of the card on narrow screens. Found via a scan of the high-risk
screens; fixed with min-w-0 + truncate/break-words on the text column and
shrink-0 on the fixed siblings:
- Balance: promocode-target tariff name vs days-left badge
- ReferralNetwork CampaignDetailPanel: start_parameter + top-referrer username
- ServerManagementSheet: country/server name vs flag emoji
- Contests / Polls: contest/poll title vs prize/reward badge
2026-06-04 23:03:42 +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
f9cc70cba3 fix(cabinet): blocking screens exit via the native Telegram back button, drop the in-page Close
On a full-screen blocking overlay in the Telegram Mini App there were two exit
affordances: the native back button — which ran SPA navigation on the hidden
route underneath, so it didn't dismiss the block and kept flip-flopping between
Back and Close as the route changed — and a redundant in-page "Close" button on
the service-unavailable screen.

Make the native back button the single, stable exit: while any blocking screen
is active, TelegramBackButton shows one Back button whose click closes the Mini
App (closeTelegramApp) instead of navigating the SPA — no route change, no
Back/Close flip-flop. The normal route-based back button is restored once the
block clears. Remove the in-page Close button.
2026-06-04 13:48:33 +03:00
c0mrade
782568e091 feat(cabinet): premium redesign of all 5 full-screen blocking states
The status screens (service-unavailable, maintenance, channel-subscription,
blacklist, account-deleted) all shared a generic flat look — an icon in a grey
circle, a title, a subtitle, three raw pulsing dots — that didn't match the
app's premium dark-glass aesthetic.

Introduce one shared BlockingShell that all five compose: an opaque themed
canvas with a self-contained accent glow, a centered glass card
(rounded-[--bento-radius] border bg-dark-900/80 backdrop-blur + inset
highlight), a gradient-ringed icon medallion (warning/error/info accent)
instead of a flat grey circle, font-display typography, the canonical Button
for every action, and a subtle framer-motion scale/slide entrance. Accent per
screen: warning (maintenance, account-deleted, service-unavailable), error
(blacklist), info (channel).

Behavior is preserved 1:1 — every i18n key, the channel list + open buttons,
the telegram deep-link, retry/check/close, the recovery poll, focus trap and
aria all carried over verbatim; only the visuals change.
2026-06-04 13:38:35 +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
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