Commit Graph

815 Commits

Author SHA1 Message Date
c0mrade
581a2ffb52 fix(a11y): radiogroup semantics for AdminPromoOfferSend button groups
Wrap template-selection and send-mode button groups in role=radiogroup +
aria-labelledby; mark each option button as role=radio with aria-checked
(WCAG 4.1.2). Labels get matching id for the labelledby relationship.
2026-05-26 11:28:37 +03:00
c0mrade
9bb1c2339a refactor(theming): blue-* → accent-* in admin/internal usages
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.
2026-05-26 11:24:28 +03:00
c0mrade
3df2134cba refactor(theming): orange-* → warning-* for promo/highlight semantics
Sweep orange across SubscriptionPurchase/GiftSubscription/PromoOffersSection/
AdminTrafficUsage/AnalyticsTab/withdrawalUtils to warning-* tokens. Stars brand
yellow→orange gradient kept in TopUpAmount.
2026-05-26 11:22:19 +03:00
c0mrade
d7cbbe077a refactor(theming): yellow-* → warning-* for status/pending semantics
Sweep yellow color classes that signal pending/warning status (admin pending pills,
withdrawal status, risk indicators) → warning-* tokens. Telegram Stars / fortune wheel
yellow kept as brand (TopUpAmount + Wheel + FortuneWheel).
2026-05-26 11:21:16 +03:00
c0mrade
122f050a1b refactor(design): flatten accent gradient CTAs to solid bg per design law
Replace gradient CTAs that fade between same-token shades with flat backgrounds
(TopUpAmount accent submit, ChannelSubscriptionScreen single-channel button, three
SuccessNotificationModal action buttons). Stars yellow→orange gradient kept as a
Telegram brand cue; decorative card-header gradients left as visual identity.
2026-05-26 11:17:42 +03:00
c0mrade
feee9f9c05 fix(theming): replace off-token TelegramCallback/VerifyEmail with design tokens
Both pages were rendering with raw Tailwind gray-* + a non-existent border-primary-600
class (invisible spinner). Map: bg-gray-50 → bg-dark-950, text-gray-900 → text-dark-50,
text-gray-500 → text-dark-400, border-primary-600 → border-accent-500. Pages now match
the dark theme and the spinner is visible.
2026-05-26 11:14:58 +03:00
c0mrade
bd8a4fad6e refactor(theming): replace amber-* with warning-* tokens
Sweep all amber-N color classes to warning-N (uniformly pending/warning semantics).
Yellow and orange left for per-file review (mixed Stars brand + semantic uses).
2026-05-26 11:10:50 +03:00
c0mrade
cb7693e3ff refactor(theming): replace green-* / emerald-* with success-* tokens
Sweep: text/bg/border/ring/from/to/via/fill/stroke/shadow-{green,emerald}-N → success-N
(all were semantic success/active/paid states).
2026-05-26 11:09:28 +03:00
c0mrade
9c5e38b594 refactor(theming): replace red-* palette with semantic error-* tokens
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.
2026-05-26 11:08:22 +03:00
c0mrade
e1b48c1ccb fix(a11y): label associations for AdminTariffCreate fields
Add htmlFor/id pairs for name, description, daily price, traffic limit, device limit
and tier level (WCAG 1.3.1).
2026-05-26 10:48:46 +03:00
c0mrade
1ec6d7e979 fix(a11y): label associations for AdminPromoOfferTemplateEdit fields
Add htmlFor/id pairs for template name, message text, button text, valid hours,
discount percent, test duration, and active discount hours (WCAG 1.3.1). Test-squads
section is a checkbox group, not a single control.
2026-05-26 10:45:29 +03:00
c0mrade
d42fcfa5dd fix(a11y): label associations for AdminServerEdit fields
Add htmlFor/id pairs for display name, description, country code, price, max users
and sort order (WCAG 1.3.1). Original name is a read-only display, not a control.
2026-05-26 10:42:15 +03:00
c0mrade
5a8195f83f fix(a11y): label associations for AdminPromoGroupCreate + AdminPinnedMessageCreate
Add htmlFor/id pairs for the name + auto-assign inputs (AdminPromoGroupCreate) and the
content textarea (AdminPinnedMessageCreate). AdminPromoOfferSend skipped — its labels
sit above button-groups, not native controls (a separate role=group/radiogroup pass).
2026-05-26 10:39:22 +03:00
c0mrade
a635ca2316 fix(a11y): label associations for AdminPromocodeCreate fields
Add htmlFor/id pairs for code, type, balance bonus, days count, tariff, promo group,
discount percent, discount validity, max uses and valid-until inputs (WCAG 1.3.1).
2026-05-26 10:37:01 +03:00
c0mrade
6f66cb6759 fix(a11y): label associations for AdminCampaignEdit fields
Add htmlFor/id pairs for tariff selector, partner selector, name, start parameter,
subscription days/traffic/devices and tariff duration days inputs (WCAG 1.3.1).
2026-05-26 10:24:58 +03:00
c0mrade
4d18be848d fix(a11y): label associations for AdminCampaignCreate fields
Add htmlFor/id pairs for tariff selector, name, start parameter, trial subscription
days/traffic/devices and tariff duration days inputs (WCAG 1.3.1). Group labels (server
selector, bonus-type buttons) deferred — they need role=group, not htmlFor.
2026-05-26 10:20:56 +03:00
c0mrade
a4897243ba fix(a11y): label associations for AdminInfoPageEditor fields
Add htmlFor/id pairs for slug, icon, sort-order, replaces-tab and title inputs (WCAG 1.3.1).
2026-05-26 00:40:12 +03:00
c0mrade
fefc52fb20 fix(a11y): label associations for AdminNewsCreate fields
- htmlFor/id pairs for title, slug, read-time, excerpt and featured-image inputs
- add an ariaLabel prop to ColoredItemCombobox (applied to the trigger button) and
  pass the category/tag labels so the comboboxes have an accessible name (WCAG 1.3.1/4.1.2)
