Commit Graph

2044 Commits

Author SHA1 Message Date
c0mrade
872f73157f feat(icons): add node/gift glyphs to the central barrel
Add Key, Inbox, Export, WarningCircle, Heartbeat, CalendarBlank, CalendarStar, ChartPie, ChartDonut, Cpu, Memory, Pulse as Phosphor Regular wrappers so feature pages stop hand-writing SVGs or importing react-icons/pi directly.
2026-06-01 13:08:06 +03:00
c0mrade
9aae399ce8 fix(cabinet-icons): correct two mismatched glyphs from the svg migration
A personal glyph-by-glyph re-review of the migration (the "steering wheel"
guard) found two wrong matches:
- MaintenanceScreen: a wrench/maintenance svg had been mapped to AdjustmentsIcon
  (sliders) → use a proper WrenchIcon (PiWrench).
- InstallationGuide tutorial button: an open-book svg had been mapped to
  DocumentIcon (file) → use BookOpenIcon (PiBookOpen).

Add WrenchIcon and BookOpenIcon to the central barrel. All other replacements
were verified correct (paper-plane→Send, server→HardDrives, warning-triangle→
Warning, etc.).
2026-05-31 23:43:32 +03:00
c0mrade
b5088c70a1 refactor(cabinet): migrate inline SVG icons to the central react-icons set
Audit-driven sweep: replaced 168 hand-written inline <svg> icons across 77
files with the central Phosphor (react-icons/pi) components from
@/components/icons, preserving each icon's size classes and colour (dynamic
stroke colours via the parent's currentColor, RefreshIcon's spinning state,
conditional rotate-180 chevrons).

Verified: tsc + vite build + eslint clean; an adversarial diff review of all
changed files found the replacements correct (70 files clean, 0 blockers).
Remaining inline <svg> dropped from 262 to ~95 — the survivors are legitimate
non-icons (brand/provider logos, loading spinners & framer-motion animations,
charts/sparklines, background decoration) plus a few ambiguous glyphs.
2026-05-31 23:37:12 +03:00
c0mrade
08f12daa40 feat(admin-remnawave): panel-style node rows with live metrics
Redesign the Nodes tab to match the Remnawave panel's node list:
- per-node row: status dot, online users, flag + name, provider badge, IP,
  traffic used with a progress bar (∞ when unlimited), uptime
- live metrics strip: RAM %, load average (1/5/15m), realtime ↓/↑ throughput
  and the node + xray versions, sourced from system.stats (memory, loadAvg,
  interface rx/txBytesPerSec) already returned by /api/nodes/
- provider name comes from the realtime stats (joined by node uuid), which is
  now loaded on the Nodes tab too
- poll the nodes endpoint every 5s so the live metrics stay fresh, like the panel
- formatSpeed mirrors the panel's bits-with-1024-step formatting
2026-05-31 22:53:33 +03:00
c0mrade
8613cb20aa fix(subscription): use the nav sparkle icon on the renew CTA button
PurchaseCTAButton (the "Продлить подписку" CTA) rendered a single inline
sparkle; swap it for the SubscriptionIcon component so it matches the two-star
sparkle in the navigation. Colour is inherited from the badge container.
2026-05-31 19:43:54 +03:00
c0mrade
5340f81a95 fix(subscription): use the nav SubscriptionIcon on the quick-renew button
The inline single sparkle didn't match the two-star sparkle shown in the nav.
Render the same SubscriptionIcon component so the button matches the nav exactly.
2026-05-31 19:38:57 +03:00
c0mrade
493de23a1d fix(admin-remnawave): scope the mobile orphan-fill to small screens only
The `nth-child(odd)` orphan rule outweighed the desktop reset (higher
specificity), so a lone trailing stat card was stretched full-width on desktop
too. Use the `max-lg`/`max-sm` variants so the fill applies only below the
multi-column breakpoint; desktop keeps the normal grid.
2026-05-31 19:38:56 +03:00
c0mrade
ff3dbfaa25 feat(admin-remnawave): auto-refresh overview, responsive mobile grids, wide sub-requests
- Auto-refresh the recap/devices/top-consumers/subscription-requests widgets
  on a 60s timer (system + health already refreshed) so the page updates itself
- Mobile: a lone trailing stat card now spans the full row (no empty gap when
  the 2-column grid reflows)
