diff --git a/src/DnsmasqWebUI/Components/Dnsmasq/AppLogsSection.razor b/src/DnsmasqWebUI/Components/Dnsmasq/AppLogsSection.razor index 0b82861..5c94319 100644 --- a/src/DnsmasqWebUI/Components/Dnsmasq/AppLogsSection.razor +++ b/src/DnsmasqWebUI/Components/Dnsmasq/AppLogsSection.razor @@ -30,4 +30,4 @@

-
@_initialPlaceholder
+ diff --git a/src/DnsmasqWebUI/Components/Dnsmasq/AppLogsSection.razor.cs b/src/DnsmasqWebUI/Components/Dnsmasq/AppLogsSection.razor.cs index 48909b0..435e0e1 100644 --- a/src/DnsmasqWebUI/Components/Dnsmasq/AppLogsSection.razor.cs +++ b/src/DnsmasqWebUI/Components/Dnsmasq/AppLogsSection.razor.cs @@ -1,5 +1,4 @@ using DnsmasqWebUI.Infrastructure.Client.Abstractions; -using DnsmasqWebUI.Models.Client; using DnsmasqWebUI.Models.Logs; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.SignalR.Client; @@ -18,6 +17,7 @@ public partial class AppLogsSection : IAsyncDisposable [Inject] private ILoggingClient LoggingClient { get; set; } = null!; private bool _justUpdated; + private bool _logsContentReceived; private int _intervalSeconds; private string _logLevel = "Information"; private bool _logLevelDisabled; @@ -42,16 +42,15 @@ public partial class AppLogsSection : IAsyncDisposable protected override void OnParametersSet() { - var next = ClientSettingsFields.AppLogsPollingInterval.Clamp(RefreshIntervalSeconds); - if (next == _intervalSeconds) return; - _intervalSeconds = next; + if (RefreshIntervalSeconds == _intervalSeconds) return; + _intervalSeconds = RefreshIntervalSeconds; RestartPollTimer(); } protected override async Task OnAfterRenderAsync(bool firstRender) { if (!firstRender) return; - _intervalSeconds = ClientSettingsFields.AppLogsPollingInterval.Clamp(RefreshIntervalSeconds); + _intervalSeconds = RefreshIntervalSeconds; var hubUri = new Uri(new Uri(Navigation.BaseUri), "hubs/logs").ToString(); _hubConnection = new HubConnectionBuilder() @@ -144,6 +143,7 @@ public partial class AppLogsSection : IAsyncDisposable await _logsJs.InvokeVoidAsync("replaceLogs", AppLogsPreId, payload.Content, LogsOptions); else await _logsJs.InvokeVoidAsync("appendLogs", AppLogsPreId, payload.Content, LogsOptions); + _logsContentReceived = true; SetJustUpdated(); StateHasChanged(); } diff --git a/src/DnsmasqWebUI/Components/Dnsmasq/LogsPreView.razor b/src/DnsmasqWebUI/Components/Dnsmasq/LogsPreView.razor new file mode 100644 index 0000000..b4f2baa --- /dev/null +++ b/src/DnsmasqWebUI/Components/Dnsmasq/LogsPreView.razor @@ -0,0 +1,11 @@ +@namespace DnsmasqWebUI.Components.Dnsmasq +@* Renders a pre element for logs. Once Freeze is true, stops re-rendering so Blazor does not overwrite JS-owned content. *@ +
@Placeholder
+ +@code { + [Parameter] public string Id { get; set; } = ""; + [Parameter] public string Placeholder { get; set; } = "Connecting…"; + [Parameter] public bool Freeze { get; set; } + + protected override bool ShouldRender() => !Freeze; +} diff --git a/src/DnsmasqWebUI/Components/Dnsmasq/LogsSection.razor b/src/DnsmasqWebUI/Components/Dnsmasq/LogsSection.razor index 546edad..fff60fc 100644 --- a/src/DnsmasqWebUI/Components/Dnsmasq/LogsSection.razor +++ b/src/DnsmasqWebUI/Components/Dnsmasq/LogsSection.razor @@ -24,7 +24,7 @@ } @if (_status.LogsConfigured) { -
@_initialPlaceholder
+ }
diff --git a/src/DnsmasqWebUI/Components/Dnsmasq/LogsSection.razor.cs b/src/DnsmasqWebUI/Components/Dnsmasq/LogsSection.razor.cs index b543494..a61e31c 100644 --- a/src/DnsmasqWebUI/Components/Dnsmasq/LogsSection.razor.cs +++ b/src/DnsmasqWebUI/Components/Dnsmasq/LogsSection.razor.cs @@ -1,4 +1,3 @@ -using DnsmasqWebUI.Models.Client; using DnsmasqWebUI.Models.Dnsmasq; using DnsmasqWebUI.Models.Logs; using Microsoft.AspNetCore.Components; @@ -18,6 +17,7 @@ public partial class LogsSection : IAsyncDisposable private DnsmasqServiceStatus? _status; private bool _refreshing; private bool _justUpdated; + private bool _logsContentReceived; private int _intervalSeconds; private HubConnection? _hubConnection; private IJSObjectReference? _logsJs; @@ -42,16 +42,15 @@ public partial class LogsSection : IAsyncDisposable protected override void OnParametersSet() { _status = Status; - var next = ClientSettingsFields.RecentLogsPollingInterval.Clamp(RefreshIntervalSeconds); - if (next == _intervalSeconds) return; - _intervalSeconds = next; + if (RefreshIntervalSeconds == _intervalSeconds) return; + _intervalSeconds = RefreshIntervalSeconds; RestartPollTimer(); } protected override async Task OnAfterRenderAsync(bool firstRender) { if (!firstRender) return; - _intervalSeconds = ClientSettingsFields.RecentLogsPollingInterval.Clamp(RefreshIntervalSeconds); + _intervalSeconds = RefreshIntervalSeconds; _status = Status; var hubUri = new Uri(new Uri(Navigation.BaseUri), "hubs/logs").ToString(); @@ -130,6 +129,7 @@ public partial class LogsSection : IAsyncDisposable await _logsJs.InvokeVoidAsync("replaceLogs", LogsPreId, payload.Content, LogsOptions); else await _logsJs.InvokeVoidAsync("appendLogs", LogsPreId, payload.Content, LogsOptions); + _logsContentReceived = true; SetJustUpdated(); StateHasChanged(); } diff --git a/src/DnsmasqWebUI/Components/Dnsmasq/StatusSection.razor.cs b/src/DnsmasqWebUI/Components/Dnsmasq/StatusSection.razor.cs index ab9332c..9f9a7e7 100644 --- a/src/DnsmasqWebUI/Components/Dnsmasq/StatusSection.razor.cs +++ b/src/DnsmasqWebUI/Components/Dnsmasq/StatusSection.razor.cs @@ -1,5 +1,4 @@ using DnsmasqWebUI.Infrastructure.Client.Abstractions; -using DnsmasqWebUI.Models.Client; using DnsmasqWebUI.Models.Dnsmasq; using Microsoft.AspNetCore.Components; @@ -26,7 +25,7 @@ public partial class StatusSection : IDisposable protected override async Task OnInitializedAsync() { - _intervalSeconds = ClientSettingsFields.ServiceStatusPollingInterval.Clamp(RefreshIntervalSeconds); + _intervalSeconds = RefreshIntervalSeconds; await RefreshAsync(); _timer = new Timer( _ => _ = InvokeAsync(OnRefreshTick), @@ -37,9 +36,8 @@ public partial class StatusSection : IDisposable protected override void OnParametersSet() { - var next = ClientSettingsFields.ServiceStatusPollingInterval.Clamp(RefreshIntervalSeconds); - if (next == _intervalSeconds) return; - _intervalSeconds = next; + if (RefreshIntervalSeconds == _intervalSeconds) return; + _intervalSeconds = RefreshIntervalSeconds; _timer?.Dispose(); _timer = new Timer( _ => _ = InvokeAsync(OnRefreshTick), diff --git a/src/DnsmasqWebUI/Components/Modals/SettingsModal.razor b/src/DnsmasqWebUI/Components/Modals/SettingsModal.razor index c77c0a2..d41122e 100644 --- a/src/DnsmasqWebUI/Components/Modals/SettingsModal.razor +++ b/src/DnsmasqWebUI/Components/Modals/SettingsModal.razor @@ -35,15 +35,23 @@ } @foreach (var group in GetVisibleGroups()) { - var isExpanded = IsGroupExpanded(group); + var isAllSections = SettingsContext == SettingsModalContext.All; + var isExpanded = isAllSections ? IsGroupExpanded(group) : true;
- + @if (isAllSections) + { + + } + else + { +
@group.DisplayName
+ }
@foreach (var key in group.SectionKeys) @@ -56,57 +64,69 @@ case SettingsModalSections.ServiceStatus: { var meta = SettingsModalSections.All[SettingsModalSections.ServiceStatus]; + var field = ClientSettingsFields.ServiceStatusPollingInterval; - + break; } case SettingsModalSections.Logs: { var meta = SettingsModalSections.All[SettingsModalSections.Logs]; + var field = ClientSettingsFields.RecentLogsPollingInterval; - + break; } case SettingsModalSections.AppLogs: { var meta = SettingsModalSections.All[SettingsModalSections.AppLogs]; + var field = ClientSettingsFields.AppLogsPollingInterval; - + break; } case SettingsModalSections.Leases: { var meta = SettingsModalSections.All[SettingsModalSections.Leases]; + var field = ClientSettingsFields.LeasesPollingInterval; - + break; } case SettingsModalSections.RecentLogsDisplay: -
- - -
-
- - -
- break; + { + var maxLinesField = ClientSettingsFields.RecentLogsMaxLines; + var autoScrollField = ClientSettingsFields.RecentLogsAutoScroll; +
+ + +
+
+ + +
+ break; + } case SettingsModalSections.AppLogsDisplay: -
- - -
-
- - -
- break; + { + var maxLinesField = ClientSettingsFields.AppLogsMaxLines; + var autoScrollField = ClientSettingsFields.AppLogsAutoScroll; +
+ + +
+
+ + +
+ break; + } }
} diff --git a/src/DnsmasqWebUI/Components/Modals/SettingsModal.razor.cs b/src/DnsmasqWebUI/Components/Modals/SettingsModal.razor.cs index 8f7d9a8..f525673 100644 --- a/src/DnsmasqWebUI/Components/Modals/SettingsModal.razor.cs +++ b/src/DnsmasqWebUI/Components/Modals/SettingsModal.razor.cs @@ -15,7 +15,6 @@ public partial class SettingsModal : IAsyncDisposable private ElementReference _dialogRef; private IJSObjectReference? _jsModule; private DotNetObjectReference? _dotNetRef; - private ClientSettings _editingSettings = new(); private List _validationErrors = new(); private string _searchTerm = string.Empty; private HashSet _expandedGroupIds = new(StringComparer.OrdinalIgnoreCase); @@ -46,18 +45,7 @@ public partial class SettingsModal : IAsyncDisposable if (visible.Count > 0) _expandedGroupIds.Add(visible[0].Id); _validationErrors.Clear(); - _editingSettings = await ClientSettingsService.LoadSettingsAsync(); - _editingSettings = new ClientSettings - { - ServiceStatusPollingIntervalSeconds = _editingSettings.ServiceStatusPollingIntervalSeconds, - RecentLogsPollingIntervalSeconds = _editingSettings.RecentLogsPollingIntervalSeconds, - AppLogsPollingIntervalSeconds = _editingSettings.AppLogsPollingIntervalSeconds, - LeasesPollingIntervalSeconds = _editingSettings.LeasesPollingIntervalSeconds, - RecentLogsMaxLines = _editingSettings.RecentLogsMaxLines, - RecentLogsAutoScroll = _editingSettings.RecentLogsAutoScroll, - AppLogsMaxLines = _editingSettings.AppLogsMaxLines, - AppLogsAutoScroll = _editingSettings.AppLogsAutoScroll - }; + await ClientSettingsService.LoadSettingsAsync(); } } @@ -120,18 +108,9 @@ public partial class SettingsModal : IAsyncDisposable private void RunValidation() { _validationErrors.Clear(); - var checks = new (string Section, int Value, ClientSettingsFields.FieldBounds Bounds)[] + foreach (var (section, field) in ClientSettingsFields.ValidationChecks) { - (SettingsModalSections.ServiceStatus, _editingSettings.ServiceStatusPollingIntervalSeconds, ClientSettingsFields.ServiceStatusPollingInterval), - (SettingsModalSections.Logs, _editingSettings.RecentLogsPollingIntervalSeconds, ClientSettingsFields.RecentLogsPollingInterval), - (SettingsModalSections.AppLogs, _editingSettings.AppLogsPollingIntervalSeconds, ClientSettingsFields.AppLogsPollingInterval), - (SettingsModalSections.Leases, _editingSettings.LeasesPollingIntervalSeconds, ClientSettingsFields.LeasesPollingInterval), - (SettingsModalSections.RecentLogsDisplay, _editingSettings.RecentLogsMaxLines, ClientSettingsFields.RecentLogsMaxLines), - (SettingsModalSections.AppLogsDisplay, _editingSettings.AppLogsMaxLines, ClientSettingsFields.AppLogsMaxLines), - }; - foreach (var (section, value, bounds) in checks) - { - if (ShouldShowSection(section) && bounds.Validate(value) is { } err) + if (ShouldShowSection(section) && field.Validate() is { } err) _validationErrors.Add(err); } } @@ -145,7 +124,7 @@ public partial class SettingsModal : IAsyncDisposable return; } - await ClientSettingsService.SaveSettingsAsync(_editingSettings); + await ClientSettingsService.SaveSettingsAsync(ClientSettingsFields.ToDto()); await (_jsModule?.InvokeVoidAsync("closeModal", _dialogRef) ?? ValueTask.CompletedTask); await OnClose.InvokeAsync(); } diff --git a/src/DnsmasqWebUI/Components/Modals/SettingsModal.razor.css b/src/DnsmasqWebUI/Components/Modals/SettingsModal.razor.css index 1ce52f3..3a913a5 100644 --- a/src/DnsmasqWebUI/Components/Modals/SettingsModal.razor.css +++ b/src/DnsmasqWebUI/Components/Modals/SettingsModal.razor.css @@ -86,6 +86,10 @@ transform: rotate(135deg); } +.settings-group-title-static { + margin-left: 0.75rem; +} + .settings-group-content { max-height: 0; overflow: hidden; diff --git a/src/DnsmasqWebUI/Components/Pages/Dhcp.razor b/src/DnsmasqWebUI/Components/Pages/Dhcp.razor index 6dccfab..785c4dd 100644 --- a/src/DnsmasqWebUI/Components/Pages/Dhcp.razor +++ b/src/DnsmasqWebUI/Components/Pages/Dhcp.razor @@ -417,8 +417,8 @@ else _message = null; try { - var clientSettings = await ClientSettingsService.LoadSettingsAsync(); - _leasesRefreshIntervalSeconds = ClientSettingsFields.LeasesPollingInterval.Clamp(clientSettings.LeasesPollingIntervalSeconds); + await ClientSettingsService.LoadSettingsAsync(); + _leasesRefreshIntervalSeconds = ClientSettingsFields.LeasesPollingInterval.Value; _status = await StatusClient.GetStatusAsync(); var entries = await DhcpHostsClient.GetDhcpHostsAsync(); _entries = entries?.ToList() ?? new List(); @@ -436,10 +436,9 @@ else private async void OnSettingsChanged() { - await InvokeAsync(async () => + await InvokeAsync(() => { - var clientSettings = await ClientSettingsService.LoadSettingsAsync(); - _leasesRefreshIntervalSeconds = ClientSettingsFields.LeasesPollingInterval.Clamp(clientSettings.LeasesPollingIntervalSeconds); + _leasesRefreshIntervalSeconds = ClientSettingsFields.LeasesPollingInterval.Value; if (_status != null && _status.LeasesPathConfigured) StartLeasesRefreshTimer(); StateHasChanged(); diff --git a/src/DnsmasqWebUI/Components/Pages/Dnsmasq.razor b/src/DnsmasqWebUI/Components/Pages/Dnsmasq.razor index d0c7574..474b2d3 100644 --- a/src/DnsmasqWebUI/Components/Pages/Dnsmasq.razor +++ b/src/DnsmasqWebUI/Components/Pages/Dnsmasq.razor @@ -78,21 +78,14 @@ else if (_status != null)
} - - - + + + } @code { private string AppTitle => AppOptions!.Value!.EffectiveTitle; - private int _serviceStatusIntervalSeconds = 15; - private int _recentLogsIntervalSeconds = 15; - private int _appLogsIntervalSeconds = 15; - private int _recentLogsMaxLines = 500; - private bool _recentLogsAutoScroll = true; - private int _appLogsMaxLines = 500; - private bool _appLogsAutoScroll = true; private DnsmasqServiceStatus? _status; private DnsmasqConfigSet? _configSet; @@ -117,14 +110,7 @@ else if (_status != null) { await Task.Delay(1, _cts.Token); var token = _cts.Token; - var clientSettings = await ClientSettingsService.LoadSettingsAsync(); - _serviceStatusIntervalSeconds = ClientSettingsFields.ServiceStatusPollingInterval.Clamp(clientSettings.ServiceStatusPollingIntervalSeconds); - _recentLogsIntervalSeconds = ClientSettingsFields.RecentLogsPollingInterval.Clamp(clientSettings.RecentLogsPollingIntervalSeconds); - _appLogsIntervalSeconds = ClientSettingsFields.AppLogsPollingInterval.Clamp(clientSettings.AppLogsPollingIntervalSeconds); - _recentLogsMaxLines = ClientSettingsFields.RecentLogsMaxLines.Clamp(clientSettings.RecentLogsMaxLines); - _recentLogsAutoScroll = clientSettings.RecentLogsAutoScroll; - _appLogsMaxLines = ClientSettingsFields.AppLogsMaxLines.Clamp(clientSettings.AppLogsMaxLines); - _appLogsAutoScroll = clientSettings.AppLogsAutoScroll; + await ClientSettingsService.LoadSettingsAsync(); _status = await StatusClient.GetStatusAsync(token); if (_status != null) { @@ -161,18 +147,7 @@ else if (_status != null) private async void OnSettingsChanged() { - await InvokeAsync(async () => - { - var clientSettings = await ClientSettingsService.LoadSettingsAsync(); - _serviceStatusIntervalSeconds = ClientSettingsFields.ServiceStatusPollingInterval.Clamp(clientSettings.ServiceStatusPollingIntervalSeconds); - _recentLogsIntervalSeconds = ClientSettingsFields.RecentLogsPollingInterval.Clamp(clientSettings.RecentLogsPollingIntervalSeconds); - _appLogsIntervalSeconds = ClientSettingsFields.AppLogsPollingInterval.Clamp(clientSettings.AppLogsPollingIntervalSeconds); - _recentLogsMaxLines = ClientSettingsFields.RecentLogsMaxLines.Clamp(clientSettings.RecentLogsMaxLines); - _recentLogsAutoScroll = clientSettings.RecentLogsAutoScroll; - _appLogsMaxLines = ClientSettingsFields.AppLogsMaxLines.Clamp(clientSettings.AppLogsMaxLines); - _appLogsAutoScroll = clientSettings.AppLogsAutoScroll; - StateHasChanged(); - }); + await InvokeAsync(StateHasChanged); } private async Task ReloadDnsmasq() diff --git a/src/DnsmasqWebUI/Components/_Imports.razor b/src/DnsmasqWebUI/Components/_Imports.razor index a32a2d2..c6d6617 100644 --- a/src/DnsmasqWebUI/Components/_Imports.razor +++ b/src/DnsmasqWebUI/Components/_Imports.razor @@ -8,6 +8,7 @@ @using Microsoft.JSInterop @using DnsmasqWebUI @using DnsmasqWebUI.Models.Client +@using DnsmasqWebUI.Models.Client.Abstractions @using DnsmasqWebUI.Infrastructure.Services.Abstractions @using DnsmasqWebUI.Components @using DnsmasqWebUI.Components.Modals diff --git a/src/DnsmasqWebUI/Infrastructure/Services/ClientSettingsService.cs b/src/DnsmasqWebUI/Infrastructure/Services/ClientSettingsService.cs index 0b1d301..c8cc5de 100644 --- a/src/DnsmasqWebUI/Infrastructure/Services/ClientSettingsService.cs +++ b/src/DnsmasqWebUI/Infrastructure/Services/ClientSettingsService.cs @@ -47,14 +47,23 @@ public sealed class ClientSettingsService : IClientSettingsService, IAsyncDispos var module = await GetModuleAsync(); if (module == null) return new ClientSettings(); var json = await module.InvokeAsync("getItem"); - if (string.IsNullOrWhiteSpace(json)) return new ClientSettings(); + if (string.IsNullOrWhiteSpace(json)) + { + var defaults = new ClientSettings(); + ClientSettingsFields.HydrateFrom(defaults); + return defaults; + } try { - return JsonSerializer.Deserialize(json, JsonOptions) ?? new ClientSettings(); + var settings = JsonSerializer.Deserialize(json, JsonOptions) ?? new ClientSettings(); + ClientSettingsFields.HydrateFrom(settings); + return settings; } catch { - return new ClientSettings(); + var defaults = new ClientSettings(); + ClientSettingsFields.HydrateFrom(defaults); + return defaults; } } diff --git a/src/DnsmasqWebUI/Models/Client/Abstractions/ClientSettingsField.cs b/src/DnsmasqWebUI/Models/Client/Abstractions/ClientSettingsField.cs new file mode 100644 index 0000000..91fa6bb --- /dev/null +++ b/src/DnsmasqWebUI/Models/Client/Abstractions/ClientSettingsField.cs @@ -0,0 +1,33 @@ +namespace DnsmasqWebUI.Models.Client.Abstractions; + +/// +/// Generic base for client settings fields. Holds the value and provides coercion on set. +/// +public abstract class ClientSettingsField : IClientSettingsField +{ + public string DisplayName { get; } + private T _value = default!; + + public virtual T Value + { + get => _value; + set => _value = CoerceValue(value); + } + + object? IClientSettingsField.Value + { + get => Value; + set => Value = (T)value!; + } + + public abstract string? Validate(T value); + + string? IClientSettingsField.Validate() => Validate(Value); + + protected virtual T CoerceValue(T value) => value; + + protected ClientSettingsField(string displayName) + { + DisplayName = displayName; + } +} diff --git a/src/DnsmasqWebUI/Models/Client/Abstractions/IClientSettingsField.cs b/src/DnsmasqWebUI/Models/Client/Abstractions/IClientSettingsField.cs new file mode 100644 index 0000000..1c77c94 --- /dev/null +++ b/src/DnsmasqWebUI/Models/Client/Abstractions/IClientSettingsField.cs @@ -0,0 +1,11 @@ +namespace DnsmasqWebUI.Models.Client.Abstractions; + +/// +/// Non-generic interface for heterogeneous iteration over client settings fields. +/// +public interface IClientSettingsField +{ + string DisplayName { get; } + object? Value { get; set; } + string? Validate(); +} diff --git a/src/DnsmasqWebUI/Models/Client/ClientSettingsBoolField.cs b/src/DnsmasqWebUI/Models/Client/ClientSettingsBoolField.cs new file mode 100644 index 0000000..547673e --- /dev/null +++ b/src/DnsmasqWebUI/Models/Client/ClientSettingsBoolField.cs @@ -0,0 +1,13 @@ +using DnsmasqWebUI.Models.Client.Abstractions; + +namespace DnsmasqWebUI.Models.Client; + +/// +/// Boolean field. No validation or coercion. +/// +public sealed class ClientSettingsBoolField : ClientSettingsField +{ + public ClientSettingsBoolField(string displayName) : base(displayName) { } + + public override string? Validate(bool value) => null; +} diff --git a/src/DnsmasqWebUI/Models/Client/ClientSettingsFields.cs b/src/DnsmasqWebUI/Models/Client/ClientSettingsFields.cs index 374decc..0eb164a 100644 --- a/src/DnsmasqWebUI/Models/Client/ClientSettingsFields.cs +++ b/src/DnsmasqWebUI/Models/Client/ClientSettingsFields.cs @@ -1,23 +1,66 @@ +using DnsmasqWebUI.Models.Client.Abstractions; + namespace DnsmasqWebUI.Models.Client; /// -/// Per-field metadata for client settings: bounds, display names for validation errors. -/// Used by SettingsModal (validation on save) and by pages/components (defensive clamp when loading from storage). +/// Static field definitions and hydration/serialization helpers. /// public static class ClientSettingsFields { - public sealed record FieldBounds(int Min, int Max, string DisplayName) + static ClientSettingsFields() { - public bool IsValid(int value) => value >= Min && value <= Max; - public int Clamp(int value) => Math.Clamp(value, Min, Max); - public string? Validate(int value) => - IsValid(value) ? null : $"{DisplayName} must be between {Min} and {Max} seconds. You entered {value}."; + HydrateFrom(new ClientSettings()); } - public static readonly FieldBounds ServiceStatusPollingInterval = new(5, 300, "Service status polling interval"); - public static readonly FieldBounds RecentLogsPollingInterval = new(5, 300, "Recent logs polling interval"); - public static readonly FieldBounds AppLogsPollingInterval = new(5, 300, "App logs polling interval"); - public static readonly FieldBounds LeasesPollingInterval = new(5, 300, "DHCP leases refresh interval"); - public static readonly FieldBounds RecentLogsMaxLines = new(100, 2000, "Recent logs max lines"); - public static readonly FieldBounds AppLogsMaxLines = new(100, 2000, "App logs max lines"); + public static readonly ClientSettingsMinMaxField ServiceStatusPollingInterval = new("Service status polling interval", 5, 300); + public static readonly ClientSettingsMinMaxField RecentLogsPollingInterval = new("Recent logs polling interval", 5, 300); + public static readonly ClientSettingsMinMaxField AppLogsPollingInterval = new("App logs polling interval", 5, 300); + public static readonly ClientSettingsMinMaxField LeasesPollingInterval = new("DHCP leases refresh interval", 5, 300); + public static readonly ClientSettingsMinMaxField RecentLogsMaxLines = new("Recent logs max lines", 100, 2000, "lines"); + public static readonly ClientSettingsMinMaxField AppLogsMaxLines = new("App logs max lines", 100, 2000, "lines"); + public static readonly ClientSettingsBoolField RecentLogsAutoScroll = new("Recent logs auto-scroll"); + public static readonly ClientSettingsBoolField AppLogsAutoScroll = new("App logs auto-scroll"); + + private static readonly (string Section, IClientSettingsField Field)[] ValidationMap = + [ + (SettingsModalSections.ServiceStatus, ServiceStatusPollingInterval), + (SettingsModalSections.Logs, RecentLogsPollingInterval), + (SettingsModalSections.AppLogs, AppLogsPollingInterval), + (SettingsModalSections.Leases, LeasesPollingInterval), + (SettingsModalSections.RecentLogsDisplay, RecentLogsMaxLines), + (SettingsModalSections.RecentLogsDisplay, RecentLogsAutoScroll), + (SettingsModalSections.AppLogsDisplay, AppLogsMaxLines), + (SettingsModalSections.AppLogsDisplay, AppLogsAutoScroll), + ]; + + public static IReadOnlyList All => [ServiceStatusPollingInterval, RecentLogsPollingInterval, AppLogsPollingInterval, LeasesPollingInterval, RecentLogsMaxLines, AppLogsMaxLines, RecentLogsAutoScroll, AppLogsAutoScroll]; + + public static IReadOnlyList<(string Section, IClientSettingsField Field)> ValidationChecks => ValidationMap; + + public static void HydrateFrom(ClientSettings dto) + { + ServiceStatusPollingInterval.Value = dto.ServiceStatusPollingIntervalSeconds; + RecentLogsPollingInterval.Value = dto.RecentLogsPollingIntervalSeconds; + AppLogsPollingInterval.Value = dto.AppLogsPollingIntervalSeconds; + LeasesPollingInterval.Value = dto.LeasesPollingIntervalSeconds; + RecentLogsMaxLines.Value = dto.RecentLogsMaxLines; + RecentLogsAutoScroll.Value = dto.RecentLogsAutoScroll; + AppLogsMaxLines.Value = dto.AppLogsMaxLines; + AppLogsAutoScroll.Value = dto.AppLogsAutoScroll; + } + + public static ClientSettings ToDto() + { + return new ClientSettings + { + ServiceStatusPollingIntervalSeconds = ServiceStatusPollingInterval.Value, + RecentLogsPollingIntervalSeconds = RecentLogsPollingInterval.Value, + AppLogsPollingIntervalSeconds = AppLogsPollingInterval.Value, + LeasesPollingIntervalSeconds = LeasesPollingInterval.Value, + RecentLogsMaxLines = RecentLogsMaxLines.Value, + RecentLogsAutoScroll = RecentLogsAutoScroll.Value, + AppLogsMaxLines = AppLogsMaxLines.Value, + AppLogsAutoScroll = AppLogsAutoScroll.Value, + }; + } } diff --git a/src/DnsmasqWebUI/Models/Client/ClientSettingsMinMaxField.cs b/src/DnsmasqWebUI/Models/Client/ClientSettingsMinMaxField.cs new file mode 100644 index 0000000..fd529e8 --- /dev/null +++ b/src/DnsmasqWebUI/Models/Client/ClientSettingsMinMaxField.cs @@ -0,0 +1,33 @@ +using DnsmasqWebUI.Models.Client.Abstractions; + +namespace DnsmasqWebUI.Models.Client; + +/// +/// Min/max bounded integer field with optional clamping on set. +/// +public sealed class ClientSettingsMinMaxField : ClientSettingsField +{ + public int Min { get; } + public int Max { get; } + public string Unit { get; } + public bool AllowClamp { get; } + + public ClientSettingsMinMaxField(string displayName, int min, int max, string unit = "seconds", bool allowClamp = true) + : base(displayName) + { + Min = min; + Max = max; + Unit = unit; + AllowClamp = allowClamp; + } + + public override string? Validate(int value) + { + if (value < Min || value > Max) + return $"{DisplayName} must be between {Min} and {Max} {Unit}. You entered {value}."; + return null; + } + + protected override int CoerceValue(int value) => + AllowClamp ? Math.Clamp(value, Min, Max) : value; +}