2026-05-26 00:20:30 +03:00
c0mrade
1b40c3856d fix(a11y): associate referral form labels with their inputs
Add htmlFor/id pairs across the partner-apply form (company, channel, website,
description, expected referrals, desired commission) and the withdrawal-request form
(amount, payment details) so labels are programmatically tied to their controls (WCAG 1.3.1).
2026-05-26 00:06:59 +03:00
c0mrade
7b0a72db5b fix(a11y): switch semantics for admin form toggles
Add role=switch + aria-checked + aria-label to 12 ad-hoc toggle buttons across admin
create/edit forms (AdminCampaignCreate/Edit, AdminPromocodeCreate, AdminPromoGroupCreate,
AdminPromoOfferTemplateEdit, AdminPaymentMethodEdit, AdminTariffCreate) so screen readers
announce on/off state (WCAG 4.1.2).
2026-05-26 00:03:17 +03:00
c0mrade
491f48a331 fix(a11y): switch semantics for user-flow toggles
Add role=switch + aria-checked + aria-label to the custom toggle buttons so screen
readers announce on/off state (WCAG 4.1.2): autopay (Subscription) and the custom-days
/ custom-traffic toggles (SubscriptionPurchase).
2026-05-25 23:57:36 +03:00
c0mrade
90888db48c fix(a11y): associate Support ticket form labels with their inputs
Add htmlFor/id pairs for the create-ticket subject and message fields so screen
readers announce the label and clicking the label focuses the control (WCAG 1.3.1).
2026-05-25 23:53:42 +03:00
c0mrade
0caba3dffb feat(a11y,i18n): more modal focus-traps + Telegram theme/language sync
Modal a11y:
- focus-trap + role=dialog/aria-modal/aria-labelledby on AdminRoles delete confirm
  and AdminBanSystem user-detail modal (+ close aria-label)

Telegram theme/language sync (first run only, explicit choice always wins):
- getTelegramColorScheme() / getTelegramLanguageCode() helpers in useTelegramSDK
- useTheme initial state follows the Telegram client color scheme when no stored theme
- applyTelegramLanguage() adopts the Telegram client language when no stored choice,
  called from main.tsx after SDK init (launch params unavailable before init)
2026-05-25 23:31:38 +03:00
c0mrade
8e0b63bac8 feat(a11y): cross-platform hardening + modal focus-trap from impeccable audit
Responsive / viewport:
- add .min-h-viewport / .h-viewport utilities (100dvh + Telegram --tg-viewport-stable-height)
- migrate min-h-screen / h-screen / 100vh across 18 files
- reveal hover-only controls on focus-within + touch (desktop nav, admin settings)
- grid breakpoints (TopUpAmount, Contests), larger touch targets, nav aria-labels

