Commit Graph

2004 Commits

Author SHA1 Message Date
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
966c2bf03a style(admin-landings): rebuild stats summary on the StatCard pattern
The landing stats page used plain, icon-less centered boxes that looked
inconsistent with the Sales/Remnawave stats. Rebuild the summary on the shared
StatCard (tinted icon chip + tone + value), consolidating the two scattered
stat rows into a clean 2×4 grid (created/paid/revenue/conversion +
purchases/regular/gifts/avg) with barrel Phosphor icons. Chart and list
containers now use the same bento-card surface as the rest of the admin stats.
2026-06-03 14:50:28 +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
71047780c9 fix(favicon): скругляем углы кастомного лого как у плитки в хедере
Кастомное лого квадратное, и в фавиконе показывалось с острыми углами. Теперь
лого прогоняется через canvas со скруглённым клипом (radiusRatio 0.3 — как
rounded-linear-lg 12px на плитке 40px в хедере) и отдаётся скруглённым PNG.
При сбое canvas (taint/нет 2d-контекста) — фолбэк на сырой лого. Применяется и
в кабинете (useBranding), и на лендинге (QuickPurchase); буквенная монограмма
уже скруглена через rx.
2026-06-03 12:33:10 +03:00
c0mrade
085958342e feat(landing): фолбэк на глобальную тему кабинета, когда у лендинга своя не задана
У лендинга есть собственное поле background_config (тема на каждый лендинг).
Если оно не заполнено, лендинг показывал голый bg-dark-950, тогда как кабинет
всегда рисует глобальную тему-анимацию (BackgroundRenderer → getAnimationConfig).

Теперь: задан background_config → своя тема лендинга (StaticBackgroundRenderer),
пусто → глобальная тема кабинета (BackgroundRenderer). Обёртка больше не красится
непрозрачным bg-dark-950 — фон рисуется порталом за контентом поверх body #0a0f1a,
ровно как в AppShell. Глобальный конфиг анимации тянется из публичного эндпоинта.
2026-06-03 12:25:20 +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
cecfe7ec03 fix(dashboard): убрать дубль кнопки покупки при пустом списке подписок
Мульти-тариф блок рендерился даже при пустом списке подписок (пустой
массив truthy) — показывал заголовок «Подписки» и кнопку покупки, которая
дублировала кнопку из блока «нет подписок» (триал-оффер). Получалось две
одинаковые «Посмотреть тарифы и купить подписку».

Теперь мульти-тариф блок рендерится только когда подписки реально есть
(length > 0), а блок «нет подписок» показывает триал (если доступен) и
ВСЕГДА одну явную кнопку покупки — заодно закрывает случай «нет подписок и
триал недоступен» (раньше его прикрывал пустой мульти-тариф блок).
2026-06-03 11:17:00 +03:00
c0mrade
39bfef58d0 fix(subscriptions): «+ Купить ещё» только при платной подписке, явный CTA покупки
В мульти-тарифе кнопка «+ Купить ещё подписку» висела всегда, когда есть
хоть какая-то подписка — даже если это только триал или всё истёкшее.
Теперь она показывается только при наличии НАСТОЯЩЕЙ платной активной
подписки (hasActivePaid: не триал + status active/limited).