- Subscription requests: full-width card with a two-column breakdown and a total
  count, instead of a half-empty narrow card
2026-05-31 19:24:44 +03:00
c0mrade
e18db02e42 fix(subscription): use the sparkle icon on the quick-renew button
The expired-card quick-renew button showed a lightning bolt, inconsistent with
the sparkle used on every other subscription CTA. Match the subscription CTA.
2026-05-31 19:24:43 +03:00
c0mrade
0a89838926 style(cabinet-icons): use regular Phosphor weight and switch wheel to dice
- Drop the Duotone variants (their translucent secondary fill looked murky on
  the dark theme) in favour of the clean regular weight across all icons
- WheelIcon: PiSteeringWheel → PiDiceFive (a steering wheel made no sense for
  the fortune-wheel feature)
2026-05-31 19:24:29 +03:00
c0mrade
b8be98939a feat(admin-remnawave): enrich panel stats UI and fix nodes-online display
- Show nodes as "online / total" (fixes "56 online" when only 5 nodes exist)
- Add recap, devices (+top users by device count), top-consumers, panel
  health and subscription-request widgets, with profile-specific Phosphor
  metric icons (CPU/RAM/uptime/devices/event-loop)
- Add the matching adminRemnawave API types and methods
2026-05-31 18:51:12 +03:00
c0mrade
d0e0b6b7e3 feat(cabinet): migrate all icons to the panel's Phosphor set
Replace the cabinet's hand-written heroicons-style SVG icon components with
the Remnawave panel's own icon family — Phosphor via react-icons/pi (Duotone).
All icons now live in a single central barrel (src/components/icons:
index + extended-icons + editor-icons, 144 icons); every feature file imports
from it instead of redefining inline SVGs (~440 icon defs removed).

- Add react-icons dependency
- Desktop nav (AppShell) now uses the central Phosphor icons, removing a
  stroke-vs-duotone inconsistency the partial migration introduced
