From d68df945f2664affe79c03332e2530c76118c71f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20A=2EP?= <53834183+Jossec101@users.noreply.github.com> Date: Thu, 7 May 2026 14:27:06 +0200 Subject: [PATCH] Add Rebalance feature with UI components and service integration stack-info: PR: https://github.com/Elenpay/NodeGuard/pull/509, branch: Jossec101/stack/15 --- src/Pages/Rebalances.razor | 327 ++++++++++++++++ src/Program.cs | 10 +- src/Shared/NavMenu.razor | 6 + src/Shared/NewRebalanceModal.razor | 597 +++++++++++++++++++++++++++++ 4 files changed, 936 insertions(+), 4 deletions(-) create mode 100644 src/Pages/Rebalances.razor create mode 100644 src/Shared/NewRebalanceModal.razor diff --git a/src/Pages/Rebalances.razor b/src/Pages/Rebalances.razor new file mode 100644 index 00000000..427df370 --- /dev/null +++ b/src/Pages/Rebalances.razor @@ -0,0 +1,327 @@ +@page "/rebalances" +@using Humanizer +@using Blazorise +@using Blazorise.Components +@using NodeGuard.Data.Models +@attribute [Authorize(Roles = "Superadmin,NodeManager,FinanceManager")] +

Rebalances

