The center streak doesn't appear in screen recordings, which is the signature of a GPU-compositing artifact — CSS filters on SVG elements mis-render when the wheel group is promoted to a layer during the spin. Replace the two SVG CSS filters with compositing-safe equivalents: LED-glow blur -> radial gradient fill, emoji drop-shadow -> SVG-native feDropShadow. (HTML background blur left as-is; it's not on the SVG and isn't a light.)
Confirmed from the user's spin video: the sparkles bunched into the middle during a spin because they were placed at 16-40% from centre (some right by the hub). Push them all to a 34-43% radius (golden-angle distributed) so they ring the outer wheel and the centre stays clear — no sparkles crossing or near the centre.
The real cause of the center line: SPARKLE_POSITIONS (yellow dots shown only while spinning) used `20 + (i*10)%60` / `15 + (i*13)%70`. For the first ~6 sparkles the modulo never wraps, so top/left increment linearly and the dots land on a straight diagonal through the centre — the 'line of lights' during the spin. Scatter them on a golden-angle spiral inside the wheel instead.
Reverts the earlier LED-glow change (it wasn't the cause): the rim-light blur is restored to the original.
The rim lights are static cx/cy SVG circles; their glow used a CSS filter:blur(2px). During the spin the wheel's transform transition promotes the SVG to a GPU layer, and Chrome mis-renders CSS filters on geometry-positioned SVG elements — the blurs streak toward the view-box origin, drawing a line of lights through the centre. Replace the blur with a soft radial-gradient fill (no filter, so the artefact can't occur).
Also drive both the chase speed and the per-dot delay from one --led-dur var so the chase stays in order at the faster spin speed instead of scrambling (the delay was a fixed 6s value that broke once the duration dropped to 2s).
Windows ships no glyphs for regional-indicator flag emoji, so flags fell back to country letters everywhere react-twemoji wasn't manually wrapped. Bundle the Twemoji country-flags woff2 locally (no runtime CDN — matters for our audience) and add it as an @font-face scoped with unicode-range to flag codepoints only, then put it first in every Tailwind font stack (sans/display/mono). The browser now uses it solely for flag characters, so every flag renders correctly app-wide with zero markup changes and no effect on any other text.
On sm+ the regular news cards sit in a 2-col grid; an odd count left an empty cell. Move the featured card out of the grid (it was col-span-full anyway, which also skewed nth-child parity) and stretch the last regular card to span both columns when it's the lone one in its row — same orphan-fill used for the admin stat cards.
Jumping only happened on mobile (the wrap reshuffled when speeds changed width); desktop fits on one line and never jumped. So keep the 3 fixed rows for mobile (sm:hidden) and restore the original single flex-wrap row for desktop (hidden sm:flex). On mobile, download/upload now sit tight in a flex row (gap-4) instead of a full-width 2-col grid that looked too spread out.
The metrics sat in one flex-wrap, so when realtime speeds change width (Kb/s<->Mb/s, large values) elements reflowed and jumped between lines, and the card height shifted. Split into 3 fixed rows: processor (CPU load + RAM), traffic (down/up), versions (remnanode + xray). Traffic uses a 2-column grid so a wide value never pushes its sibling; tabular-nums keeps digit widths constant.
The panel marks each node version with its product logo. Add an XrayIcon (the Xray core brand pinwheel, custom SVG like RemnawaveIcon) and put RemnawaveIcon before the remnanode version + XrayIcon before the xray version, matching the original node rows. Drop the redundant 'xray' word since the logo conveys it.
Checked the live panel — every node metric carries an icon there, but our card showed RAM% and load average as bare text while the speeds had icons. Add MemoryIcon before RAM% and CpuIcon before the load average so the metrics row matches the original.
Give the inbound/outbound tag min-w-0 flex-1 so a long tag shrinks and ellipsizes instead of pushing the byte counters off-screen on narrow viewports; tighten the counter gap.
Drop the separate Traffic tab and fold its per-node inbound/outbound breakdown into the Nodes tab: click a node card to expand its traffic breakdown, click again to collapse (chevron indicator; action buttons stop propagation). Realtime totals (down/up/Σ) move to a compact line above the node list. Realtime stats now load on the Nodes tab.
The 2-column grid inside the max-w-2xl container made each gift card a narrow ~330px half. Gift cards are content-rich (header, code, share preview, buttons), so stack them single-column full-width — they now stretch to the container width on every breakpoint.
Each Remnawave user status now gets a distinct icon instead of the same people glyph: ACTIVE -> check-circle, DISABLED -> prohibit, LIMITED -> gauge, EXPIRED -> clock (people icon as fallback for unknown statuses).
The panel's provider.faviconLink is the provider's site URL (e.g. https://waicore.com/), not an image, so rendering it directly as <img src> failed and onError hid it. Derive the real favicon image from the URL's hostname via Google's favicon service, like the panel does.
Replace the user-facing 'RemnaWave' spelling with 'Remnawave' across all locale values (en/ru/zh/fa, 67 strings), t() display fallbacks, and code comments. Translation keys (refreshRemnaWave, ...) and code identifiers (RemnaWaveService, setRemnaWaveUuid, ...) are left untouched.
- Render the provider favicon in the node badge (new provider_favicon / provider_name fields), with graceful fallback.
- Mobile-friendly node card: stop the versions block from shifting right on wrapped rows, truncate long addresses, cap the provider badge.
- Give node-summary cards (total/online/offline/disabled) and traffic-period cards (2d/7d/30d/month/year) distinct, meaningful icons instead of all-the-same globe / bar-chart.
- Show traffic as 'used / limit' (e.g. 2.35 TB / ∞).
- Route every icon through the central barrel (drop the direct react-icons/pi import).
- Build the share deep-link as t.me/<bot>?start=GIFT_<code> (literal underscore). The old GIFT%5F encoding was passed to the bot verbatim by Telegram, so its startswith('GIFT_') never matched and activation silently failed.
- Source the bot username from the runtime branding config (env var is only a fallback), so the 'activate via bot' line no longer vanishes on builds without VITE_TELEGRAM_BOT_USERNAME.
- Widen the page on desktop (max-w-2xl) and show sent gifts in a 2-col grid; it was cramped to a mobile column.
- Replace the remaining hand-written SVG icons with the central barrel.
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.).
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.
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
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.
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.
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.
- 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
The expired-card quick-renew button showed a lightning bolt, inconsistent with
the sparkle used on every other subscription CTA. Match the subscription CTA.
- 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)
- 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
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)
На 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.
Новый пользователь с доступным триалом видел только TrialOfferCard (CTA
активации), без пути в витрину тарифов — приходилось сперва активировать
пробную. Добавлена вторичная кнопка «Посмотреть тарифы и купить подписку»
→ /subscription/purchase под карточкой триала на обеих страницах, где она
рендерится: Subscriptions и Dashboard (вход по умолчанию).
Telegram-баг #605056/#605063.
В превью конструктора рассылки кнопка реферала отображалась сырым ключом
«referrals»: presetLabels имел ключ «partners», а бот отдаёт «referrals»
(каталог BROADCAST_BUTTONS), и presetLabels[id] || id фолбэчил на сам id.
Ключ переименован partners → referrals (метка «Партнёрка» сохранена).
Реальная отправка не затронута — бот строит клавиатуру из своего каталога.
Telegram-баг #602989.
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.
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.
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.
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.
.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.
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%).
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).
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).