- Add per-landing analytics goals (view/click) with admin editor toggle
- Add sticky pay button option for mobile landing pages
- Add daily purchases bar chart (created vs paid) to landing stats
- Replace single purchase count with created/paid split in stats summary
- Add referrer tracking to purchases with hostname display in stats
- Add time display to purchase cards alongside date
- Pass user timezone to stats API for correct daily grouping
- Clamp referrer (500 chars) and subid (255 chars) to backend limits
- Persist contact value per-landing-slug in localStorage
- Fire buy_success analytics goal on successful delivery
- Export USER_TIMEZONE from format utils
- Add analytics/stats translations for fa.json and zh.json locales
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Pass yandex_cid to all auth endpoints (telegram, email, OIDC, OAuth)
- Add OfflineConvGoal interface and offline_conv_* fields to AnalyticsCounters
- Add storeYandexCid API method for server-side CID persistence
- Add analytics fields to LandingConfig (view/click goals, sticky_pay_button)
- Add yandex_cid/referrer/subid to PurchaseRequest
- Add offline conversions UI block in admin AnalyticsTab
- Add cacheYandexCid, syncYandexCid, fireAnalyticsEvent to analytics hook
- Create yandexCid.ts utility (localStorage get/set helpers)
- Add sticky pay button with portal on mobile in QuickPurchase
- Fire view/click analytics goals on landing pages
- Persist contact value and referrer/subid in session/localStorage
- Add i18n keys for offlineConv and apiKey in all 4 locales
- Add media_items array support to ticket messages (types, API clients)
- Create shared MessageMediaGrid component with photo grid, fullscreen
viewer, keyboard nav, video/document rendering
- Replace per-page AdminMessageMedia/MessageMedia with MessageMediaGrid
- Add linkifyText util (DOMPurify-sanitized) for auto-linking URLs
- Support multi-file upload (up to 10) in AdminTickets and Support pages
- Remove unused ticketsApi import from AdminUserDetail
- Load panelInfo eagerly on page load (not just on subscription tab)
- Add VPN connection card to info tab: last/first connection, online status indicator, last node
- Rename "Last activity" to "Bot activity" to distinguish from VPN connection
- Show cabinet_last_login field that was never rendered
- Add collapsible subscription request history section in subscription tab with paginated table
- Add subscription request history API types and method
- Add i18n keys for all 4 locales (ru, en, zh, fa)
Add campaign and partner filter dropdowns for user filtering, implement
delete_user bulk action with delete_from_panel checkbox, fix ActionModal
to use createPortal with viewport-fixed positioning and safe area insets.
- Add replaces_tab field to admin editor with conflict detection
- Show custom InfoPages as extra tabs on /info page
- Replace built-in tab content when replacement is configured
- Rich DOMPurify sanitizer for TipTap content (images, video, iframe)
- Fix query race: built-in queries wait for tab-replacements to load
- Horizontal scroll for tabs on mobile, 44px touch targets
- Responsive prose: scrollable tables, h-auto on img/video, light theme borders
- Loyalty tier names truncation, progress bar labels stack on mobile
- Remove "Days" column from main table (each subscription shows own)
- Tariff column shows ALL subscription tariffs as comma-separated list
- Subscription sub-rows show device count with phone icon
- New bulk action "Изменить устройства" (set_devices) with input 1-50
- Update UserListItemSubscription type with device_limit field
- i18n: setDevices + deviceLimit keys (ru + en)
- Detect multi-tariff mode when users have multiple subscriptions
- Expandable user rows with chevron: click to show subscription sub-rows
- Each subscription sub-row shows: tariff name, status badge, days
remaining (color-coded green/amber/red), traffic progress bar
- Independent subscription checkboxes for subscription-level actions
- FloatingActionBar shows dual counters: users (accent) + subscriptions
(green), actions grouped by target type with disabled state
- Subscription-level actions send subscription_ids, user-level send
user_ids to the backend
- Backward compatible: single-tariff mode unchanged (no chevrons,
no sub-rows, user-only selection)
- i18n: subscriptionsSelected, usersSelected, expand/collapse,
daysUnit, trafficOf, trafficGbUnit (ru + en)
- Update UserListItem type to include tariff_id, tariff_name,
traffic_used_gb, traffic_limit_gb, device_limit, days_remaining
from the backend (now returned in user list API)
- Fix tariff column: was showing subscription_status, now shows
actual tariff_name from the API response
- Real-time progress: SSE streaming with animated progress bar,
live success/error counters, scrollable log of last 10 results
with auto-scroll, percentage display
- New action "Выдать подписку": tariff selector + days input,
warning about users with existing subscriptions being skipped
- Error details: collapsible section with per-user error list,
colored summary line (green successes / red errors)
- Fix: modal cannot be closed during loading (backdrop + Escape)
- Fix: row checkbox aria-labels now user-specific
- i18n: full progress/grant/error translations (ru + en)
Full-featured admin page for mass operations on users:
- TanStack Table with checkbox selection (select all / individual)
- Server-side pagination, search, and filters (status, tariff, promo group)
- Columns: user, subscription status, tariff, balance, days left, promo group
- Floating glass-effect action bar with 7 bulk operations:
extend subscription, deactivate, activate, change tariff,
add traffic, add balance, assign promo group
- Action modal dialogs with input fields, loading state, result summary
- Responsive design for desktop, mobile, and Telegram Mini App
- Full i18n support (ru, en)
- Dark theme with accent highlights and status color badges
* fix(connection): respect happ_cryptolink mode
- read connect_mode from connection-link endpoint
- auto-redirect to happ cryptolink flow when mode is HAPP_CRYPTOLINK
- keep installation guide behavior for other modes
* docs(readme): fix source build step 2
- use compose service name for build/create commands
- copy static files from created compose container
- remove compose container via docker compose rm
* fix(connection): handle mode and ws path
- treat any non-guide connect mode as direct happ redirect
- wait for connection-link response before rendering installation guide
- build websocket URL from VITE_API_URL (supports /api and absolute URLs)
* docs(readme): fix docker cp static path
- copy /usr/share/nginx/html contents via html/.
- prevent nested /srv/cabinet/html deployment and 404 on root
* fix(connection): keep guide, use happ links
- restore guide page behavior for /connection (no auto redirect)
- use happ cryptolink URL in /connection/qr when happ crypt mode is active
- replace subscription page connection URL with happ cryptolink from connection-link API
* fix(connection): avoid wrong redirect flow
- force happ scheme deeplink in HAPP_CRYPTOLINK mode for guide connect buttons
- use cabinet redirect page only inside Telegram Mini App
- open deeplink directly in regular browsers
* fix(connection): enforce happ cryptolink
- prioritize backend crypt deeplink fields in HAPP_CRYPTOLINK mode
- fallback to local crypt4/crypt3 generation from subscription URL when backend returns plain happ://sub...
- apply same resolution for guide open action, qr page source and subscription page link display/copy
* fix(subscription): truncate long connect link
- render connection URL in a single line with ellipsis on /subscriptions
- preserve full URL in tooltip for easier manual copy
- keep copy button behavior unchanged
* fix(connection): build cryptolink from happ sub
- allow cryptolink fallback generation from happ://sub... URLs in addition to http(s)
- prevent plain happ://sub links from leaking into UI in HAPP_CRYPTOLINK mode
Frontend now reads campaign slug from localStorage and includes it
in the registration API call, ensuring campaign attribution works
even if email verification happens in a different browser.
- 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)
- 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)
- 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
- 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
- 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
- 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
All backend endpoints accept subscription_id via Query(...) parameter,
but frontend was sending it in the request body via bodyWithSubId helper.
This caused multi-tariff operations (pause, autopay, renew, traffic,
devices, servers, tariff switch) to silently fail — backend couldn't
find subscription_id in query params and fell back to first subscription.
Changed bodyWithSubId to return [body, config] tuple with subscription_id
in params, spread into axios calls as separate arguments.
- Add SubscriptionListItem and SubscriptionsListResponse types
- Add getSubscriptions/getSubscriptionById API methods
- Add optional subscriptionId parameter to all 30+ API methods
- Create /subscriptions page with subscription list cards
- Add /subscription/:subscriptionId route for per-subscription management
- Dashboard: show multi-tariff navigation block when user has 2+ subscriptions
- Subscription page: read subscriptionId from URL params, pass to all queries
- SubscriptionPurchase: accept subscriptionId from query params
- SubscriptionCardExpired: pass subscription.id to API calls
- Full backward compatibility: without subscriptionId, works as before
- Fix tag using category_color instead of its own color when editing
- Add FormData auto-detection interceptor in client.ts (root cause of 422)
- Remove redundant Content-Type overrides from all 5 upload endpoints
- Fix falsy id check (id: 0 treated as null) with strict comparison
- Await invalidateQueries to prevent stale dropdown data
- ColoredItemCombobox component with search, color swatches, inline creation
- Replace text inputs with combobox dropdowns for category and tag selection
- API methods for categories/tags CRUD
- TypeScript types for NewsCategory, NewsTag
- i18n keys for combobox (ru, en)
- Register DOMPurify hooks at module init (not per sanitizeHtml call)
- Always set iframe allow attribute (not conditional on existing)
- Add AbortController to featured image upload for unmount cleanup
- Check defaultPrevented in React onDrop to prevent double upload with TipTap
- Remove explicit Content-Type header on upload (axios auto-sets boundary)
- uploadCount: always decrement in finally (prevent permanent uploading state)
- AbortSignal: pass to actual HTTP request (cancel network, not just UI)
- Concurrent uploads: use Set<AbortController> instead of single ref
- DOMPurify hooks: scope inside sanitizeHtml() to avoid global pollution
- Video src: restrict to HTTPS only (was allowing HTTP)
- GIF: remove from accept attributes (backend rejects GIF)
- Ref mutation: move handleMediaUploadRef update to useEffect
- 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.)
- 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