Platform routing (Telegram WebView correctness):
- clipboard -> copyToClipboard util with execCommand fallback (10 files)
- window.open -> openTelegramLink / openLink (Profile, Referral, ChannelSubscriptionScreen)
- window.confirm -> useNativeDialog (admin pages); PromoOffersSection -> useDestructiveConfirm
- window.prompt -> usePrompt / PromptDialogHost (TipTap link editors)
- ESLint no-restricted-properties guard against regressions (adapters/clipboard util exempt)

Modal accessibility:
- new useFocusTrap hook (focus trap, Esc, scroll lock, focus restore)
- role=dialog / aria-modal / focus-trap: Polls, SuccessNotificationModal, AdminPolicies,
  AdminPromoGroups, AdminCampaigns, BroadcastPreview (Telegram + Email)
- new global usePrompt store + PromptDialogHost
2026-05-25 23:16:07 +03:00
Fringg
e44a09312e fix(subscription): forward subscription_id to purchaseTariff to fix renew race
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).
2026-05-16 16:10:32 +03:00
Fringg
47f0359ed6 fix(subscription): invalidate devices query after revoke
On revoke, RemnaWave panel clears all device HWIDs along with the
subscription URL rotation, but the cabinet was only invalidating
['subscription'], ['connection-link'], ['subscriptions-list'] — leaving
['devices', subscriptionId] cached. UI showed the now-stale device list
until the user manually refreshed.

The GET /subscription/devices endpoint reads HWIDs live from RemnaWave
(no DB cache to clear), so invalidating the TanStack query is
sufficient — the immediate refetch returns the now-empty device list.
2026-05-16 04:34:32 +03:00
Fringg
e964780e6f refactor(devices): close all MED/LOW review items
- 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.
2026-05-16 04:07:25 +03:00
Fringg
d1e5ce873b fix(devices): unicode escape bug + onSuccess race + en locale + apiErr surface
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.
2026-05-16 03:53:49 +03:00
Fringg
321c65b68b feat(devices): inline rename UI for connected HWID devices
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
2026-05-16 03:02:45 +03:00
Fringg
f301d44f24 fix(admin): show flag for every country code, not just hardcoded 25-36
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).
2026-05-16 01:11:00 +03:00
Fringg
abbbc6a216 fix(onboarding): prevent invisible overlay blocking clicks and stuck tour
- Tie spotlight/tooltip pointer-events to isVisible so opacity:0 stops
  trapping taps during step transitions
- Render target click-catcher only when overlay is fully visible and
  clear stale targetRect on step change
- Retry target lookup up to 6 times and auto-skip step (or complete
  tour) when target is missing, instead of leaving overlay stuck
  invisible-but-interactive
- Persist completion to localStorage before unmounting the tour, so
  the flag survives even if unmount throws
2026-05-15 05:34:38 +03:00
Fringg
5493c25e5f chore(topup): harden direct-open guard and admin toggle defenses
Post-review nits:
* AdminPaymentMethodEdit.tsx: `config.open_url_direct ?? false` when seeding
  state — defends against stale backend rendering (e.g. cache invalidation
  race before migration applies)
* Same file: aria-label now uses the localized `admin.paymentMethods.openUrlDirect`
  translation key instead of hardcoded English
* TopUpAmount.tsx: case-insensitive guard for Telegram deep-link URLs —
  `https://t.me/`, `http://t.me/`, and `tg://` all match regardless of provider
  casing quirks. Lowercase normalization done once before the comparisons.
2026-05-13 11:13:33 +03:00
Fringg
aa8bfc9d08 feat(topup): direct-open payment page when method.open_url_direct is set
User asked for the gift-style seamless flow on balance top-up: provider
checkout opens inside Telegram MiniApp WebView without a click-to-open link
panel. Made it an admin per-method toggle so it can be enabled selectively.

src/pages/TopUpAmount.tsx — on topup mutation success:
* Move saveTopUpPendingInfo to BEFORE any redirect so /balance/top-up/result
  can still pick up the pending payment after the provider's return_url fires
* If method?.open_url_direct === true AND the URL is not a t.me/ deep link
  (Stars/CryptoBot), window.location.href = redirectUrl and return early
* Otherwise fall through to the existing setPaymentUrl panel — preserves
  current behavior for methods without the flag enabled

The t.me/ guard is important: window.location.href to a Telegram deep link
inside a MiniApp WebView is unreliable (native shell cant always intercept).
Those URLs continue to go through openTelegramLink / openInvoice in the panel
path. Stars never reaches topUpMutation.onSuccess anyway (handled by the
separate starsPaymentMutation); the guard is defense-in-depth for CryptoBot
and any future t.me-deep-link providers.

