Critique flagged the card as carrying '12 chrome elements for 2 facts'.
Removing 8 of 12 without browser QA risks gutting functional info.
Apply the conservative 3-element strip: only kill chromes with zero
information value.
• Background radial glow (200x200 circle, top-right, decorative tint
based on traffic zone) — pure ornament, no signal.
• Trial shimmer border (animate-trial-glow on inset wrapper) — trial
state is already signalled by the badge in the header; the second
signal was visual noise.
• Trial badge own animate-trial-glow + 2-stop gradient bg — flatten
to single bg-accent-400/10 + border-accent-400/25 + solid text.
The badge still says TRIAL with brand color; it no longer pulses.
• Zone dot 8px-radius color-tinted boxShadow + 'transition: all' —
drop the glow, transition only background. Reduces filter+composite
work, dot still signals zone via color.
Kept (functional):
• Big percentage + traffic-used metadata (primary info, not chrome)
• Progress bar (primary signal)
• Connect Device CTA with HoverBorderGradient (primary action;
rotating border is its own UX debate, deferred)
• Sparkline + connected devices count (functional metadata)
Full 12→4 collapse the critique recommended needs browser QA and a
design-direction call (move days/tariff/refresh to the detail page
header). Bigger restructure as separate PR.
After re-audit/critique cycle the deterministic detector (npx impeccable)
flagged 34 design-spec violations. Knock out 33 of them.
• 25 × pure-black-white: sed-sweep bg-black/X → bg-dark-950/X across
18+ files (modal scrims, photo viewer backdrop, code blocks). The
base resolves to rgba(10,15,26,X) — visually identical to true
black, satisfies the 'no #000' impeccable rule.
• 3 × bounce-easing: SuccessNotificationModal celebration icon and
SyncTab loading arrows used animate-bounce; replaced with
animate-pulse. Bounce easing reads dated; pulse conveys 'in
progress' without the cartoon feel.
• 3 × border-accent-on-rounded: TelegramCallback + VerifyEmail
spinners used 'border-b-2 border-accent-500' on rounded-full —
detector reads it as a side-stripe even though it's a ring loader.
Switch to canonical 'border-2 border-accent-500 border-t-transparent'
(3/4 ring colored). Same visual, no spec violation.
• 1 × ai-color-palette: AdminLandingStats had text-purple-400 on a
gift-stats heading; purple is not in the brand palette. Swap to
text-accent-400.
• 1 × layout-transition: TrafficProgressBar.tsx fill bar still used
transition: width 1.2s (slipped past the earlier optimize pass).
Convert to transform: scaleX with origin-left. Same gradient, same
duration, runs on the compositor.
Remaining: 1 finding in third-party Aceternity background-beams-collision
component (indigo-500 gradient on decorative WebGL background) — left
as-is, it's lifted decorative third-party code.
Detector: 34 → 1.
The #FFB800 (23 hits) and #FF3B5C (11 hits) raw hex literals across
Subscription / SubscriptionPurchase / TrialOfferCard / useTrafficZone
all represent the same semantic: 'expiring soon' and 'expired'.
Distinct from warning-500 (#f59e0b) and error-500 (#ef4444) — the
subscription timeline needs a sharper, hotter signal than the
system-wide warning/error roles.
Add two semantic tokens:
--color-urgent-400: 255, 184, 0 (#FFB800)
--color-critical-500: 255, 59, 92 (#FF3B5C)
Exposed via Tailwind as urgent.400 / critical.500. Sweep all
single-and double-quoted hex literals to rgb(var(--color-...))
form so style attrs, SVG stroke=, and ternary string values all
resolve through CSS vars (auto-respects future light-theme
variants).
Left as-is: 4 hex appearances inside gradient pair strings —
linear-gradient(135deg, #FFB800, #FF8C00) — where the partner
color (#FF8C00 / #FF6B35) isn't in the token set. Partial-replace
inside gradient strings would just add noise; full extraction of
the orange-ramp companion is out of scope for this pass.
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).
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`).
- Dashboard card: change date label from "Истекла" to "Активна до"
for limited subscriptions (traffic exhausted but time remaining)
- Dashboard button: change text from "Трафик исчерпан" (dead-end) to
"Докупить трафик" (actionable) for limited status
- Subscription page: CountdownTimer shows remaining time for limited
subs instead of "Истекла" (was passing is_active=false)
- PurchaseCTAButton: don't treat limited as expired — limited subs
still have time, should show extend CTA not "buy new subscription"
SubscriptionCardActive: passes ?sub= to /connection navigation.
Connection: QR state includes subscriptionId for back-navigation.
ConnectionQR: preserves subscription context on back button.
Dashboard + Subscription: localStorage traffic cache keyed per-subscription
to prevent cooldown bleed between different subscriptions.
- 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
- Кнопка «Подключить устройство» заблокирована при достижении лимита
- Haptic feedback при нажатии на заблокированную кнопку
- Предупреждение «Отключите устройства для подключения новых»
- Убран дублирующийся текст трафика под прогресс-баром (безлимит)
- 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
PendingGiftCard was linking to /buy/success/:token which uses the landing
API (exact token match). Since gift tokens are now truncated to 12 chars
in API responses, this caused 404s. Route to /gift?tab=activate&code=
which uses the gift API with prefix-based lookup.
- Expired daily tariffs now call purchaseTariff(tariff_id, 1) for 1-day purchase
- Balance check uses daily_price_kopeks for daily tariffs instead of 100 kopeks minimum
- Disabled daily tariffs still use togglePause for resume
- Handle expired status in GiftResult (stop polling + show FailedState)
- Add PollErrorState for balance mode poll errors (softer UX)
- Remove non-null assertions in handleSubmit (explicit narrowing)
- Wrap gift routes in ErrorBoundary
- Add pollErrorTitle/pollErrorDesc i18n keys to all 4 locales
- Show SubscriptionCardExpired for disabled daily subscriptions
- Use togglePause() API for resume instead of renewSubscription()
- Add "Suspended" status badge and balance query invalidation
- Add dashboard.suspended and subscription.pause.suspended translations (ru, en, zh, fa)
- QuickPurchase: публичная страница покупки с выбором тарифа/периода/оплаты
- PurchaseSuccess: статус покупки с поллингом и копированием ссылки
- AdminLandings: управление лендингами (создание, сортировка, удаление)
- AdminLandingEditor: полнофункциональный редактор лендинга
- API-клиент landings.ts для всех эндпоинтов
- Роутинг /buy/:slug и /buy/success/:token
- Локализация ru/en/zh/fa для всех текстов лендинга
- Санитизация HTML/CSS, rate limit защита, DOMPurify
Normal traffic zone now uses configurable accent palette instead of
hardcoded success/green. Added useTrafficZone hook that resolves
mainHex dynamically from theme colors. Replaced emerald/teal classes
with success palette in SuccessNotificationModal. Fixed hardcoded
green hex values in Subscription and SubscriptionPurchase pages.
- Increase card opacity and shadow contrast in glassTheme for light mode
- Add accent-tinted borders and shadows to subscription cards (active, expired, trial)
- Fix grid patterns to use dark lines instead of white in light mode
- Fix trial CTA button: solid green with dark text instead of invisible transparent
- Fix trial icon background: light accent gradient instead of hardcoded dark
- Add + prefix to referral earnings in StatsGrid
- Remove red "Пробный период" badge from expired trial card
- Remove "Пробный период завершен" subtitle text
- Simplify header layout (no justify-between needed without badge)
- Change "Тарифы" button link to /subscription/purchase
Move ~1900 lines of purchase/renewal logic from Subscription.tsx into
a new SubscriptionPurchase.tsx page at /subscription/purchase.
- Create SubscriptionPurchase.tsx with tariffs grid, classic mode wizard,
switch preview modal, and promo handling
- Create PurchaseCTAButton.tsx with animated gradient border, state-aware
colors and context-aware text
- Create subscriptionHelpers.ts with shared utilities
- Add CopyIcon to shared icons module
- Register /subscription/purchase route with lazy loading
- Update SubscriptionCardExpired and PromoOffersSection navigators
- Add CTA translation keys to ru.json and en.json
- Remove all scrollToExtend references
- SubscriptionCardExpired: use trialTitle for expired trials, remove traffic/devices grid, show only expiry date
- Subscription page: remove duplicate trial badge from zone indicator, fix expired trial badge showing "Пробный период" instead of "Истекла"
- Subscription page: replace static days counter with live HH:MM:SS countdown timer, merge "Действует до" into countdown block
- Subscription page: remove duplicate traffic percentage block
- Handle expired state in countdown (show "Истекла" in red instead of 00:00:00)
- Use live countdown.days for urgency threshold instead of stale server value
- Add Subscription tab to desktop header navigation (was only in mobile)
- Fix device dots overflow for large limits (>10) by using progress bar
- Show available referral balance on dashboard instead of total earnings
- Add available_balance and withdrawn fields to ReferralInfo type
- Add glassTheme utility with theme-aware color tokens
- Replace hardcoded rgba(255,255,255,...) with dynamic values
- Replace text-white with text-dark-50 for auto theme remapping
- Restyle subscription page block to match dashboard glassmorphic design
- Light mode: white semi-opaque cards with subtle shadows
- Dark mode: unchanged visual appearance
- Rewrite TrafficProgressBar with multi-segment gradient fill, flex-based
zone tints, shimmer + highlight overlays, radial glow at fill edge
- Rewrite SubscriptionCardActive with zone header, big percentage on right,
connect device card with device dots, tariff/days-left two-column row,
sparkline placeholder, traffic refresh controls
- Rewrite SubscriptionCardExpired with red glow, grid pattern, gradient
renew button, outline tariffs button
- Rewrite TrialOfferCard with animated glow background, grid pattern,
icon glow animation, price tag with old price, gradient CTA buttons
- Rewrite StatsGrid with 2x2 card layout, icon+label+chevron header,
big value numbers, zone-colored balance/earnings cards
- Update Sparkline with color prop and last-point dot indicator
- Update useAnimatedNumber to use easeOutExpo matching prototype
- Add formatTraffic utility for MB/GB/TB unit formatting
- Add mainHex to trafficZone for inline style support
- Update animations to match prototype timing
- Add new i18n keys for redesigned components
SubscriptionCardActive with animated zone dot, traffic bar, connect button.
SubscriptionCardExpired with red theme and renewal actions.
TrialOfferCard with stats grid and free/paid activation.
StatsGrid 2x2 with font-display values and font-mono labels.