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;
|