А «Посмотреть тарифы и купить подписку» из тусклой (пунктир + opacity-60,
спрятана в триал-блоке) стала ЯВНОЙ primary-кнопкой (solid accent) и
показывается, когда платной активной подписки ещё нет (триал/истёкшие/
новый юзер) — мы продаём подписки, путь к покупке должен быть очевиден.
Затронуты Dashboard и Subscriptions. Добавлен ключ subscriptions.browsePlans
(раньше рендерился только inline-фолбэк) в 4 локали.
2026-06-03 11:07:45 +03:00
c0mrade
3ce0c523f2 Merge pull request #448 from BEDOLAGA-DEV/release-please--branches--main--components--cabinet-frontend
chore(main): release 1.56.0
2026-06-02 22:57:30 +03:00
github-actions[bot]
e196c0a248 chore(main): release 1.56.0 2026-06-02 19:53:13 +00:00
c0mrade
03185632e8 Merge pull request #447 from BEDOLAGA-DEV/dev
Релиз: переработка статистики продаж, новый датапикер, INCY
2026-06-02 22:52:28 +03:00
c0mrade
7efe069be0 fix(admin-remnawave): причесать иконки в карточках статистики
Локальный StatCard держал иконку в чипе p-2, который подстраивался под
размер иконки (а часть иконок передавалась без className) — отсюда разные
по размеру чипы и «кривые» иконки. Привёл к виду карточек статистики продаж:
лейбл сверху, фиксированный чип h-9 w-9 с принудительным размером иконки
([&>svg]:h-5 w-5 нормализует все иконки разом), значение в тон чипу. Рамку
и subValue сохранил.
2026-06-02 21:15:33 +03:00
c0mrade
488503b360 fix(deeplink): поддержать схему incy:// в редиректе добавления подписки
Новое приложение INCY (incy://import/{url}) не открывалось на роутах
/add и /connect: DeepLinkRedirect держит хардкод-allowlist разрешённых
схем (happ, hiddify, v2rayng, streisand…), и isValidDeepLink резал
incy:// как неизвестную схему. Добавил incy:// в список.

Бэкенд не трогаю: _create_deep_link / _get_url_scheme_for_app собирают
deep-link из urlScheme Remnawave-конфига полностью обобщённо (без списка
имён), incy:// он строит сам. Mini-app путь (redirect.html) тоже generic.
2026-06-02 19:54:34 +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
40c684cc88 fix(sales-stats): «Выручка» → «Оборот по подпискам» во вкладке Продажи
Поле суммирует все подписочные платежи (новые + продления + докупки),
поэтому пересекалось по смыслу с вкладками Продления/Допы. Честное имя
«Оборот по подпискам» убирает путаницу: видно, что это итог по всем
подписочным платежам, а не только новые продажи. Все 4 локали.
2026-06-02 16:45:40 +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
a8213ce5d5 fix(datepicker): читаемость календаря в любой теме
Переменные --rdp-* библиотека объявляет на самом .rdp-root, поэтому
оверрайды на родителе (.rdp-dark) шадовились — отсюда синий акцент,
синяя рамка вместо заливки и тусклые дни. Теперь целимся в
`.rdp-dark .rdp-root` (специфичность выше базовой). Выбранный день —
залит акцентом, числа — яркие, сегодня — акцентная подсветка, все
цвета через токены темы (dark/accent), так что инвертируются для
светлых тем (champagne) и остаются читаемыми везде.
2026-06-02 16:32:24 +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
a687cc3d18 fix(sales-stats): крупные суммы сводки без копеек (короче, не режет)
formatWithCurrency по умолчанию даёт 2 знака без разделителей разрядов
(«2521000.00 ₽») — длинно и режется. Для итоговых сумм сводки (доход, доход с
допов, ручные пополнения) показываем целые рубли.
2026-06-02 15:26:12 +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
5e837621ad fix(stats-charts): убрать focus-outline артефакт при клике по графику
В recharts столбцы/секторы/точки становятся фокусируемыми, и при клике браузер
рисует свою обводку — артефакт на read-only графиках. Добавил гашение outline
для .recharts-wrapper/.surface/.sector/.rectangle.
2026-06-02 14:57:40 +03:00
c0mrade
33f8ddac9a fix(payment-labels): добавить лейбл OverPay (показывался сырым ключом)
METHOD_LABELS не содержал overpay, хотя метод есть в боте — в статистике/истории
он отображался как сырой ключ «overpay». Добавил человекочитаемое название.
2026-06-02 14:45:43 +03:00
c0mrade
ec346d38d8 fix(navigation): single-tariff subscription detail всегда Close, не Back
Регрессия после a62d689: Back прятался только при deep-link входе
(depthRef === 0). При внутреннем переходе на /subscriptions/:id (depth > 0)
показывался Back, а в single-tariff «назад» уходил в /subscriptions, который
тут же редиректит обратно на деталь (Subscriptions.tsx) — петля, и юзер видел
Back вместо Close.

Решение depth-независимое: если список-редирект всё равно отбрасывает на эту
же страницу (single-tariff, ≤1 подписки = listRedirectsToDetail, инверсия
listIsSafe из обработчика), деталь — это верхний экран. Прячем Back всегда,
Telegram показывает нативный Close. Петля невозможна по построению: без кнопки
Back обработчик navigate(-1) в редиректящий список просто не вызвать.

Multi-tariff и >1 подписки сохраняют реальный Back к осмысленному списку.
2026-06-01 20:29:03 +03:00
c0mrade
827e428f33 Merge pull request #446 from BEDOLAGA-DEV/release-please--branches--main--components--cabinet-frontend
chore(main): release 1.55.0
2026-06-01 18:35:43 +03:00
github-actions[bot]
c50a3d9416 chore(main): release 1.55.0 2026-06-01 15:35:17 +00:00
c0mrade
388fceddd9 Merge pull request #445 from BEDOLAGA-DEV/dev
Релиз: миграция иконок, /admin/remnawave, колесо, флаги, подарки
2026-06-01 18:34:17 +03:00
c0mrade
484c3ad005 fix(subscriptions): fill the orphan card so a lone subscription has no empty cell
The My Subscriptions list is a 2-col grid on sm+, so a single subscription (e.g. one tariff in multi-tariff mode) left an empty cell. Stretch the last card to span both columns when it's the lone one in its row — same orphan-fill used for the news grid and admin stat cards.
2026-06-01 18:19:57 +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
b877d7f175 fix(flags): render flag emoji on Windows globally via a scoped flag font
Windows ships no glyphs for regional-indicator flag emoji, so flags fell back to country letters everywhere react-twemoji wasn't manually wrapped. Bundle the Twemoji country-flags woff2 locally (no runtime CDN — matters for our audience) and add it as an @font-face scoped with unicode-range to flag codepoints only, then put it first in every Tailwind font stack (sans/display/mono). The browser now uses it solely for flag characters, so every flag renders correctly app-wide with zero markup changes and no effect on any other text.
2026-06-01 16:00:28 +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
97d436721a fix(admin-remnawave): node metrics 3-rows on mobile, original single row on desktop
Jumping only happened on mobile (the wrap reshuffled when speeds changed width); desktop fits on one line and never jumped. So keep the 3 fixed rows for mobile (sm:hidden) and restore the original single flex-wrap row for desktop (hidden sm:flex). On mobile, download/upload now sit tight in a flex row (gap-4) instead of a full-width 2-col grid that looked too spread out.
2026-06-01 15:42:29 +03:00
c0mrade
43acb70ab9 fix(admin-remnawave): lay node metrics in 3 fixed rows (processor/traffic/versions)
The metrics sat in one flex-wrap, so when realtime speeds change width (Kb/s<->Mb/s, large values) elements reflowed and jumped between lines, and the card height shifted. Split into 3 fixed rows: processor (CPU load + RAM), traffic (down/up), versions (remnanode + xray). Traffic uses a 2-column grid so a wide value never pushes its sibling; tabular-nums keeps digit widths constant.
2026-06-01 14:52:28 +03:00