- Icons with custom props (SortIcon's direction, expandable chevrons) kept as
  thin Phosphor wrappers; the RemnawaveIcon brand logo and animated SVGs
  (checkmarks, spinners) are intentionally left as-is
- Restore the original per-call-site icon sizes wherever the centralized
  default differed (211 call sites)
2026-05-31 18:50:50 +03:00
c0mrade
a62d689fd3 fix(navigation): надёжная кнопка «Назад» через тип навигации, без петли (#436)
На iOS Telegram кнопка «Назад» на /subscriptions/:id казалась мёртвой:
fallback вёл на /subscriptions, который авто-редиректит обратно на деталь
(single-tariff, 1 подписка) — молчаливая петля. Причина — ненадёжный
window.history.state.idx, который мутируют собственные redirect'ы.

idx заменён на надёжный счётчик глубины по useNavigationType (PUSH +1,
POP −1, REPLACE без изменений, дедуп по location.key). Кнопка корректно
показывается/прячется, navigate(-1) берётся только при реальной истории.
Плюс fail-closed fallback: с детали уходим в '/' если список не доказанно
безопасен (multi-tariff или >1 подписки) — петля невозможна.

GitHub-issue #436.
2026-05-31 15:52:09 +03:00
c0mrade
2d5982d82b fix(subscriptions): дать новым юзерам путь в витрину без активации триала
Новый пользователь с доступным триалом видел только TrialOfferCard (CTA
активации), без пути в витрину тарифов — приходилось сперва активировать
пробную. Добавлена вторичная кнопка «Посмотреть тарифы и купить подписку»
→ /subscription/purchase под карточкой триала на обеих страницах, где она
рендерится: Subscriptions и Dashboard (вход по умолчанию).

Telegram-баг #605056/#605063.
2026-05-31 15:52:08 +03:00
c0mrade
c24ae0039c fix(broadcasts): показывать «Партнёрка» вместо сырого ключа referrals в превью
В превью конструктора рассылки кнопка реферала отображалась сырым ключом
«referrals»: presetLabels имел ключ «partners», а бот отдаёт «referrals»
(каталог BROADCAST_BUTTONS), и presetLabels[id] || id фолбэчил на сам id.
Ключ переименован partners → referrals (метка «Партнёрка» сохранена).
Реальная отправка не затронута — бот строит клавиатуру из своего каталога.

Telegram-баг #602989.
2026-05-31 15:52:07 +03:00
c0mrade
4ac91529ba Merge remote-tracking branch 'origin/main' into dev 2026-05-29 11:40:31 +03:00
c0mrade
0ed8bb1d48 fix(navigation): single-tariff /subscriptions/:id deep-link shows Close, not Back
Refines 3c2f650 — that commit broke the loop by suppressing the single-tariff
redirect on idx=0, but left the BackButton visible. The user still saw Back
on the detail page and needed two taps (Back → Close) to exit. Серёжа's
report was specifically that Back should not appear at all when you land on
your only subscription via a bot button.

Move the decision up to TelegramBackButton:

  - Subscribe to the existing ['subscriptions-list'] query (deduped by
    React Query, so no extra fetch).
  - On /subscriptions/:id, when multi_tariff_enabled is false and there is
    no in-app history (idx=0 deep-link entry), hide the BackButton. Telegram
    then surfaces its native Close (X) in the header.
  - Multi-tariff users still see BackButton on /subscriptions/:id deep-links
    because their /subscriptions list is meaningful (they have many subs).
    The 2bcba3b fallback-to-parent behaviour remains intact for unrelated
    deep-links like /balance/top-up, /admin/*, etc.

Revert the defensive guard in Subscriptions.tsx — the loop can no longer be
triggered because no BackButton is shown to trigger it.

Behaviour summary:

  flow                                    BackButton  exit cost
  --------------------------------------  ----------  ---------
  deep-link → /subscriptions/:id (1T)     hidden      Close, 1 tap
  / → /subscriptions → /subscriptions/:id shown       Back → /, 1 tap
  deep-link → /balance/top-up             shown       Back → /balance ✓
  deep-link → /subscriptions/:id (multi)  shown       Back → /subscriptions list

Reported by Серёжа, bugs topic #599679.
2026-05-29 11:36:22 +03:00
c0mrade
3c2f650c28 fix(navigation): break BackButton loop on single-tariff /subscriptions/:id deep-link
User flow that hung:
  1. Bot deep-link opens MiniApp directly on /subscriptions/:id (idx=0).
  2. Telegram shows BackButton.
  3. Tap → TelegramBackButton handler sees no in-app history and falls
     back to getFallbackParentPath('/subscriptions/:id') = '/subscriptions'.
  4. Subscriptions page renders, sees single-tariff + 1 subscription,
     <Navigate replace> redirects right back to /subscriptions/:id.
  5. BackButton looks dead — taps do nothing visible.

Fix: only redirect when there is in-app history. When idx=0 (deep-link or
just-arrived-via-back), render the list page instead. The user then sees
their single subscription as a card and can tap into it, and the next
BackButton tap exits the MiniApp as expected.

Reported by Серёжа in bugs topic #599679.
2026-05-29 11:22:49 +03:00
c0mrade
ca6e081d06 Merge pull request #443 from BEDOLAGA-DEV/release-please--branches--main--components--cabinet-frontend
chore(main): release 1.54.1
2026-05-28 18:58:05 +03:00
github-actions[bot]
dc807bce12 chore(main): release 1.54.1 2026-05-28 15:57:34 +00:00
c0mrade
4082f0841c Merge pull request #442 from BEDOLAGA-DEV/dev
hotfix: переводы интерфейса (после v1.54.0)
2026-05-28 18:56:55 +03:00
c0mrade
e1f401c333 fix(i18n): backfill 120 missing keys + close 2 typo regressions from purchase refactor
Surface: users (and especially Серёжа в bugs topic) were seeing raw i18n keys
like `subscription.promoGroup.title` rendered as literal text on the renewal
screen. Audit found 118 keys referenced in code that were absent from all 4
locale files, plus 140 keys missing only in zh/fa (silently falling back to
Russian for those users).

Root causes:
- cf52999 (extract TariffPickerGrid) introduced `subscription.promoGroup.title`
  that never existed — should have been `subscription.promoGroup.yourGroup`.
- Subscription.tsx used `subscription.autopay.title` (object path) while
  SubscriptionListCard.tsx used `subscription.autopay` (leaf) — i18next can't
  hold both shapes for the same key.
- Many user-flow keys (subscription.notFound, subscriptions.empty, common.ok,
  successNotification.*, etc.) were never added when their components landed.
- Historical drift had ~140 keys in ru/en that never got zh/fa translations.

Fix:
- Rename TariffPickerGrid usage `promoGroup.title` → `promoGroup.yourGroup`.
- Rename Subscription.tsx usage `autopay.title` → `autopay`.
- Add 116 brand-new keys across ru/en/zh/fa (464 entries).
- Add 8 partially-missing keys (en+ru gaps) for landing periods, dashboard
  campaign/referrer stats, and promo offer failure counts.
- Translate 280 entries to close the zh/fa drift.

Final state: 0 keys missing in any of the 4 locale files for any code-referenced
translation key. Build + tsc green.
2026-05-28 18:47:58 +03:00
c0mrade
cfc563c5ed Merge pull request #441 from BEDOLAGA-DEV/release-please--branches--main--components--cabinet-frontend
chore(main): release 1.54.0
2026-05-28 17:04:25 +03:00
github-actions[bot]
a69e598e5a chore(main): release 1.54.0 2026-05-28 14:03:07 +00:00
c0mrade
9b8d89cc10 Merge pull request #440 from BEDOLAGA-DEV/dev
dev → main: следующий релиз
2026-05-28 16:59:37 +03:00
c0mrade
574da67ca0 feat(yandex-conv): forward CID through remaining paid API methods
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.
2026-05-28 16:43:22 +03:00
c0mrade
1b82280853 feat(yandex-conv): forward CID through subscription purchase requests
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.
2026-05-28 15:51:20 +03:00
c0mrade
ed0b46e833 fix(prose): keep TipTap list markers inline with their content
.prose ul/ol used list-inside, but TipTap wraps each <li>'s content
in <p>. Combining position:inside with a block-level <p> dropped the
content onto the next line under the marker — visible both inside
the editor and on the rendered article page.

Drop list-inside (default to outside) and add `.prose li > p` rules:
first paragraph renders inline with the marker, subsequent paragraphs
stay block to preserve multi-paragraph list items.

Closes Bedolaga bug-topic #597116.
2026-05-28 15:51:10 +03:00
c0mrade
34fd8132c2 Merge pull request #438 from BEDOLAGA-DEV/chore/impeccable-audit-fixes
chore(impeccable): full audit + critique + 4 god-page decompositions
2026-05-27 10:11:55 +03:00
c0mrade
0aaf58c8b8 refactor(admin-traffic): extract 5 filter components
Third and final step in the AdminTrafficUsage decomp. Move the
filter dropdowns + PeriodSelector into
src/components/admin/trafficUsage/filters/:

- PeriodSelector.tsx (103 lines): fixed period tabs (1/3/7/14/30
  days) + custom date-range mode. Exports the PERIODS constant
  so the parent's prefetch effect iterates the same canonical
  list.

- TariffFilter.tsx (133 lines): pop-over multi-select for tariff
  names.

- StatusFilter.tsx (147 lines): pop-over multi-select for
  subscription statuses. Exports STATUS_COLORS alongside.

- NodeFilter.tsx (135 lines): pop-over multi-select for traffic
  nodes (with flag emoji + traffic-share numbers).

- CountryFilter.tsx (132 lines): pop-over multi-select for
  source countries.

All five are pure controlled components — parent owns every
Set<string> filter state and feeds them via {available, selected,
onChange}. The parent imports them and drops 9 now-unused icon
imports (FilterIcon, ChevronDownIcon, ServerIcon, CalendarIcon,
StatusIcon, GlobeIcon — they live inside the filter files now).

AdminTrafficUsage.tsx: 1593 → 977 lines (-616).
Cumulative across the 3-step admin-traffic decomp:
1904 → 977 (-927, -49%).
2026-05-27 09:45:29 +03:00
c0mrade
c94bd3d593 refactor(admin-traffic): extract TrafficIcons barrel
Second step in the AdminTrafficUsage decomp. Move the 15 inline SVG
icons (Search/Chevron/Refresh/Download/Sort/Filter/Server/Calendar/X/
Status/Globe/Shield/ServerSmall) into a single barrel at
src/components/admin/trafficUsage/TrafficIcons.tsx. Pure SVG, no
behavioural change.

AdminTrafficUsage.tsx: 1734 → 1593 lines (-141).
2026-05-27 09:40:01 +03:00
c0mrade
4c2af82d91 refactor(admin-traffic): extract helpers + RiskBadge
First step in the AdminTrafficUsage decomp. Two new files under
src/components/admin/trafficUsage/:

- trafficUsageHelpers.ts (166 lines): the TanStack Table module
  augmentation (sticky/align/bold meta), formatters (formatBytes,
  formatCurrency, formatShortDate, formatGbPerDay,
  toBackendSortField, getFlagEmoji wrapper), risk-assessment helpers
  (bytesToGbPerDay, getRatio, getRowBgColor, getNodeTextColor,
  getRiskLevel, getCompositeRisk), and the RISK_STYLES constant.

- RiskBadge.tsx (36 lines): the small per-row risk indicator
  (dot + GB/d value + mini progress bar). Pure presentational.

Parent imports everything from the new modules; the inline
'TanStack Table module augmentation' lives in the helpers file
so the declaration merge still picks up across the codebase
once the helpers are imported.

AdminTrafficUsage.tsx: 1904 → 1734 lines (-170).
2026-05-27 09:38:18 +03:00
c0mrade
c11bfaf012 refactor(bulk-actions): extract SubscriptionSubRow + StatusBadge
Fourth and final step in the AdminBulkActions decomp.

SubscriptionSubRow: the expanded per-user secondary <tr> shown when
the operator drills into multi-subscription users (tariff, status,
days remaining, traffic bar, devices). Pure presentational.

StatusBadge: the pill used both inside the sub-row and the primary
user-table column (subscription_status). Co-located here because
both consumers want the same visual contract.

AdminBulkActions.tsx: 1367 → 1182 lines (-185).
Cumulative across the 4-step admin decomp: 2539 → 1182 (-1357, -53%).
2026-05-27 09:35:37 +03:00
c0mrade
a8df415931 fix(bulk-actions): drop unused ChevronDownIcon import
After MultiSelectDropdown extraction the parent no longer references
ChevronDownIcon directly — the icon now lives only inside the moved
sub-component. Trailing import cleanup from the previous commit.
2026-05-27 09:34:07 +03:00
c0mrade
8d25e5201a refactor(bulk-actions): extract MultiSelectDropdown
Third step in the AdminBulkActions decomp. Move the 154-line
pop-over multi-select used by the tariff / status / node filters
into src/components/admin/bulkActions/MultiSelectDropdown.tsx.

Pure controlled primitive: closes on outside click, provides
select-all / deselect-all helpers, uses the shared ChevronDownIcon
from DropdownSelect.tsx. Exports MultiSelectOption type alongside.

Parent drops its inline ChevronDownIcon import (now only referenced
by the moved sibling) and imports MultiSelectDropdown + the option
type from the new module.

AdminBulkActions.tsx: 1517 → 1367 lines (-150).
2026-05-27 09:32:54 +03:00
c0mrade
f398d9e95c refactor(bulk-actions): extract FloatingActionBar + actionTargets
Second step in the AdminBulkActions decomp. Two new files:

- actionTargets.ts (27 lines): SUBSCRIPTION_LEVEL_ACTIONS set +
  isSubscriptionLevelAction(). The classification is the source of
  truth shared between FloatingActionBar (renders two grouped menus
  when multi-tariff is on) and the parent page (gates selection-count
  semantics + the active-paid count in delete-subscription).

- FloatingActionBar.tsx (304 lines): the bottom-dock selection
  toolbar. Pure presentational — parent owns selection state and
  supplies onAction (opens the modal with the picked action type)
  + onToggleAllSubscriptions. The full 11-item action config (icons
  + label keys + colour classes) lives inside the file as a private
  ActionConfig array.

Parent now imports FloatingActionBar + isSubscriptionLevelAction
from the new modules; the inline ActionConfig interface is gone.

AdminBulkActions.tsx: 1819 → 1517 lines (-302).
Cumulative across the 2-step admin decomp: 2539 → 1517 (-1022, -40%).
2026-05-27 09:31:10 +03:00
c0mrade
217d60ab19 refactor(bulk-actions): extract ActionModal + DropdownSelect
First and largest step in the AdminBulkActions decomp. Two new
files under src/components/admin/bulkActions/:

- DropdownSelect.tsx (47 lines): the small native-<select> wrapper
  used both by the filter row and the ActionModal's tariff/promo
  picker. Exports DropdownSelect + the shared ChevronDownIcon +
  DropdownOption type.

- ActionModal.tsx (712 lines): the per-action configuration +
  execution modal. Wraps three internal views — ProgressView (live
  SSE log + counters while a job streams), ErrorDetails (collapsible
  per-row error list after completion), and the per-action form
  (days / tariff / traffic / balance / promo-group / grant /
  set-devices / delete-* confirmations). Exports the ModalState
  and ProgressState types alongside.

Self-contained: parent feeds modal state + reference data (tariffs,
promo groups, users for active-paid counting) and receives
onExecute(params) when the operator confirms. No queries or
mutations here — execution flow stays in the parent's
handleExecuteAction.

Parent keeps a local CheckIcon (still used by MultiSelectDropdown
checkboxes + the user-table column defs) and re-imports
ChevronDownIcon / DropdownOption from the new file.

AdminBulkActions.tsx: 2539 → 1796 lines (-743).
2026-05-27 09:28:39 +03:00
c0mrade
72c325f8cc refactor(purchase): extract ClassicPurchaseWizard from god page
The largest and last structural extraction in the SubscriptionPurchase
decomp. Moves the classic-mode purchase flow (period → traffic →
servers → devices → confirm) into a self-contained step wizard at
src/components/subscription/purchase/ClassicPurchaseWizard.tsx.

The wizard self-owns:
- the preview query (gated on confirm step + form open)
- the submitPurchase mutation
- all six pieces of wizard state (currentStep, selectedPeriod,
  selectedTraffic, selectedServers, selectedDevices, showPurchaseForm)
- the steps memo + currentSelection memo + currentStepIndex/isFirst/isLast
- the init useEffect (seeds from classicOptions.selection)
- toggleServer / goToNextStep / goToPrevStep / resetPurchase / getStepLabel
- its own useCloseOnSuccessNotification handler (closes form + resets
  step on global success notification) — replaces the parent's
  handleCloseAllModals dependency for the classic flow

Parent shrinks to a pure orchestrator: route params, three top-level
queries (subscription, purchase-options, subscriptions-list), mode
detection, two pieces of tariffs-mode state, the success-notification
handler for the remaining tariff modals, and JSX that delegates to
the four extracted sub-components.

SubscriptionPurchase.tsx: 908 → 370 lines (-538).
Cumulative across the 5-step decomp: 2054 → 370 (-1684, -82%).
Drops 11 parent imports (useEffect, useMemo, useCallback, useMutation,
useQueryClient, useNavigate, PurchaseSelection, PeriodOption,
InsufficientBalancePrompt, CheckIcon, Twemoji, useCurrency,
usePromoDiscount, getErrorMessage, PurchaseStep).
2026-05-27 00:02:31 +03:00
c0mrade
cf5299973a refactor(purchase): extract TariffPickerGrid from god page
Move the tariff selection surface (promo-group banner, all-purchased
empty state, and the 1/2-col tariff cards grid with per-tariff CTAs)
into src/components/subscription/purchase/TariffPickerGrid.tsx.

The grid is pure presentation — it owns no queries or mutations.
Two callbacks back into the parent:
- onSelectTariff(tariff)  → parent sets selectedTariff + showForm
- onSwitchTariff(tariffId) → parent sets switchTariffId

Internal helpers (formatPrice via useCurrency, applyPromoDiscount
via usePromoDiscount, glass tokens via useTheme + getGlassColors,
navigate for the 'all purchased' back-to-list button) all derive
inside the component — nothing threaded as props beyond data.

SubscriptionPurchase.tsx: 1202 → 908 lines (-294).
2026-05-26 23:55:23 +03:00
c0mrade
a89ab96953 refactor(purchase): extract TariffPurchaseForm from god page
The single biggest extraction in the SubscriptionPurchase decomp.
Moves the per-tariff purchase form into a self-contained component
at src/components/subscription/purchase/TariffPurchaseForm.tsx.

The form self-owns:
- the purchaseTariff mutation (mode-aware: daily-tariff activate vs
  period-based + custom-days / custom-traffic toggles + summary)
- the auto-scroll-into-view ref + effect on mount
- selectedTariffPeriod / customDays / customTrafficGb / useCustomDays /
  useCustomTraffic — five pieces of state that were parent-level but
  only ever read inside this form. Form remounts with fresh state
  when the parent passes a new tariff (key={selectedTariff.id}).

Parent only owns selectedTariff + showTariffPurchase (the two pieces
the orchestrator needs to decide what to render) and threads them
into <TariffPurchaseForm>. Tariff card click handlers drop their
period-seeding line (the form seeds itself from tariff.periods[0]
on mount). The subscription_expired fallback callback simplifies
for the same reason.

SubscriptionPurchase.tsx: 1838 → 1222 lines (-616). Drops useRef,
TariffPeriod, and getInsufficientBalanceError from parent imports.
2026-05-26 23:52:42 +03:00
c0mrade
349a9ca421 refactor(purchase): extract SwitchTariffSheet from god page
Move the tariff-switch preview modal (137 lines of inline JSX
+ its previewTariffSwitch query + switchTariff mutation + the
auto-scroll-into-view useEffect + the ref) into a self-contained
sheet at src/components/subscription/sheets/SwitchTariffSheet.tsx.

Parent keeps the trigger (switchTariffId selection state lives on
the TariffPickerGrid card click) but no longer holds the query, the
mutation, the ref, or the side-effect. The subscription_expired
backend signal — which used to call three setters at parent level
(setSelectedTariff, setSelectedTariffPeriod, setShowTariffPurchase)
— now flows through a single onExpiredFallback(tariff) callback.

SubscriptionPurchase.tsx: 2010 → 1841 lines (-169).
2026-05-26 23:44:31 +03:00
c0mrade
f3eb3ea603 refactor(purchase): extract usePromoDiscount hook
Prerequisite for the SubscriptionPurchase god-page decomposition.
The applyPromoDiscount helper + its active-discount useQuery were
inlined in SubscriptionPurchase.tsx and called from 9 sites across
3 purchase flows (classic wizard, tariff picker, tariff form,
switch-tariff sheet). Lifting both into src/hooks/usePromoDiscount
gives every future extracted sub-component a single seam to call,
without threading the function through props or re-fetching the
discount in each child.

SubscriptionPurchase.tsx: 2054 → 2009 lines (-45).
2026-05-26 23:40:15 +03:00
c0mrade
5784efd7a3 fix(chrome): replace 2 platform emoji + 1 en-dash with on-brand tokens
PRODUCT.md anti-references call out the SaaS-template / emoji-in-chrome
register: the reference products (Linear, Stripe, Things 3) don't put
platform emoji in product chrome. Three small drift spots:

- Balance saved-cards link used a 💳 emoji as the leading icon.
  Replaced with a new CreditCardIcon added to the barrel (Heroicons-
  style stroke, matches the chevron / chat / wallet siblings).

- Subscription paused-info banner used a ⏸️ emoji. Replaced with a
  new PauseIcon added to the barrel. Same 24×24 stroke, currentColor,
  preserves the urgent-400 tint via inline style.

- Balance payment-method min/max range used a literal en-dash ('–')
  in source. PRODUCT.md's anti-em-dash rule covers the en-dash by
  the same logic. Switched to a translated 'to' connector
  (t('common.rangeTo', 'to')) so other locales can translate freely.
2026-05-26 23:33:21 +03:00
c0mrade
68a97b896b fix(nav): keep Support pinned in mobile bottom-nav
Previously the bottom nav swapped Support out for Wheel whenever the
operator enabled the wheel feature flag. This left a frustrated
paying customer (a key persona — already mid-ticket, opening the
app to follow up) unable to find help from the primary nav: they had
to know to dig into the hamburger drawer.

Wheel is a brand-moment surface, Support is a critical-path surface.
Replacing the second with the first is the wrong IA trade.

New rule: Support is the floor of the bottom nav and never moves.
Slot 4 still carries an optional item, with Wheel winning over
Referral when both are enabled (operator opted into Wheel as a
deliberate brand moment; Referral users can use the hamburger).

Critique P3 issue.
2026-05-26 23:27:44 +03:00
c0mrade
75f775064c fix(decoration): strip 4 accent / status-hue leaks across hero surfaces
DESIGN.md 'Tunable-but-Scarce Rule' caps accent at <=10% of any
screen and reserves the accent + status hues for action / status,
never decoration. Four sites were spending those tokens on pure
chrome:

- Subscription.tsx purchased-traffic bar: zone.mainHex linear-gradient
  on a non-status progress bar; the per-purchase bar inherited the
  user's GLOBAL traffic-zone color (a fresh purchase could read
  'critical' just because the user's overall usage was). Replaced
  with solid accent-500 fill — same affordance, honest semantics.

- SubscriptionCardActive tariff badge: zone-colored linear-gradient
  background + zone-colored label. The tariff name has no traffic-zone
  semantics, so tinting it by the global traffic zone was a
  Status-Hue Lockout violation. Switched to glass innerBg/innerBorder.

- Balance hero card: bg-gradient-to-br from-accent-500/10 + glow prop
  on Card. Removed; flat surface, the giant numeric is the affordance.
  Eliminates the SaaS hero-metric template tell.

- Login background: two stacked fixed inset gradients (linear +
  accent radial halo) read as the airdrop / crypto aesthetic
  PRODUCT.md explicitly anti-references. Replaced with the plain
  body bg-dark-950.
2026-05-26 23:26:20 +03:00
c0mrade
312a34f715 fix(subscription): clear critique P0 + P1 + part of P2
Three issues from the post-PRODUCT.md /impeccable critique pass:

P0 — backdrop-blur-xl shipped to mobile on the two hero cards
(SubscriptionCardActive + Subscription detail). DESIGN.md
'Mobile-Without-Blur Rule' restricts backdrop-blur to ≥1024px.
Gate with lg: prefix so phones (half the audience) get the opaque
surface and skip the scroll-jank-inducing GPU blur layer.

P1 — Two destructive device-deletion sites still called the bare
browser confirm(). The codebase already imports useDestructiveConfirm
and uses it correctly for revoke (line 484). Bring the all-devices
and per-device delete paths onto the same platform-aware path, so
inside Telegram the user gets the native destructive popup with
haptic + theme, and on web they get the inline destructive panel.

P2 (partial) — Removed the decorative ambient radial-gradient halo
behind the Subscription detail hero card and the trial-shimmer border
overlay. Same chrome was distilled out of SubscriptionCardActive
earlier in this branch; the rationale is identical (zone/accent hue
leaking into pure decoration violates DESIGN.md Tunable-but-Scarce
+ Status-Hue Lockout rules). Trial state is conveyed by the header
badge.
2026-05-26 23:23:47 +03:00
c0mrade
c77d1e948e chore(gitignore): ignore impeccable PRODUCT.md / DESIGN.md / DESIGN.json
Keep impeccable context files local-only. The skill loader reads them
from disk; they are not artefacts the team needs in the shared repo.
2026-05-26 23:10:44 +03:00
c0mrade
13b64ab4e7 refactor(subscription): use ChevronRightIcon from icons barrel
The five extracted sheet components each had a hand-inlined chevron
SVG copy-pasted from the original god page. Swap them for the
existing ChevronRightIcon from src/components/icons — same visual,
one source of truth (Heroicons-aligned path), and pre-baked sizing
through the cn() className merge.

Note on the broader 'icon unification' audit recommendation: this
project does not depend on lucide-react. It already ships a 703-line
hand-rolled icon barrel at src/components/icons/index.tsx. The real
cleanup is migrating the ~139 inline SVGs across the app to that
barrel, not adding a new icon library. This commit consolidates the
five duplicated chevrons introduced by the recent sheet extraction;
broader inline-SVG migration is a separate, larger refactor.
2026-05-26 22:28:41 +03:00
c0mrade
bf3c114a72 refactor(subscription): extract DeleteSubscriptionSheet from god page
Move the delete-subscription expand/confirm block into
src/components/subscription/sheets/DeleteSubscriptionSheet.tsx.

The sheet itself decides between the Telegram destructive native dialog
and the inline web confirm panel, so the parent no longer needs the
platform discriminator. It owns deleteLoading and the API call; the
parent passes onDeleted (navigate + invalidate) and the textSecondary
glass color token used in the warning copy.

Subscription.tsx: 1828 → 1750 lines.
Drops now-unused imports (usePlatform; deleteLoading state).
2026-05-26 22:24:20 +03:00
c0mrade
d48d053e6e refactor(subscription): extract ServerManagementSheet from god page
Move the classic-mode manage-servers sheet (~280 lines of inline
JSX, the countries useQuery, its seeding useEffect, and the update
mutation) into src/components/subscription/sheets/ServerManagementSheet.tsx.

Parent keeps selectedServersToUpdate so the global 'close all modals'
callback can still clear it; the sheet owns the rest. The query stays
gated on trial state inside the sheet.

Subscription.tsx: 2117 → 1828 lines (−289).
2026-05-26 22:21:12 +03:00