diff --git a/src/Pages/DynamicFeeManagement.razor b/src/Pages/DynamicFeeManagement.razor index a0b4bcde..e961299c 100644 --- a/src/Pages/DynamicFeeManagement.razor +++ b/src/Pages/DynamicFeeManagement.razor @@ -178,6 +178,7 @@ Channel Capacity Local / Remote + Category Dynamic fee @@ -203,6 +204,20 @@ @("—") } + + @if (ch.RoutingState is { } rs) + { + @CategoryLabel(rs.PeerFlowCategory) + @if (rs.PendingCategory is { } pending) + { + → @CategoryLabel(pending) + } + } + else + { + + } + @* Pending channels (ChanId 0) can't be fee-managed yet, so they're not toggleable. *@ _channels = new(); private List _nodeChannels = new(); private IReadOnlyDictionary? _live; + private Dictionary _routingStates = new(); private List _peers = new(); private readonly HashSet _expanded = new(); private string _search = string.Empty; @@ -273,6 +290,8 @@ var allChannels = await ChannelRepository.GetAllManagedByUserNodes(LoggedUser.Id); _channels = allChannels.Where(c => c.Status == Channel.ChannelStatus.Open).ToList(); + await LoadRoutingStatesAsync(); + // Live balances are best-effort — structural stats and the toggles still work without LND. _live = null; try @@ -336,6 +355,8 @@ ChannelState? live = null; _live?.TryGetValue(channel.ChanId, out live); + _routingStates.TryGetValue(channel.Id, out var routingState); + overview.Channels.Add(new PeerChannelRow { Channel = channel, @@ -346,6 +367,7 @@ RemoteBalanceSats = live?.RemoteBalance, Active = live?.Active, BelowMinSize = channel.SatsAmount < Constants.ROUTING_ENGINE_FEE_MIN_CHANNEL_SIZE_SATS, + RoutingState = routingState, }); } @@ -368,14 +390,32 @@ Rebuild(); } - private void OnNodeChanged() + private async Task OnNodeChanged() { _selectedNode = _managedNodes.FirstOrDefault(n => n.Id == _selectedNodeId); _expanded.Clear(); _search = string.Empty; + await LoadRoutingStatesAsync(); Rebuild(); } + // Routing state is per managed node, so it's re-read whenever the selected node changes. + // Best-effort: categories just render as "—" when it can't be loaded. + private async Task LoadRoutingStatesAsync() + { + _routingStates = new(); + if (_selectedNode is null) return; + try + { + var states = await ChannelRoutingStateRepository.GetByManagedNodePubKey(_selectedNode.PubKey); + _routingStates = states.ToDictionary(s => s.ChannelId); + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Could not load routing state for node {PubKey}", _selectedNode.PubKey); + } + } + private IEnumerable FilteredPeers() { if (string.IsNullOrWhiteSpace(_search)) return _peers; @@ -517,6 +557,22 @@ return (updated, failed); } + private static string CategoryLabel(PeerFlowCategory category) => category switch + { + PeerFlowCategory.Sink => "Sink", + PeerFlowCategory.Source => "Source", + PeerFlowCategory.Bidirectional => "Bidirectional", + _ => "Uncategorized", + }; + + private static string CategoryClass(PeerFlowCategory category) => category switch + { + PeerFlowCategory.Sink => "cat-sink", + PeerFlowCategory.Source => "cat-source", + PeerFlowCategory.Bidirectional => "cat-bidi", + _ => "cat-uncat", + }; + private static string Btc(long sats) => (sats / 100_000_000m).ToString("0.####"); private static string Sats(long sats) => sats.ToString("N0"); private static string F(double d) => d.ToString("0.##", System.Globalization.CultureInfo.InvariantCulture); @@ -556,5 +612,6 @@ public long? RemoteBalanceSats { get; init; } public bool? Active { get; init; } public bool BelowMinSize { get; init; } // below the engine's minimum size, so it won't be fee-managed + public ChannelRoutingState? RoutingState { get; init; } // null until the engine has evaluated the channel } } diff --git a/src/Pages/DynamicFeeManagement.razor.css b/src/Pages/DynamicFeeManagement.razor.css index 1250f863..bcc69a39 100644 --- a/src/Pages/DynamicFeeManagement.razor.css +++ b/src/Pages/DynamicFeeManagement.razor.css @@ -170,6 +170,30 @@ table.peers-tbl tbody tr.peer-row:hover { background: #faf9fe; } .df-none { background: #eeedf4; color: var(--muted); } .df-partial { background: rgba(221,164,72,.16); color: #a9791f; } +/* flow category */ +.cat-badge { + font-family: var(--mono); + font-size: .68rem; + font-weight: 700; + letter-spacing: .02em; + padding: 3px 9px; + border-radius: 20px; + white-space: nowrap; +} + +.cat-sink { background: rgba(221,164,72,.16); color: #a9791f; } +.cat-source { background: rgba(59,119,226,.14); color: #2a5cb8; } +.cat-bidi { background: rgba(151,51,239,.13); color: #7b23c9; } +.cat-uncat { background: #eeedf4; color: var(--muted); } +.cat-none { background: transparent; color: var(--faint); } + +.cat-pending { + font-family: var(--mono); + font-size: .62rem; + color: var(--faint); + margin-left: 5px; +} + .gate-warn { color: #a9791f; font-size: .72rem;