- Extract DEVICE_ALIAS_MAX_LENGTH to src/constants/devices.ts so the
user page and admin page share one source of truth (mirroring the
bot's ALIAS_MAX_LENGTH=64). Was duplicated as a magic 64 in both
files (MED-5).
- Replace ✓ / ✕ / ✏️ glyphs in AdminUserDetail.tsx with inline SVGs,
matching the icon style already used by Subscription.tsx for the
same rename/save/cancel actions. Eliminates the platform-dependent
emoji rendering inconsistency (MED-4).
- Add haptic feedback to the user-side renameDeviceMutation —
notification('success') on save, notification('error') on failure.
Brings it in line with the other Subscription.tsx mutations
(LOW-9).
- Translate the new rename keys into zh.json and fa.json (en.json
already covered in the previous follow-up). Cabinet now ships full
parity across all 4 locales for the rename UI.
Code-review follow-up on 321c65b.
- prettier ate the raw glyphs (✓/✕/✏️) in AdminUserDetail.tsx and emitted
\u2713 / \u2715 / \u270F\uFE0F as JSX text children. JSX does NOT
process \u… escapes inside element children, so admin users saw the
literal six-character strings on Save/Cancel/Rename buttons. Wrap each
glyph in a JS expression {'…'} so it survives both prettier and JSX.
- renameDeviceMutation.onSuccess on Subscription.tsx no longer
unconditionally clears the edit state. If the user already navigated
to a different device while the previous save was in flight, we keep
their in-progress input. Same shape applied to AdminUserDetail.tsx —
the alias is snapshot before the await so a race can't smuggle the
wrong value into the request, and the post-success reset is guarded.
- AdminUserDetail rename error path now surfaces the backend's
response.data.detail (e.g. '64-char limit' message) instead of the
generic localized userActions.error toast.
- Add the new rename i18n keys to src/locales/en.json so English users
no longer fall back to Russian labels. ua/zh/fa to follow in a
dedicated translation pass.
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
User reported the EE (Estonia) flag rendered as plain text 'EE' on the
admin servers page. Root cause: getCountryFlag() in AdminServers.tsx
and AdminServerEdit.tsx hardcoded a 25-entry codeMap that didn't
include EE; falling through to 'return code' produced raw text.
Two other admin pages (AdminRemnawaveSquadDetail, AdminUserDetail)
had bigger 35-entry maps that did include EE — so EE worked there
but MX/AR/EG/ZA and friends still wouldn't. AdminRemnawave repeated
the same 35-entry map. AdminTrafficUsage already had the correct
algorithmic ISO→regional-indicator code but as a local duplicate of
utils/subscriptionHelpers.
Unify all six on the single algorithmic helper:
- getFlagEmoji() in utils/subscriptionHelpers.ts now:
* accepts string | null | undefined (callers don't need to guard)
* trims whitespace
* validates [A-Za-z]{2} before composing regional indicators
- Each admin page now either imports getFlagEmoji directly or wraps
it with the page's preferred fallback character (e.g. '🌍' for
empty codes where the UI expects a placeholder).
- 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
- Show VPN connection block even when panelInfo not yet loaded
- Add subscription dropdown when user has multiple subscriptions
- Show spinner while loading, "no data" when panel returns not found
- Changing subscription in VPN block reloads panel info for that sub
- Request history subscription selector no longer mutates shared
activeSubscriptionId — uses independent requestHistorySubId state
- Request history reloads when subscription selector changes
- Split mount effect: loadPanelInfo in separate effect to avoid
redundant loadUser calls when activeSubscriptionId changes
- 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)
- SubscriptionListCard: show traffic progress bar for limited subs
in multi-tariff list (was hidden because isActive excluded limited)
- SubscriptionListCard: amber border/background for limited status
cards (was using default neutral style, inconsistent with rest of UI)
- Subscription page: hide delete button for limited subs in
multi-tariff mode (limited subs still have valid days remaining)
- SubscriptionPurchase: allow tariff switch for limited subs
(was blocked by is_active-only gate in canSwitch formula)
- AdminUserDetail: include limited status in "already purchased"
tariff filter to prevent duplicate subscription creation
- 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)
- 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
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).
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)
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.
- 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 is_limited to Subscription type
- Show yellow traffic-exhausted badge on Subscription and AdminUsers pages
- SubscriptionCardExpired: amber theme with warning icon and buy-traffic CTA for limited
- Dashboard: route limited subscriptions to SubscriptionCardExpired
- Subscription page: show additional options (buy traffic) for limited subs
- Use is_limited boolean instead of status string comparison
- Add trafficLimited translations for ru, en, zh, fa
- AdminUserDetail StatusBadge: add limited yellow styling
memory.used from systeminformation includes disk cache/buffers,
showing ~91% when actual usage is ~18%. Now uses (total - available)
which matches htop/free output. Also added PB/EB units to formatBytes.
Show connected devices in subscription tab with ability to:
- View device platform, model, HWID and connection date
- Delete individual devices
- Reset all devices at once
- Info tab: promo group card with inline dropdown to change/remove group
- Balance tab: active promo offer display with deactivate button
- Balance tab: send promo offer form (discount %, valid hours)
- Add deactivateDiscount API method to promocodesApi
- Add locale keys for ru/en/zh/fa
- Fix race condition in subscription create (stale state read)
- Fix unhandled promise in handleInlineConfirm
- Remove unused legacy adminUsers block from ru.json
- Load panel + node data once on tab open (not per period change)
- Compute 7/14/30 day totals locally from cached daily data
- Add refresh button to subscription section
- No API calls when switching periods
- Add country_code to UserNodeUsageItem type
- Add getCountryFlag helper for country code to emoji mapping
- Show country flag next to node name in usage bars
- Add campaign card and referrals list to info tab
- Add panel config/links, live traffic, connection info to subscription tab
- Add node usage bars with 7/14/30 day period selector
- Add getPanelInfo/getNodeUsage API methods and types
- Add 18 new i18n keys across 4 languages
Replace ticket navigation with inline chat view. Clicking a ticket
opens the conversation directly within the user detail page with
message history, media support, and reply form.
Add ability to view user's tickets directly from the user detail page.
Backend gets user_id filter on tickets endpoint, frontend shows ticket
list with status badges, message preview and navigation to ticket.
- Add fullDeleteUser API method calling DELETE /admin/users/{id}/full
- Add Reset trial, Reset subscription, Disable, Delete buttons to AdminUserDetail info tab
- Implement inline confirm pattern (click → "Are you sure?" → execute, 3s timeout)
- Delete now calls /full endpoint removing user from bot DB and Remnawave panel
- Remove UserActionsMenu dropdown, ConfirmationModal and related code from AdminUsers list
- Update delete confirmation text in ru/en locales to reflect full deletion
- Unified all inputs to use .input CSS class, consistent label styles, and btn-primary/btn-secondary buttons
- Fixed number inputs to use number | '' pattern allowing field clearing without default replacement
- Added field-level validation with inline error borders and messages
- Synced frontend constraints with backend Pydantic schemas (maxLength, min/max values)
- Fixed toggle switch dimensions across all admin forms
Fixed issue where users had to click the Telegram Mini App back button twice
in many admin pages. The replace flag was causing conflicts between Telegram's
BackButton internal history and React Router's history management.
Updated all 32 admin pages to use simple navigate() calls without replace flag.
Add replace: true to all navigate() calls in useBackButton hooks
Prevents history cycles when navigating back and forth
All 32 admin pages now use navigate(path, { replace: true })
Back button now goes directly to parent without cycling
Main sections navigate to /admin (15 pages)
Detail/edit pages navigate to parent lists (16 pages)
All pages use useBackButton hook with proper navigation paths
Native Telegram back button in Mini App, UI button on web
Email template pages keep UI-only back buttons as required