+ + + + + Status + + + + + + Source Node + + + + + + Requestor + + + + + + Is Manual + + + + + + From Date + + + + + + To Date + + + + + + + + + + + + + + + + + + + + + + @(context.Node?.Name ?? "Unknown") + + + + + @(context.SourceChanIdLnd?.ToString() ?? "Any") + + + + + @{ + var sourcePeer = context.SourceChannel == null ? null + : context.SourceChannel.SourceNode?.PubKey == context.SourceNodePubKey + ? context.SourceChannel.DestinationNode + : context.SourceChannel.SourceNode; + } + @if (sourcePeer != null) + { + + @(sourcePeer.Name ?? StringHelper.TruncateTail(sourcePeer.PubKey, 10)) + + } + else + { + @("—") + } + + + + + @if (!string.IsNullOrEmpty(context.TargetPubkey)) + { + + @StringHelper.TruncateTail(context.TargetPubkey, 10) + + } + else + { + @("Any") + } + + + + + @($"{context.RequestedAmount:f8} BTC") + + + + + @($"{context.Amount:f8} BTC ({Math.Round(PriceConversionService.BtcToUsdConversion(context.Amount, _btcPrice), 2)} USD)") + + + + + + + @($"{context.MaxFeePct:0.####}%") + + + + + + @if (context.FeePaidSats.HasValue) + { + + @context.FeePaid.ToUnit(NBitcoin.MoneyUnit.BTC).ToString("f8") BTC + + } + else + { + @("-") + } + + + + + @context.Status.Humanize() + + + + + + + @context.CreationDatetime.ToString("yyyy-MM-dd HH:mm:ss") + + + + + + + @context.UpdateDatetime.ToString("yyyy-MM-dd HH:mm:ss") + + + + + +
No records were found.
+
+ +
+ +
+
+
+
+
+ + + +@inject IPriceConversionService PriceConversionService +@inject INodeRepository NodeRepository +@inject IRebalanceRepository RebalanceRepository +@inject IApplicationUserRepository ApplicationUserRepository +@inject IToastService ToastService +@code { + [CascadingParameter] private ApplicationUser? LoggedUser { get; set; } + + private DataGrid _dataGrid = new(); + private List _rebalances = new(); + private List _availableNodes = new(); + private List _availableUsers = new(); + private decimal _btcPrice; + public required NewRebalanceModal _newRebalanceModal; + private int _totalItems; + private int _filtersResetKey; + + private RebalanceStatus? _statusFilter; + private int? _nodeFilter; + private string? _userFilter; + private bool? _isManualFilter; + private DateTime? _fromDate; + private DateTime? _toDate; + + private List _statusOptions = new() { null }; + private List _isManualOptions = new() + { + new BoolOption { Label = "All", Value = null }, + new BoolOption { Label = "Yes", Value = true }, + new BoolOption { Label = "No", Value = false }, + }; + + public class BoolOption + { + public string Label { get; set; } = ""; + public bool? Value { get; set; } + } + + protected override async Task OnInitializedAsync() + { + if (LoggedUser == null) return; + _btcPrice = await PriceConversionService.GetBtcToUsdPrice(); + if (_btcPrice == 0) + { + ToastService.ShowError("Bitcoin price in USD could not be retrieved."); + } + _availableNodes = await NodeRepository.GetAllManagedByUser(LoggedUser.Id); + _availableUsers = await ApplicationUserRepository.GetAll(); + _statusOptions.AddRange(Enum.GetValues().Cast()); + } + + private async Task OnReadData(DataGridReadDataEventArgs e) + { + if (e.CancellationToken.IsCancellationRequested) return; + + var fromDate = _fromDate.HasValue + ? new DateTimeOffset(DateTime.SpecifyKind(_fromDate.Value.Date, DateTimeKind.Local)).ToUniversalTime() + : (DateTimeOffset?)null; + var toDate = _toDate.HasValue + ? new DateTimeOffset(DateTime.SpecifyKind(_toDate.Value.Date.AddDays(1).AddTicks(-1), DateTimeKind.Local)).ToUniversalTime() + : (DateTimeOffset?)null; + + var (rows, total) = await RebalanceRepository.GetPaginatedAsync( + e.Page, e.PageSize, + status: _statusFilter, + nodeId: _nodeFilter, + userId: _userFilter, + isManual: _isManualFilter, + fromDate: fromDate, + toDate: toDate); + + _rebalances = rows; + _totalItems = total; + } + + private async Task OnFiltersChanged() => await _dataGrid.Reload(); + + private async Task ClearAllFilters() + { + _statusFilter = null; + _nodeFilter = null; + _userFilter = null; + _isManualFilter = null; + _fromDate = null; + _toDate = null; + _filtersResetKey++; + await _dataGrid.Reload(); + } + + private async Task NewRebalance() + { + _newRebalanceModal.Clear(); + StateHasChanged(); + await _newRebalanceModal.Show(); + } + + private async Task OnRebalanceCreated() + { + await _dataGrid.Reload(); + } +} diff --git a/src/Program.cs b/src/Program.cs index 0f079640..75756f61 100644 --- a/src/Program.cs +++ b/src/Program.cs @@ -121,6 +121,7 @@ public static async Task Main(string[] args) builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); + builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); @@ -144,6 +145,7 @@ public static async Task Main(string[] args) builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); + builder.Services.AddTransient(); builder.Services.AddScoped(); //DbContext @@ -191,7 +193,7 @@ public static async Task Main(string[] args) o.Protocols = HttpProtocols.Http1); }); - + // Npgsql JSON mapping is opt-in per https://www.npgsql.org/doc/release-notes/8.0.html#breaking-changes NpgsqlConnection.GlobalTypeMapper.EnableDynamicJson(); @@ -256,7 +258,8 @@ public static async Task Main(string[] args) opts.ForJob(nameof(AutoLiquidityManagementJob)) .WithIdentity($"{nameof(AutoLiquidityManagementJob)}Trigger") .StartNow().WithSimpleSchedule(scheduleBuilder => - { if (Constants.IS_DEV_ENVIRONMENT) + { + if (Constants.IS_DEV_ENVIRONMENT) { scheduleBuilder.WithIntervalInMinutes(1).RepeatForever(); } @@ -348,7 +351,6 @@ public static async Task Main(string[] args) } }); }); - // Audit Log Cleanup Job q.AddJob(opts => { @@ -362,7 +364,7 @@ public static async Task Main(string[] args) .WithIdentity($"{nameof(AuditLogCleanupJob)}Trigger") .StartNow().WithSimpleSchedule(scheduleBuilder => { - scheduleBuilder.WithIntervalInHours(24).RepeatForever(); + scheduleBuilder.WithIntervalInHours(24).RepeatForever(); }); }); }); diff --git a/src/Shared/NavMenu.razor b/src/Shared/NavMenu.razor index ca3a0120..48181b9b 100644 --- a/src/Shared/NavMenu.razor +++ b/src/Shared/NavMenu.razor @@ -66,6 +66,12 @@ + +