Pair with the bot's #558449 follow-up. Pass getYandexCid() through
the four cabinet API methods that previously omitted it:
- switchTraffic (PUT /traffic)
- activateTrial (POST /trial — body added, was previously empty POST)
- updateCountries (POST /countries)
- switchTariff (POST /tariff/switch)
Without these the backend now-typed yandex_cid field would silently
stay None for these paid flows even when the user had a CID cached.
Pair-commit with the bot's #558449 fix. On the user's first cabinet
visit, the separate POST /yandex-cid that Metrika init fires may not
finish before the user clicks Buy. Pass the locally-cached CID
directly in every purchase request body — renewSubscription,
purchaseTraffic, purchaseDevices, submitPurchase, purchaseTariff —
so the backend can persist it synchronously and the offline-conversion
event fires reliably.
Sweep non-brand blue across admin pages, AnalyticsTab, TvQuickConnect, and menuLayout
style picker to accent tokens. Telegram-brand blue kept in BroadcastPreview (TG chat
mockup), the blocking screens (TG channel/bot CTAs), and the SuccessNotificationModal hero.
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.
Companion to bedolaga-bot a527df23. Bot's purchase-tariff endpoint
now accepts optional subscription_id to resolve the EXACT target row
by ID instead of doing a (user_id, tariff_id) re-lookup that races
with concurrent panel webhooks (produces 'Тариф уже активен' +
refund + no extension).
Frontend changes:
* subscriptionApi.purchaseTariff() — new optional subscriptionId arg
appended after trafficGb. Forwarded as subscription_id in POST body.
* SubscriptionPurchase.tsx — forwards URL searchParam subscriptionId
(set when user lands here from 'Renew this subscription').
* SubscriptionCardExpired.tsx — passes subscription.id when renewing
an expired daily tariff (the .purchaseTariff(tariff_id, 1) call).
Pairs with backend feat(deleted-users) — when the cabinet returns
403 {detail: {code: 'account_deleted', message, bot_username,
telegram_deep_link}} the SPA shows a dedicated screen instead of the
generic auth-error toast.
* Adds AccountDeletedScreen with localized title/description, an
Open-bot button using the backend-provided telegram_deep_link, and a
Retry button that clears the block + reloads (so the next request
observes the now-revived row).
* useBlockingStore gains accountDeleted slice mirroring the existing
maintenance/channel-subscription/blacklisted slots — consistent with
the precedent for cross-cutting 403 codes.
* apiClient: AccountDeletedError type + isAccountDeletedError guard +
response interceptor routes the 403 into the blocking store.
* i18n: ru/en/zh/fa parity for title, description, openBot, retry, hint.
No behavior change for ACTIVE users. BLOCKED users still get the
generic 403 (the backend keeps the message opaque for admin actions).
Pairs with the bot commit that adds the user_device_aliases table.
Surfaces the new local_name field on the existing devices list in
both the user-facing subscription page and the admin user-detail
page, with the same inline-edit pattern in both places.
User-side (src/pages/Subscription.tsx):
- pencil button next to each device row toggles edit mode
- input is focused automatically, capped at 64 chars (matches the
backend ALIAS_MAX_LENGTH and DB column width)
- Enter saves, Escape cancels, empty input clears the alias
- display priority: local_name → device_model → platform
- works identically in classic / single-tariff / multi-tariff —
subscriptionId is forwarded as a query param like every other
device-management endpoint already does
Admin-side (src/pages/AdminUserDetail.tsx):
- same pencil + inline input pattern, admin acts on behalf of the
user. notify.success on save, loadDevices() refresh.
API (src/api/subscription.ts + src/api/adminUsers.ts):
- new renameDevice(hwid, name, subscriptionId?) on subscriptionApi
- new renameUserDevice(userId, hwid, name) on adminUsersApi
- existing getDevices/getUserDevices contracts widened with
local_name?: string | null on the returned device shape
Locales (src/locales/ru.json):
- subscription.renameDevice / .renameDeviceSave / .renameDeviceCancel
/ .renameDevicePlaceholder / .deviceRenamed
- admin.users.detail.devices.rename / .renameSave / .renamed
Pairs with the bedolaga-bot commit that exposes
GET /cabinet/public/site-verification (JSON: { apay_tag: string | null }).
New useSiteVerification hook fires once on App mount, fetches the
configured tag value, and upserts <meta name='apay-tag' content='...'>
into document.head. When the bot returns null we proactively remove
any previously-rendered tag so toggling the env var off cleans up the
page.
Failure modes are silent — verification is best-effort and must
never block the cabinet from rendering. No admin UI field is needed:
the value lives in the bot's .env (ANTILOPAY_APAY_VERIFICATION_TAG),
matching how all other Antilopay credentials are configured.
Add force_delete_active_paid guard to prevent accidental deletion of
active paid subscriptions. Shows warning with count and requires
explicit checkbox confirmation. Also fixes allVisibleSubscriptionIds
to use filteredUsers and getFilteredSubs to respect trialOnly filter
on subscription sub-rows.
- 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)