Node fill color now encodes subscription status (paid/trial active/expired),
while border color indicates referral role (partner/top/active referrer).
Added BFS-based radial initial positioning to spread referral branches outward.
Updated legend with two sections and adjusted FA2 gravity for less clustering.
Add subscription status colors: green (paid active), blue (trial active),
red (paid expired), orange (trial expired). Extract NODE_COLORS constant
as single source of truth. Add SubscriptionStatus union type. Show status
badge in user detail panel. Add i18n for all 4 locales.
Superadmin role (level 999) is managed via env config only. Hide X
button for superadmin assignments and show ENV badge instead. Filter
superadmin from assignable roles dropdown. Add envManaged translation
to all 4 locales.
When users hit 'Try Again' after auth failure, clear ALL cached state
(tokens, SDK launch params, initData, zustand persist, tg_user_id)
to prevent stale token loops. Applies to both Login and TelegramRedirect
retry handlers.
When the purchase-options API call failed (e.g. for email-registered
users), the page rendered only the header with no content. Now shows
an error message with retry button. Also adds a fallback for when
the API returns no available tariffs or periods.
Subscription card showed stale traffic from local DB (0.0 GB) while
the live traffic section below showed correct data from panel (3.52 GB).
Now uses panelInfo.used_traffic_bytes when available, with DB fallback.
- Add inline custom button editor (no popups): label, type
(callback/URL), action value with byte-level validation
- Frontend validates callback_data in UTF-8 bytes via TextEncoder
- URLs restricted to https:// and tg:// matching backend
- Form wrapped in <form> for Enter key submit support
- Fix pre-existing stale mediaType bug in file upload handler
- Translations added for ru, en, zh, fa locales
- Remove redirect to Telegram bot when email auth disabled + referral code present
(cabinet links should stay on cabinet, bot links should go to bot)
- Remove referral_code from deep link auth (existing users can't get referrals)
- Don't consume referralCode in deep link path — leave it in localStorage for
OIDC/widget auth methods that actually send it to the backend
- Consume campaign slug once into ref to survive retries (codesConsumedRef pattern)
- Update loginWithDeepLink to only accept (token, campaignSlug) — no referralCode
- Update pollDeepLinkToken API to match backend schema change
When the Telegram OIDC widget fails to load (ad blockers, DNS, CSP),
the deep link polling fallback caused an infinite page reload:
1. pollDeepLinkToken received 202 (pending) which axios treated as success
2. loginWithDeepLink stored undefined tokens with isAuthenticated=true
3. checkAdminStatus() fired with invalid token → 401
4. Response interceptor called safeRedirectToLogin() → page reload → loop
Fixes:
- pollDeepLinkToken: validateStatus rejects non-200 (202 now caught in poll loop)
- AUTH_ENDPOINTS: add /cabinet/auth/deeplink/ and /cabinet/auth/login/auto
- safeRedirectToLogin: guard against redirect when already on /login
- loginWithDeepLink: validate response tokens before storing
- setTokens: validate tokens in store method (protects AutoLogin, VerifyEmail, etc.)
- Raise portal z-index to z-50 so it covers MobileBottomNav
- Use TG SDK bottom safe area (JS) via CSS variable instead of unreliable env()
- Fix TicketNotificationBell regression: conditional inline style for fullscreen only
- Add bottom safe area padding to detail panel scroll areas
- Extract useHeaderHeight() hook — single source of truth for header
height across AppShell, ReferralNetwork, and TicketNotificationBell
- Portal top offset now computed dynamically from TG safe area insets
instead of hardcoded top-16 (fixes overlap with TG header bar)
- Two-row mobile layout: title + selector on separate rows so dropdown
gets full width on small screens
- Add MOBILE_HEADER_HEIGHT_PX and DESKTOP_HEADER_HEIGHT_PX to constants
- Fix TicketNotificationBell using hardcoded 45px for all platforms
- removeScope preserves graph state when items remain (only resets on empty)
- Extract shared CheckIcon, CloseIcon, Spinner, ScopeListItem components
- Unify three handleSelect* into single handleToggle
- Fix shared mutable Set in reset state (factory function)
- Add MAX_SCOPE_ITEMS UI feedback (disabled button, warning banner)
- Correct ARIA: dialog > tablist + listbox > option structure
- Event listeners only attached when dropdown is open
- isSelected uses Set lookup via useMemo instead of O(n) scan
- Clear user search only on add, not on deselect
- Replace full graph with scoped view (campaign/partner/user)
- ScopeSelector component with tabs and searchable dropdown
- Store resets all state on scope change (selection, hover, filters)
- Remove dead NetworkSearch component and graphInstance globals
- i18n keys for scope selector in all 4 locales
Portal to document.body escapes main's contain:content which broke
fixed positioning (container had height:0 and was clipped to main).
Visual fixes for 662+ node graphs:
- labels only on large nodes (threshold 6→14, density 0.5→0.12)
- leaf nodes 5px, referrers 10px, campaigns 16px
- edge opacity reduced to prevent white starburst
- default edge color opacity lowered
- fix top offset breakpoint: sm:top-14 → lg:top-14 to match AppShell
header visibility (mobile header is lg:hidden with h-16)
- add gravity to FA2 layout to prevent disconnected nodes from flying
to extreme positions, making auto-rescale shrink everything to dots
- clamp outlier node positions after FA2 finishes (3σ threshold)
- add ResizeObserver to sync sigma canvas with container size changes
- call sigma.resize() before camera reset after FA2
- increase node sizes: user min 8→12, max 40→50, campaign 18→24
- reduce FA2 duration 5s→3s and slowDown 5→2 for tighter layout
- Switch to CSS Grid layout for reliable content area height
- Use inferSettings() for ForceAtlas2 auto-tuning based on graph structure
- Enable linLogMode and adjustSizes for better node clustering
- Fit camera after layout settles with animatedReset
- Increase node sizes for better visibility (MIN 8, MAX 40)
- Center controls at bottom, fix overlay positions
- Page uses top-16/sm:top-14 to start below AppShell header (z-50)
instead of inset-0 which renders behind the header
- Sigma init polls with RAF until container has real dimensions
instead of allowInvalidContainer which creates broken canvas
- Removed overflow-hidden from content area (not needed)
Container has no computed height when useEffect fires immediately
after data arrives. Defer to next animation frame so browser layout
completes. Also add allowInvalidContainer as safety net.
- Page now uses fixed inset-0 overlay to fill entire viewport
- Simplified top bar to single row (back + title + search + filter)
- Filter dropdown uses absolute positioning on desktop (no layout shift)
- Filter button always visible, toggles dropdown on click
- Split top bar into two rows on mobile: title + filter icon, then
full-width search below
- Desktop keeps single-row layout with search and filter inline
- Filter button: icon-only on mobile with active indicator dot
- Filter panel: full-screen overlay with backdrop on mobile,
inline dropdown on desktop
- Outside-click closes filter panel
- Removed subtitle (only on desktop, saves space)
- Increase FA2 scalingRatio to 10 and decrease gravity to 0.5 for
better node spacing (less cluttered graph)
- Use 100dvh instead of 100vh for Telegram Mini App viewport compat
- Hide legend on mobile to prevent overlay collision
- Move controls to bottom-right on mobile, bottom-center on desktop
- Make stats single-column on mobile for compact layout
Admin can now attach photos, videos, and documents when replying to
tickets. Upload happens on file selection via existing media upload
endpoint. Includes attachment preview with type-specific icons,
upload progress, error handling, race condition protection, and
proper blob URL lifecycle management.
Added video player rendering for video attachments in message thread.
Updated ticket list preview to show [media_type] for media-only messages.