src/pages/AdminPaymentMethodEdit.tsx — added Open URL directly toggle with
the same slider styling as the is_enabled toggle. Defaults to off.

src/types/index.ts
* PaymentMethod.open_url_direct?: boolean (user-facing)
* PaymentMethodConfig.open_url_direct: boolean (admin shape)

Translations added for ru/en/zh/fa with a hint clarifying behavior and the
t.me/ exemption.
2026-05-13 10:50:31 +03:00
Fringg
2a342f6adc fix(topup): preserve canonical RUB amount to avoid FX round-trip shortfall
User report: a user in EN locale paid 1.65 USD for a 150₽ subscription, but the
bot received 14960 kopeks (149.60₽) — short by 40 kopeks, blocking the
subscription purchase.

Root cause: TopUpAmount.tsx displays the prefilled RUB amount converted via
`.toFixed(2)`. With an exchange rate like 90.66 RUB/USD, 150₽ → 1.6545 USD →
displayed "1.65" (rounded down by 0.0045 USD ≈ 0.4 RUB). When the user submits
without editing, `convertToRub("1.65")` runs again and returns 1.65 × 90.66 =
149.589₽ — less than the 150₽ the user is trying to pay. Math.round/Math.ceil
on this only handles sub-kopek IEEE-754 fractions, not the deeper FX display-
rounding direction.

Fix: when the user does not edit the prefilled amount (`amount === initialDisplayAmount`)
and `initialAmountRubles > 0` is known, bypass the FX round-trip and send
`Math.round(initialAmountRubles * 100)` directly. Math.ceil for non-RUB targets
when the user does type a custom amount still helps with sub-kopek fractions.

`?amount=150` from a renew CTA → "1.65 USD" displayed → user clicks pay → bot
receives 15000 kopeks instead of 14960. Subscription renews as expected.
2026-05-13 10:24:08 +03:00
Fringg
172850e13a fix(profile): use displayName helper for name field
Follow-up to displayName migration. Profile page still rendered
`{user?.first_name} {user?.last_name}` directly, leaving a trailing space when
last_name is null. displayName() handles the joining and fallback uniformly.
2026-05-13 09:20:48 +03:00
Fringg
4de01ccd1d fix(dashboard): show full name on welcome, fix gender mismatch on trial-expired card
User reported single-letter welcome "Добро пожаловать, О!" (short first_name
"Олег"-style users) and a confused "Истекла" feminine label on the masculine
"Пробный период истёк" trial-expired card.

* Add `src/utils/displayName.ts` helper that composes `first_name + last_name`
  with `username` and `#telegram_id` fallbacks. Single source of truth for
  user-facing name rendering across the app.
* Apply `displayName(user)` in Dashboard welcome, AppHeader mobile drawer,
  and DesktopSidebar profile chip. Now a user with `first_name="О"` and
  `last_name="Иванов"` sees "О Иванов" instead of just "О".
* `SubscriptionCardExpired` — context-aware Russian label: for trial
  subscriptions render masculine "Истёк" (agrees with "пробный период"),
  for paid subscriptions keep feminine "Истекла" (agrees with "подписка").
  Uses i18next `context: subscription.is_trial ? 'trial' : ''` — falls back
  to base key for EN/ZH/FA which are grammatically neutral.
* Add `expiredDate_trial: "Истёк"` only to `ru.json` (no changes needed for
  other locales — i18next context falls back to `expiredDate`).
2026-05-13 09:02:10 +03:00
Fringg
7c10843d9c fix(cabinet): show trial offer in multi-tariff mode
Two cabinet routes never showed TrialOfferCard for users without any
subscription when MULTI_TARIFF was enabled:

1. Dashboard (/)
   hasNoSubscription was derived from subscriptionResponse, but the
   /cabinet/subscription query is disabled in multi-tariff mode
   (`enabled: !isMultiTariff`). subscriptionResponse stayed undefined
   forever, so `has_subscription === false` was never true and the
   trial card never rendered.

   Fix: branch on isMultiTariff — use multiSubData.subscriptions.length
   when in multi-tariff, keep the single-tariff path unchanged.

