- Encode underscores as %5F in share URLs (bot link + cabinet link)
to prevent Telegram auto-link detection from stripping trailing _
- Strip GIFT-/GIFT_ prefix from URL code param before pre-filling input
- Use ?? instead of || for prefix stripping to preserve empty strings
- Add language field to balance topup and landing purchase requests
- Cap news article featured image height with max-h-96
Cherry-picked from PR #362 (without favicon change — files don't exist)
- Add-referral search: exclude users already in referrals list
- Both search debounces: clear stale results immediately when query
changes to prevent previous results flashing during debounce
- New search UI in referrals list section to find and bind users
as this user's referrals (calls assign-referrer on target user)
- Debounced search with cancelled flag and loading state
- Click-outside to close dropdown
- i18n: addReferral, referralAdded keys in all 4 locales
- Commission display: use conditional instead of nullish coalescing
to avoid rendering "default%" (now shows "default" without %)
- loadReferrals: add || [] guard on data.users to prevent crash
- Search debounce: add cancelled flag to prevent stale responses
overwriting fresher results
- Search dropdown: add loading state to prevent "No users found"
flash during debounce wait
- New Рефералы tab with three sections:
- Referred By: shows who referred this user, assign/remove referrer
- Stats grid: total referrals, earnings, commission, referral code
- Referrals list: all referred users with navigate and remove buttons
- User search with debounce for assigning referrer
- Shows warning when selected user already has a referrer
- API methods: assignReferrer, removeReferrer, removeReferral
- i18n translations for all 4 locales (en, ru, zh, fa)
- SettingsTab: use searchQuery.trim() instead of searchQuery to prevent
whitespace-only input from triggering empty search results view
- AdminSettings: fallback to branding when activeSection is unknown
instead of rendering blank content area
- SettingsSearchMobile: change sm:hidden to lg:hidden to match mobile
layout breakpoint (search was invisible on 640-1023px tablets)
- SettingsTableRow: sync isLongValue suffixes with SettingInput
(_list, _json, _periods, _discounts were missing)
- SettingsTableRow: read-only bools now show enabled/disabled labels
instead of raw "true"/"false" strings
- Use activeSection instead of activeTreeInfo as useMemo dependency
(activeTreeInfo is derived from activeSection, new object each render)
- Add category label matching to sidebar autocomplete search filter
to match global search behavior
- Replace flat sidebar with collapsible tree groups (7 groups, 52 sub-items)
- Map all 61 backend category keys to sidebar navigation
- Add QuickToggles panel for boolean settings
- Replace card-style SettingRow with compact SettingsTableRow
- Add breadcrumb navigation for tree sub-items
- Add source badges (modified/DB/ENV) on settings
- Implement two-level mobile navigation with group chips
- Add i18n translations for all new keys (en, ru, zh, fa)
- Add NodeRealtimeStats and InboundTraffic TypeScript interfaces
- Type getNodesRealtime return as NodeRealtimeStats[]
- Add new Traffic tab to AdminRemnawave page showing per-node
inbound/outbound traffic sorted by total bytes
- Display download/upload/total per inbound tag for each node
- ServerInfo: remove cpu_physical_cores, memory_available
- NodeInfo: replace cpu_count/cpu_model/total_ram with versions/system,
xray_uptime changed to number, users_online now required
- NodeStatus: same field updates, remove dead uptime field
- Extract formatUptime to shared utils/format.ts
- Update AdminRemnawave and AdminDashboard components
The CosmosBackground orbs were overlapping the app's existing theme
background, creating a double-layer effect. Removed the component
entirely so only the theme background is visible.
- Rewrite AdminPanel with cosmic floating orbs background using theme colors
- Add stats bar: uptime, bot version, cabinet version, trial/paid counts from API
- Glass morphism cards with 3D tilt effect (rAF-throttled) and entry animations
- Live search with Cmd+K shortcut, highlight matches, permission-aware filtering
- Register Tailwind `light:` variant for proper light theme support
- Mobile optimizations: reduced orbs (3), lower blur (80px), compact 2-col stats
- Telegram MiniApp safe area support via useTelegramSDK
- Full accessibility: aria-hidden on decorative SVGs, aria-labels, role="status"
- React.memo on StatsBar, GlassCard, CosmosBackground for render performance
- Memo-stable section refs via Set<id> pattern to preserve GlassCard memo during search
- i18n keys added to all 4 locales (ru, en, zh, fa)
- Add subscription dropdown to sync tab when user has 2+ subscriptions
- Pass subscription_id to getSyncStatus, syncFromPanel, syncToPanel
- loadSyncStatus re-fetches when selected subscription changes
- Show tariff name in UUID info section from API response
- Add selectSubscription i18n keys to all 4 locales
- device_price_kopeks: change > 0 to >= 0 so 0 (free) is sent instead of undefined
- traffic_topup_packages: change > 0 to >= 0 so free traffic packages can be added
- Reset isKeyboardOpen on route change so bottom nav always restores
- Disable autoFocus on payment input in Telegram Mini App
- Skip auto-focus on Profile email inputs in Telegram
When a new build changes chunk hashes, users with cached index.html
get "Failed to fetch dynamically imported module" errors. Now:
- lazyWithRetry() wraps all lazy imports with auto-reload on failure
- ErrorBoundary catches chunk errors and auto-reloads once
- 30s guard in sessionStorage prevents reload loops
- Add tariff_id/tariff_name to PromoCode types
- AdminPromocodeCreate: tariff dropdown for trial_subscription type
- Shows default trial tariff or warns if none configured
- Balance page: handle select_subscription response — show subscription
picker when promo code adds days and user has multiple tariffs
- API: activatePromocode now accepts optional subscriptionId
- AdminPromocodeCreate: show trial tariff info when creating
trial_subscription promo code, warn if no trial tariff configured
In single-tariff mode, Subscriptions list auto-redirects to detail page.
Back button from detail now goes to / (dashboard) instead of /subscriptions,
breaking the redirect loop.
Subscriptions.tsx: navigate() in render → <Navigate> component (React safe).
Subscription.tsx: same fix for multi-tariff redirect.
AdminUserDetail.tsx: useRef guard for one-time auto-select instead of
activeSubscriptionId in dependency array (prevented manual selection).
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.
SubscriptionCardActive: passes ?sub= to /connection navigation.
Connection: QR state includes subscriptionId for back-navigation.
ConnectionQR: preserves subscription context on back button.
Dashboard + Subscription: localStorage traffic cache keyed per-subscription
to prevent cooldown bleed between different subscriptions.
Level 1 (subscription list):
- Cards with tariff name, status badge, traffic, date, devices
- Click to drill into subscription detail
- "Создать подписку" with filtered purchased tariffs
Level 2 (subscription detail):
- Back button to return to list
- All actions scoped to selected subscription
- No "Сменить тариф" in multi-subscription mode
- Panel info, devices, traffic stats per-subscription
Single subscription: keeps current behavior (direct detail view)
- 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
- New WebBackButton component: visible on web, hidden in Telegram
(native back button handles navigation there)
- Added to: Subscription detail, SubscriptionPurchase, RenewSubscription
- RenewSubscription: removed max-w-lg, grid layout for period cards
- Remove max-w-lg constraint to match dashboard width
- Grid layout: 2 columns on desktop, 1 on mobile
- Move "Новый тариф" button to header row as accent-styled button
- Larger empty state with more breathing room