Switch labelColor to attribute-based mode so hovered/highlighted nodes
get dark text (#111827) readable on the white hover background, while
normal labels stay light (#e5e7eb) readable on the dark canvas.
Campaign users no longer get a separate amber fill color. Their campaign
association is already visible through the edge connecting them to the
campaign node. Users from campaigns now show their subscription status
color (or regular gray if no subscription), reducing visual noise.
- campaignUser: #4dd9c0 (teal) → #fbbf24 (amber) — no longer blends with trial blue
- regular: #6b7280 (gray) → #94a3b8 (slate) — visible on dark background
- Campaign edges updated to amber tint
- defaultNodeColor now references NODE_COLORS.regular instead of hardcoded value
- Legend gradient uses actual campaign palette colors
The createNodeBorderProgram was missing the second border entry
with { size: { fill: true }, color: { attribute: 'color' } }
which renders the node's fill color. Without it, node bodies
were transparent and only edges were visible.
When subscription_status is not available (backend not deployed or user
has no subscription), fill color now falls through to referral role
(partner/topReferrer/activeReferrer) before defaulting to campaign/regular.
Increases border size from 0.25 to 0.35 and merges legend sections.
Node fill color now encodes subscription status (paid/trial active/expired),
while border color indicates referral role (partner/top/active referrer).
Added BFS-based radial initial positioning to spread referral branches outward.
Updated legend with two sections and adjusted FA2 gravity for less clustering.
Add subscription status colors: green (paid active), blue (trial active),
red (paid expired), orange (trial expired). Extract NODE_COLORS constant
as single source of truth. Add SubscriptionStatus union type. Show status
badge in user detail panel. Add i18n for all 4 locales.
- Replace full graph with scoped view (campaign/partner/user)
- ScopeSelector component with tabs and searchable dropdown
- Store resets all state on scope change (selection, hover, filters)
- Remove dead NetworkSearch component and graphInstance globals
- i18n keys for scope selector in all 4 locales
Portal to document.body escapes main's contain:content which broke
fixed positioning (container had height:0 and was clipped to main).
Visual fixes for 662+ node graphs:
- labels only on large nodes (threshold 6→14, density 0.5→0.12)
- leaf nodes 5px, referrers 10px, campaigns 16px
- edge opacity reduced to prevent white starburst
- default edge color opacity lowered
- fix top offset breakpoint: sm:top-14 → lg:top-14 to match AppShell
header visibility (mobile header is lg:hidden with h-16)
- add gravity to FA2 layout to prevent disconnected nodes from flying
to extreme positions, making auto-rescale shrink everything to dots
- clamp outlier node positions after FA2 finishes (3σ threshold)
- add ResizeObserver to sync sigma canvas with container size changes
- call sigma.resize() before camera reset after FA2
- increase node sizes: user min 8→12, max 40→50, campaign 18→24
- reduce FA2 duration 5s→3s and slowDown 5→2 for tighter layout
- Switch to CSS Grid layout for reliable content area height
- Use inferSettings() for ForceAtlas2 auto-tuning based on graph structure
- Enable linLogMode and adjustSizes for better node clustering
- Fit camera after layout settles with animatedReset
- Increase node sizes for better visibility (MIN 8, MAX 40)
- Center controls at bottom, fix overlay positions
- Page uses top-16/sm:top-14 to start below AppShell header (z-50)
instead of inset-0 which renders behind the header
- Sigma init polls with RAF until container has real dimensions
instead of allowInvalidContainer which creates broken canvas
- Removed overflow-hidden from content area (not needed)
Container has no computed height when useEffect fires immediately
after data arrives. Defer to next animation frame so browser layout
completes. Also add allowInvalidContainer as safety net.
- Increase FA2 scalingRatio to 10 and decrease gravity to 0.5 for
better node spacing (less cluttered graph)
- Use 100dvh instead of 100vh for Telegram Mini App viewport compat
- Hide legend on mobile to prevent overlay collision
- Move controls to bottom-right on mobile, bottom-center on desktop
- Make stats single-column on mobile for compact layout