2. /subscriptions list
   When the array came back empty the page rendered EmptyState directly,
   bypassing trial eligibility entirely. Users who navigated to
   'Подписки' from the menu before opening the dashboard saw the empty
   placeholder and nothing about the trial.

   Fix: fetch trial-info on empty state, render TrialOfferCard if
   trialInfo.is_available, fall back to EmptyState otherwise. Also
   wired the activate mutation locally (mirrors Dashboard) so the
   button works without bouncing through the home screen first.

Single-tariff behavior is unchanged. After the fix both entry points
agree: if you have no subscriptions and trial is available, you see
the offer regardless of which page you opened first.
2026-05-13 05:31:22 +03:00
Fringg
6ffd0ae824 fix(format): convert price by exchange rate on landings and gift page
formatPrice from utils/format.ts only swapped the currency symbol
(220 ₽ -> ¥220) without converting the underlying amount, because it
had no source of exchange rates and the landing/gift pages never called
the useCurrency hook that knew how to fetch them.

- Add a module-level rates cache in utils/format with setExchangeRates
  setter. formatPrice now converts kopeks/100 from RUB to the target
  currency via currencyApi.convertFromRub when rates are cached.
- useCurrency pushes its loaded rates into that cache so any subsequent
  formatPrice call benefits, including subcomponents that cannot easily
  receive a prop.
- Call useCurrency in QuickPurchase, GiftSubscription, and
  AdminLandingEditor — the only entry points whose subcomponents still
  use the synchronous formatPrice (everything else already routes
  through useCurrency.formatAmount).

For RU locale behavior is unchanged. For EN/ZH/FA the amount is now
divided by the per-currency rate and formatted via Intl.NumberFormat
with 2 fraction digits (0 for IRR since amounts are large).
2026-05-13 04:56:15 +03:00
Fringg
1fdafbd0a1 fix(subscription): hide strikethrough free label on device addon price
The per-device price line in the buy-devices modal showed a
strikethrough 'Бесплатно' for users with a promo-group discount,
because original_price_per_device_kopeks was missing in the API
response and formatPrice(0) renders 'Бесплатно'. Now the
strikethrough is rendered only when original_price_per_device_kopeks
is present and non-zero, mirroring the existing guard on the total
price block.
2026-05-10 09:59:57 +03:00
Fringg
60c835301d fix: move reissue button to standalone block outside device_limit guard
The reissue button was inside the Additional Options card which is gated
by device_limit !== 0, so it was hidden when device_limit was 0. Move it
to its own card block with independent visibility condition.
2026-05-04 17:41:21 +03:00
Fringg
f7cc445127 feat: add subscription reissue button with cooldown timer
- Add revokeSubscription API method
- Add reissue button in Additional Options with amber styling,
  destructive confirm dialog, 15-min cooldown with countdown timer,
  localStorage persistence across page reloads
- Add i18n keys in all 4 locales (en, ru, zh, fa)
2026-05-04 17:22:05 +03:00
Fringg
d21c6637bf feat(admin/broadcasts): add preview buttons for Telegram + Email broadcasts
Add BroadcastPreview component with local rendering of Telegram messages
(HTML tokenizer with tag whitelist, no dangerouslySetInnerHTML) and Email
preview (sandboxed iframe). Preview buttons appear in broadcast create page
section headers, disabled when content is empty.

Based on PR #416 by @smediainfo.
2026-05-04 06:18:12 +03:00
Fringg
afffab17d3 feat: bulk delete subscription protection for active paid subs
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.
2026-04-29 11:48:59 +03:00
Fringg
ae55a18fc9 feat: dedicated RBAC permissions for bulk actions, info pages, news 2026-04-29 11:14:38 +03:00
Fringg
020f4c95e2 feat: landing analytics goals, daily bar chart, referrer tracking, contact persistence
- 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>
2026-04-29 09:17:52 +03:00
Fringg
80059681da feat: Yandex Metrika CID tracking, offline conversions UI, sticky pay button
- 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
2026-04-29 08:59:19 +03:00
Fringg
6d3010b621 feat: multi-media attachments, linkify URLs, shared MessageMediaGrid
- 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
2026-04-29 08:45:15 +03:00
Fringg
8044b664c3 fix: restore cabinet_last_login in user detail (now shows real data) 2026-04-29 06:46:20 +03:00
Fringg
8fcdbbe53e fix: remove stale cabinet_last_login field from user detail 2026-04-29 06:42:25 +03:00
Fringg
bc37f31350 feat: subscription selector in VPN connection block for multi-tariff
- 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
2026-04-29 06:34:41 +03:00