framer-motion stagger раздаёт детям команду показаться один раз при монтировании
родителя; дети, смонтированные позже (когда resolve-ится react-query), оставались
в initial (opacity:0) — пустые страницы после F5/прямого захода.
- ConnectedAccounts, SavedCards: key={isLoading?...} перемонтирует контейнер
после загрузки → stagger проигрывается со всеми детьми;
- Balance (2), Profile, Support: явные initial/animate на условных staggerItem —
элемент самоанимируется при mount, вне оркестрации родителя;
- Wheel: исправлены несуществующие варианты hidden/show → initial/animate
(stagger истории спинов молча не работал).
Валидация на смердженном дереве: tsc, biome lint/format, build — чисто.
Правки #477 и #478 в общих файлах (ConnectedAccounts/Profile) сосуществуют.
Оркестрация staggerContainer/staggerItem у framer-motion раздаёт детям
команду «показаться» один раз — при анимации родителя на mount. Дети,
смонтированные позже (когда resolve'ится запрос react-query), в
оркестрацию не попадают и навсегда остаются в initial (opacity: 0, y: 8).
Симптом: после жёсткой перезагрузки (F5) на странице виден только
заголовок — карточки есть в DOM, но прозрачны. При SPA-навигации кэш
react-query тёплый, дети монтируются вместе с родителем — поэтому баг
проявлялся только при холодном заходе и маскировался на проде (нет
StrictMode, same-origin API, тёплый кэш почти всегда выигрывают гонку
с ~300-мс окном анимации).
Два способа фикса в зависимости от формы страницы:
1. key={isLoading ? 'loading' : 'ready'} на контейнере — перемонтирование
после загрузки, stagger проигрывается заново уже со всеми детьми.
Для страниц, где весь контент асинхронный (каскад сохраняется):
- ConnectedAccounts — карточки провайдеров
- SavedCards — список сохранённых карт
2. Явные initial="initial" animate="animate" на условном staggerItem —
элемент анимирует себя сам, не дожидаясь оркестрации родителя. Для
страниц, где асинхронны только 1-2 опциональные карточки (не
перезапускает анимацию всей страницы на каждый resolve):
- Balance — «Способы пополнения» и ссылка «Сохранённые карты»
- Support — карточка «Написать нам» (support_type='both')
- Profile — реферальный виджет
Попутно: в Wheel у контейнера истории спинов стояли несуществующие
варианты initial="hidden"/animate="show" (у staggerContainer/staggerItem
ключи — initial/animate/exit), из-за чего stagger там молча не работал
вовсе; имена исправлены.
Проверено и не требует правок: TopUpAmount (early-return до загрузки
методов), TopUpMethodSelect и MergeAccounts (динамический контент —
обычный HTML внутри всегда смонтированных staggerItem), вложенный
контейнер транзакций в Balance (самоанимирующийся).
Один инструмент вместо девяти 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.
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).
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.
- 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)
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)
Sweep 30 files: text/bg/border/ring/from/to/via/fill/stroke/shadow/divide/decoration/
outline/placeholder-red-N → -error-N. All red usages were semantically error/danger
(no brand red), so tokens now flow through the design-system CSS variables and respond
to palette overrides.
When user has 2+ eligible subscriptions and chooses subscription_days
payment, a visible warning hint now appears explaining they need to
select a subscription before spinning.
- API: spin() accepts subscriptionId, WheelConfig has eligible_subscriptions
- Auto-select subscription when only one eligible
- Show subscription picker when multiple eligible (days payment)
- Disable spin button until subscription selected (when >1 eligible)
- Daily tariffs excluded from eligible list
- Fix stale initData comparison in clearStaleSessionIfNeeded that destroyed
valid refresh tokens on mobile WebView reopens
- Restrict X-Telegram-Init-Data header to auth endpoints only
- Close Mini App on auth retry to force fresh initData from Telegram
- Merge Connection page error/not-configured states for better UX
- Remove unnecessary comments across 40+ files (section dividers,
restating comments, noise catch block comments)
- Configure ESLint allowEmptyCatch to properly handle intentional
empty catch blocks (62 warnings resolved)
- Add has_subscription to WheelConfig type
- Disable spin button when user has no subscription
- Add noSubscription guard in handleUnifiedSpin handler
- Show warning banner directing user to activate subscription
- Prevent banner stacking (noSubscription takes priority over daily limit)
- Add noSubscription i18n keys for ru, en, zh, fa
Stars payment flow previously used Math.random() for wheel rotation,
causing visual mismatch between where the wheel lands and the actual
prize won. Now polls for backend result first, calculates the correct
rotation angle matching the prize sector, then starts the animation.
Trial users saw an "insufficient balance" prompt suggesting to top up
rubles, but the wheel only accepts Telegram Stars (via invoice) or
subscription days. Stars payment via invoice doesn't require ruble
balance, so the spin button should not be disabled for it.
- Replace confirm dialog with inline UI panel to avoid browser popup
blockers — pay button click is a direct user gesture
- Pre-open browser window synchronously on pay click for fallback
- Unify payment type display: show same styled panel whether one or
both methods are enabled (single method = full-width button)
- Remove getCostSubtitle text, add payStars translation key (ru, en,
zh, fa)
- Show confirmation dialog before redirecting to Telegram for Stars
payment (native popup in Mini App, confirm in browser)
- Warn admin when saving wheel config with Stars enabled but
Telegram Stars payment method is disabled globally
- Add translation keys: confirmStarsTitle, confirmStarsPayment,
starsNotEnabledGlobally (ru, en, zh, fa)
- Use Telegram Stars invoice on all platforms instead of balance
payment on desktop
- Show star cost in toggle and subtitle everywhere
- Show popup/toast when Stars are not enabled and user tries to spin
- Remove platform-specific payment type auto-select logic
- Add starsNotAvailable translation key (ru, en, zh, fa)
The auto-select useEffect for payment type (days/stars) was running
on every config refetch, resetting the user's manual selection back
to stars. Now it only runs once on initial load using a ref flag.
- Fix lights animation to run continuously (even when idle)
- Lights now move sequentially around the circle
- Remove text labels from wheel, keep only emoji
- Add WheelLegend component with color indicators
- Display legend on right (desktop) and bottom (mobile)
- Add 'prizes' translation key for all languages
- Improve wheel rotation animation with accumulated rotation tracking
- Enhance LED lights with running animation effect
- Replace sidebar history with collapsible section
- Unify spin button for all payment methods
- Update admin prize card styles with new borders and backgrounds
- Add improved drag-and-drop collision detection
- Prefer Telegram Stars payment in Mini App by default
Wheel.tsx (user page):
- Always show history sidebar on desktop
- Show placeholder when no history
- Remove history toggle button
AdminWheel.tsx:
- Replace auto-save with form + Save button
- Fix toggle switch styling
- Replace HTML5 drag-drop with @dnd-kit for mobile/Telegram
- Improve mobile responsiveness (tabs, cards, buttons)
- Add live wheel preview on desktop
- Group settings into sections
FortuneWheel.tsx:
- Slow down LED animation (250ms -> 600ms)
Navigation:
- Add wheel to mobile bottom nav (replaces Support when enabled)
- Add wheel button to mobile bottom nav (replaces Support when enabled)
- Slow down LED lights animation (250ms -> 600ms)
- Replace result modal with inline card in Wheel.tsx
- Add haptic feedback on spin result
- AdminWheel.tsx: inline prize editing instead of modal
- AdminWheel.tsx: drag-and-drop for prize reordering
- AdminWheel.tsx: live wheel preview on desktop
- AdminWheel.tsx: group settings into sections
- Replace confirm() with native dialog
- Add translations for all languages