From a8ddcdec4e7160c33d954a9fb4df31bb8aead277 Mon Sep 17 00:00:00 2001 From: Alex Hope-O'Connor Date: Tue, 3 Feb 2026 01:35:26 +1000 Subject: [PATCH] Dnsmasq/Hosts UI tweaks, test harness: down before redeploy, exclude leases from mount - Dnsmasq page: Config (Main config, Managed file, Config set) under Service configuration; remove Paths/Leases (in Effective config DHCP); EffectiveConfigSection collapsible groups; StatusSection flex-wrap - Hosts: HostsFileSection always shows table; 'No host entries' in tbody when empty - Models: reorganize into sub-namespaces (Config, EffectiveConfig, Hosts, Dhcp, Status); move EffectiveConfig/Status/Logs components under Dnsmasq - Test harness: prepare-test-mount.sh runs 'docker compose down' before mount+up so all containers (including DHCP clients) restart; exclude testdata/leases from sync (unit tests only); testdata/README --- docker-compose.test.yml | 1 - scripts/prepare-test-mount.sh | 7 +- .../DnsmasqConfDhcpHostLineParserTests.cs | 2 +- .../DnsmasqConfDirectiveParserTests.cs | 3 +- .../DnsmasqConfFileLineParserTests.cs | 2 +- .../DnsmasqConfigSetServiceTests.cs | 2 +- .../DnsmasqLeasesFileLineParserTests.cs | 2 +- .../EffectiveDnsmasqConfigTests.cs | 2 +- .../HostsFileLineParserTests.cs | 2 +- .../Http/Abstractions/IConfigSetClient.cs | 2 +- .../Http/Abstractions/IDhcpHostsClient.cs | 3 +- .../Client/Http/Abstractions/IHostsClient.cs | 3 +- .../Client/Http/Abstractions/ILeasesClient.cs | 2 +- .../Client/Http/Abstractions/IStatusClient.cs | 2 +- .../Client/Http/ConfigSetClient.cs | 2 +- .../Client/Http/DhcpHostsClient.cs | 3 +- src/DnsmasqWebUI/Client/Http/HostsClient.cs | 3 +- src/DnsmasqWebUI/Client/Http/LeasesClient.cs | 2 +- src/DnsmasqWebUI/Client/Http/StatusClient.cs | 2 +- .../EffectiveConfigMultiValueRow.razor | 2 +- .../EffectiveConfigOptionItem.razor | 2 +- .../EffectiveConfigSection.razor | 178 ++++++++++++++++ .../EffectiveConfigSection.razor.css | 79 +++++++ .../{Home => Dnsmasq}/LogsSection.razor | 0 .../{Home => Dnsmasq}/LogsSection.razor.cs | 2 +- .../{Home => Dnsmasq}/StatusSection.razor | 12 +- .../{Home => Dnsmasq}/StatusSection.razor.cs | 2 +- .../Components/Hosts/HostsFileSection.razor | 48 +++-- .../Components/Hosts/_Imports.razor | 1 - .../Components/Pages/Dnsmasq.razor | 199 +++--------------- src/DnsmasqWebUI/Components/Pages/Hosts.razor | 3 - src/DnsmasqWebUI/Components/_Imports.razor | 8 +- .../Controllers/ConfigController.cs | 2 +- .../Controllers/DhcpController.cs | 3 +- .../Controllers/HostsController.cs | 4 +- .../Controllers/LeasesController.cs | 2 +- .../Controllers/StatusController.cs | 3 +- .../Models/Config/DnsmasqConfLine.cs | 2 +- .../Models/{ => Dhcp}/DhcpHostEntry.cs | 2 +- .../Models/{ => Dhcp}/LeaseEntry.cs | 2 +- .../Models/{ => Dhcp}/LeasesResult.cs | 2 +- .../ConfigValueSource.cs | 2 +- .../DnsmasqConfFileSource.cs | 2 +- .../{ => EffectiveConfig}/DnsmasqConfigSet.cs | 2 +- .../DnsmasqConfigSetEntry.cs | 2 +- .../EffectiveConfigSources.cs | 2 +- .../EffectiveDnsmasqConfig.cs | 2 +- .../Models/{ => Hosts}/HostEntry.cs | 2 +- .../Models/{ => Hosts}/ReadOnlyHostsFile.cs | 2 +- .../{ => Status}/DnsmasqServiceStatus.cs | 4 +- .../{ => Status}/SaveWithReloadResult.cs | 2 +- src/DnsmasqWebUI/Parsers/ConfParserHelpers.cs | 42 ++++ .../Parsers/DnsmasqConfDhcpHostLineParser.cs | 46 +++- .../Parsers/DnsmasqConfDirectiveParser.cs | 58 +++-- .../Parsers/DnsmasqConfFileLineParser.cs | 12 +- .../Parsers/DnsmasqConfIncludeParser.cs | 29 ++- .../Parsers/DnsmasqLeasesFileLineParser.cs | 2 +- .../Parsers/HostsFileLineParser.cs | 2 +- .../Abstractions/IDnsmasqConfigService.cs | 3 +- .../Abstractions/IDnsmasqConfigSetService.cs | 2 +- .../Abstractions/IHostsFileService.cs | 2 +- .../Services/Abstractions/ILeasesCache.cs | 2 +- .../Abstractions/ILeasesFileService.cs | 2 +- .../Services/DnsmasqConfigService.cs | 3 +- .../Services/DnsmasqConfigSetService.cs | 2 +- .../EnsureManagedConfigHostedService.cs | 2 +- src/DnsmasqWebUI/Services/HostsFileService.cs | 3 +- src/DnsmasqWebUI/Services/LeasesCache.cs | 2 +- .../Services/LeasesFileService.cs | 2 +- testdata/README | 2 +- 70 files changed, 542 insertions(+), 303 deletions(-) rename src/DnsmasqWebUI/Components/{ => Dnsmasq}/EffectiveConfig/EffectiveConfigMultiValueRow.razor (74%) rename src/DnsmasqWebUI/Components/{ => Dnsmasq}/EffectiveConfig/EffectiveConfigOptionItem.razor (87%) create mode 100644 src/DnsmasqWebUI/Components/Dnsmasq/EffectiveConfig/EffectiveConfigSection.razor create mode 100644 src/DnsmasqWebUI/Components/Dnsmasq/EffectiveConfig/EffectiveConfigSection.razor.css rename src/DnsmasqWebUI/Components/{Home => Dnsmasq}/LogsSection.razor (100%) rename src/DnsmasqWebUI/Components/{Home => Dnsmasq}/LogsSection.razor.cs (98%) rename src/DnsmasqWebUI/Components/{Home => Dnsmasq}/StatusSection.razor (69%) rename src/DnsmasqWebUI/Components/{Home => Dnsmasq}/StatusSection.razor.cs (98%) delete mode 100644 src/DnsmasqWebUI/Components/Hosts/_Imports.razor rename src/DnsmasqWebUI/Models/{ => Dhcp}/DhcpHostEntry.cs (97%) rename src/DnsmasqWebUI/Models/{ => Dhcp}/LeaseEntry.cs (90%) rename src/DnsmasqWebUI/Models/{ => Dhcp}/LeasesResult.cs (84%) rename src/DnsmasqWebUI/Models/{ => EffectiveConfig}/ConfigValueSource.cs (97%) rename src/DnsmasqWebUI/Models/{ => EffectiveConfig}/DnsmasqConfFileSource.cs (79%) rename src/DnsmasqWebUI/Models/{ => EffectiveConfig}/DnsmasqConfigSet.cs (88%) rename src/DnsmasqWebUI/Models/{ => EffectiveConfig}/DnsmasqConfigSetEntry.cs (83%) rename src/DnsmasqWebUI/Models/{ => EffectiveConfig}/EffectiveConfigSources.cs (98%) rename src/DnsmasqWebUI/Models/{ => EffectiveConfig}/EffectiveDnsmasqConfig.cs (98%) rename src/DnsmasqWebUI/Models/{ => Hosts}/HostEntry.cs (94%) rename src/DnsmasqWebUI/Models/{ => Hosts}/ReadOnlyHostsFile.cs (83%) rename src/DnsmasqWebUI/Models/{ => Status}/DnsmasqServiceStatus.cs (98%) rename src/DnsmasqWebUI/Models/{ => Status}/SaveWithReloadResult.cs (86%) diff --git a/docker-compose.test.yml b/docker-compose.test.yml index 2c6d6e0..79db599 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -21,7 +21,6 @@ services: DNSMASQ_CONF: /data/dnsmasq-test.conf Dnsmasq__MainConfigPath: /data/dnsmasq-test.conf Dnsmasq__ManagedFileName: zz-dnsmasq-webui.conf - # System hosts: app default /etc/hosts shows the container's hosts file (read-only). Dnsmasq__ReloadCommand: "pkill -HUP -x dnsmasq" Dnsmasq__StatusCommand: "pgrep -x dnsmasq" # Simulated systemctl status + real logs (dnsmasq logs to /data/dnsmasq.log via log-facility in dnsmasq-test.conf) diff --git a/scripts/prepare-test-mount.sh b/scripts/prepare-test-mount.sh index ea237df..5885742 100755 --- a/scripts/prepare-test-mount.sh +++ b/scripts/prepare-test-mount.sh @@ -168,6 +168,10 @@ if [ ! -d "$SOURCE_DIR" ]; then exit 1 fi +# Take the harness down so all containers (including one-shot DHCP clients) are recreated on up. +echo "Stopping test harness: docker compose -f $COMPOSE_FILE down" +docker compose -f "$COMPOSE_FILE" down + mkdir -p "$MOUNT_DIR" if [ "$NO_CLEAR" = false ]; then @@ -176,9 +180,10 @@ if [ "$NO_CLEAR" = false ]; then fi if command -v rsync >/dev/null 2>&1; then - rsync -a "$SOURCE_DIR/" "$MOUNT_DIR/" + rsync -a --exclude='leases' "$SOURCE_DIR/" "$MOUNT_DIR/" else cp -r "$SOURCE_DIR/." "$MOUNT_DIR/" + rm -f "$MOUNT_DIR/leases" fi # Remove any leftover managed config from previous runs so dnsmasq starts clean (app will create zz-dnsmasq-webui.conf on startup). diff --git a/src/DnsmasqWebUI.Tests/DnsmasqConfDhcpHostLineParserTests.cs b/src/DnsmasqWebUI.Tests/DnsmasqConfDhcpHostLineParserTests.cs index 2664b37..a23d85f 100644 --- a/src/DnsmasqWebUI.Tests/DnsmasqConfDhcpHostLineParserTests.cs +++ b/src/DnsmasqWebUI.Tests/DnsmasqConfDhcpHostLineParserTests.cs @@ -1,4 +1,4 @@ -using DnsmasqWebUI.Models; +using DnsmasqWebUI.Models.Dhcp; using DnsmasqWebUI.Parsers; namespace DnsmasqWebUI.Tests; diff --git a/src/DnsmasqWebUI.Tests/DnsmasqConfDirectiveParserTests.cs b/src/DnsmasqWebUI.Tests/DnsmasqConfDirectiveParserTests.cs index 2977444..908e2ac 100644 --- a/src/DnsmasqWebUI.Tests/DnsmasqConfDirectiveParserTests.cs +++ b/src/DnsmasqWebUI.Tests/DnsmasqConfDirectiveParserTests.cs @@ -1,5 +1,6 @@ -using DnsmasqWebUI.Models; using DnsmasqWebUI.Models.Config; +using DnsmasqWebUI.Models.Dhcp; +using DnsmasqWebUI.Models.EffectiveConfig; using DnsmasqWebUI.Parsers; namespace DnsmasqWebUI.Tests; diff --git a/src/DnsmasqWebUI.Tests/DnsmasqConfFileLineParserTests.cs b/src/DnsmasqWebUI.Tests/DnsmasqConfFileLineParserTests.cs index cf236bd..1766955 100644 --- a/src/DnsmasqWebUI.Tests/DnsmasqConfFileLineParserTests.cs +++ b/src/DnsmasqWebUI.Tests/DnsmasqConfFileLineParserTests.cs @@ -1,5 +1,5 @@ -using DnsmasqWebUI.Models; using DnsmasqWebUI.Models.Config; +using DnsmasqWebUI.Models.Dhcp; using DnsmasqWebUI.Parsers; namespace DnsmasqWebUI.Tests; diff --git a/src/DnsmasqWebUI.Tests/DnsmasqConfigSetServiceTests.cs b/src/DnsmasqWebUI.Tests/DnsmasqConfigSetServiceTests.cs index 09b5c56..db47d5f 100644 --- a/src/DnsmasqWebUI.Tests/DnsmasqConfigSetServiceTests.cs +++ b/src/DnsmasqWebUI.Tests/DnsmasqConfigSetServiceTests.cs @@ -1,5 +1,5 @@ using DnsmasqWebUI.Configuration; -using DnsmasqWebUI.Models; +using DnsmasqWebUI.Models.EffectiveConfig; using DnsmasqWebUI.Services; using Microsoft.Extensions.Options; diff --git a/src/DnsmasqWebUI.Tests/DnsmasqLeasesFileLineParserTests.cs b/src/DnsmasqWebUI.Tests/DnsmasqLeasesFileLineParserTests.cs index d976604..40acf91 100644 --- a/src/DnsmasqWebUI.Tests/DnsmasqLeasesFileLineParserTests.cs +++ b/src/DnsmasqWebUI.Tests/DnsmasqLeasesFileLineParserTests.cs @@ -1,4 +1,4 @@ -using DnsmasqWebUI.Models; +using DnsmasqWebUI.Models.Dhcp; using DnsmasqWebUI.Parsers; namespace DnsmasqWebUI.Tests; diff --git a/src/DnsmasqWebUI.Tests/EffectiveDnsmasqConfigTests.cs b/src/DnsmasqWebUI.Tests/EffectiveDnsmasqConfigTests.cs index d27a155..0e778b1 100644 --- a/src/DnsmasqWebUI.Tests/EffectiveDnsmasqConfigTests.cs +++ b/src/DnsmasqWebUI.Tests/EffectiveDnsmasqConfigTests.cs @@ -1,4 +1,4 @@ -using DnsmasqWebUI.Models; +using DnsmasqWebUI.Models.EffectiveConfig; namespace DnsmasqWebUI.Tests; diff --git a/src/DnsmasqWebUI.Tests/HostsFileLineParserTests.cs b/src/DnsmasqWebUI.Tests/HostsFileLineParserTests.cs index bacd01c..3e359a4 100644 --- a/src/DnsmasqWebUI.Tests/HostsFileLineParserTests.cs +++ b/src/DnsmasqWebUI.Tests/HostsFileLineParserTests.cs @@ -1,4 +1,4 @@ -using DnsmasqWebUI.Models; +using DnsmasqWebUI.Models.Hosts; using DnsmasqWebUI.Parsers; namespace DnsmasqWebUI.Tests; diff --git a/src/DnsmasqWebUI/Client/Http/Abstractions/IConfigSetClient.cs b/src/DnsmasqWebUI/Client/Http/Abstractions/IConfigSetClient.cs index 8b94e52..0c54917 100644 --- a/src/DnsmasqWebUI/Client/Http/Abstractions/IConfigSetClient.cs +++ b/src/DnsmasqWebUI/Client/Http/Abstractions/IConfigSetClient.cs @@ -1,4 +1,4 @@ -using DnsmasqWebUI.Models; +using DnsmasqWebUI.Models.EffectiveConfig; namespace DnsmasqWebUI.Client.Http.Abstractions; diff --git a/src/DnsmasqWebUI/Client/Http/Abstractions/IDhcpHostsClient.cs b/src/DnsmasqWebUI/Client/Http/Abstractions/IDhcpHostsClient.cs index 3e12bf3..b0c7214 100644 --- a/src/DnsmasqWebUI/Client/Http/Abstractions/IDhcpHostsClient.cs +++ b/src/DnsmasqWebUI/Client/Http/Abstractions/IDhcpHostsClient.cs @@ -1,4 +1,5 @@ -using DnsmasqWebUI.Models; +using DnsmasqWebUI.Models.Dhcp; +using DnsmasqWebUI.Models.Status; namespace DnsmasqWebUI.Client.Http.Abstractions; diff --git a/src/DnsmasqWebUI/Client/Http/Abstractions/IHostsClient.cs b/src/DnsmasqWebUI/Client/Http/Abstractions/IHostsClient.cs index 5886774..d438886 100644 --- a/src/DnsmasqWebUI/Client/Http/Abstractions/IHostsClient.cs +++ b/src/DnsmasqWebUI/Client/Http/Abstractions/IHostsClient.cs @@ -1,4 +1,5 @@ -using DnsmasqWebUI.Models; +using DnsmasqWebUI.Models.Hosts; +using DnsmasqWebUI.Models.Status; namespace DnsmasqWebUI.Client.Http.Abstractions; diff --git a/src/DnsmasqWebUI/Client/Http/Abstractions/ILeasesClient.cs b/src/DnsmasqWebUI/Client/Http/Abstractions/ILeasesClient.cs index 7c753bf..81b373e 100644 --- a/src/DnsmasqWebUI/Client/Http/Abstractions/ILeasesClient.cs +++ b/src/DnsmasqWebUI/Client/Http/Abstractions/ILeasesClient.cs @@ -1,4 +1,4 @@ -using DnsmasqWebUI.Models; +using DnsmasqWebUI.Models.Dhcp; namespace DnsmasqWebUI.Client.Http.Abstractions; diff --git a/src/DnsmasqWebUI/Client/Http/Abstractions/IStatusClient.cs b/src/DnsmasqWebUI/Client/Http/Abstractions/IStatusClient.cs index db08309..a5ca13e 100644 --- a/src/DnsmasqWebUI/Client/Http/Abstractions/IStatusClient.cs +++ b/src/DnsmasqWebUI/Client/Http/Abstractions/IStatusClient.cs @@ -1,4 +1,4 @@ -using DnsmasqWebUI.Models; +using DnsmasqWebUI.Models.Status; namespace DnsmasqWebUI.Client.Http.Abstractions; diff --git a/src/DnsmasqWebUI/Client/Http/ConfigSetClient.cs b/src/DnsmasqWebUI/Client/Http/ConfigSetClient.cs index 6361939..3113ab4 100644 --- a/src/DnsmasqWebUI/Client/Http/ConfigSetClient.cs +++ b/src/DnsmasqWebUI/Client/Http/ConfigSetClient.cs @@ -1,5 +1,5 @@ using DnsmasqWebUI.Client.Http.Abstractions; -using DnsmasqWebUI.Models; +using DnsmasqWebUI.Models.EffectiveConfig; using System.Net.Http.Json; namespace DnsmasqWebUI.Client.Http; diff --git a/src/DnsmasqWebUI/Client/Http/DhcpHostsClient.cs b/src/DnsmasqWebUI/Client/Http/DhcpHostsClient.cs index aba7a57..366a5c1 100644 --- a/src/DnsmasqWebUI/Client/Http/DhcpHostsClient.cs +++ b/src/DnsmasqWebUI/Client/Http/DhcpHostsClient.cs @@ -1,7 +1,8 @@ using System.Net.Http.Json; using System.Text.Json.Serialization; using DnsmasqWebUI.Client.Http.Abstractions; -using DnsmasqWebUI.Models; +using DnsmasqWebUI.Models.Dhcp; +using DnsmasqWebUI.Models.Status; namespace DnsmasqWebUI.Client.Http; diff --git a/src/DnsmasqWebUI/Client/Http/HostsClient.cs b/src/DnsmasqWebUI/Client/Http/HostsClient.cs index 1d5c02c..7b23fde 100644 --- a/src/DnsmasqWebUI/Client/Http/HostsClient.cs +++ b/src/DnsmasqWebUI/Client/Http/HostsClient.cs @@ -1,5 +1,6 @@ using DnsmasqWebUI.Client.Http.Abstractions; -using DnsmasqWebUI.Models; +using DnsmasqWebUI.Models.Hosts; +using DnsmasqWebUI.Models.Status; using System.Net.Http.Json; namespace DnsmasqWebUI.Client.Http; diff --git a/src/DnsmasqWebUI/Client/Http/LeasesClient.cs b/src/DnsmasqWebUI/Client/Http/LeasesClient.cs index d1c8147..1ea45f9 100644 --- a/src/DnsmasqWebUI/Client/Http/LeasesClient.cs +++ b/src/DnsmasqWebUI/Client/Http/LeasesClient.cs @@ -1,5 +1,5 @@ using DnsmasqWebUI.Client.Http.Abstractions; -using DnsmasqWebUI.Models; +using DnsmasqWebUI.Models.Dhcp; using System.Net.Http.Json; namespace DnsmasqWebUI.Client.Http; diff --git a/src/DnsmasqWebUI/Client/Http/StatusClient.cs b/src/DnsmasqWebUI/Client/Http/StatusClient.cs index d925150..e5fa151 100644 --- a/src/DnsmasqWebUI/Client/Http/StatusClient.cs +++ b/src/DnsmasqWebUI/Client/Http/StatusClient.cs @@ -1,6 +1,6 @@ using System.Text.Json; using DnsmasqWebUI.Client.Http.Abstractions; -using DnsmasqWebUI.Models; +using DnsmasqWebUI.Models.Status; using System.Net.Http.Json; namespace DnsmasqWebUI.Client.Http; diff --git a/src/DnsmasqWebUI/Components/EffectiveConfig/EffectiveConfigMultiValueRow.razor b/src/DnsmasqWebUI/Components/Dnsmasq/EffectiveConfig/EffectiveConfigMultiValueRow.razor similarity index 74% rename from src/DnsmasqWebUI/Components/EffectiveConfig/EffectiveConfigMultiValueRow.razor rename to src/DnsmasqWebUI/Components/Dnsmasq/EffectiveConfig/EffectiveConfigMultiValueRow.razor index 216fd42..fcd71af 100644 --- a/src/DnsmasqWebUI/Components/EffectiveConfig/EffectiveConfigMultiValueRow.razor +++ b/src/DnsmasqWebUI/Components/Dnsmasq/EffectiveConfig/EffectiveConfigMultiValueRow.razor @@ -1,4 +1,4 @@ -@namespace DnsmasqWebUI.Components.EffectiveConfig +@namespace DnsmasqWebUI.Components.Dnsmasq.EffectiveConfig
  • @Label: @Content diff --git a/src/DnsmasqWebUI/Components/EffectiveConfig/EffectiveConfigOptionItem.razor b/src/DnsmasqWebUI/Components/Dnsmasq/EffectiveConfig/EffectiveConfigOptionItem.razor similarity index 87% rename from src/DnsmasqWebUI/Components/EffectiveConfig/EffectiveConfigOptionItem.razor rename to src/DnsmasqWebUI/Components/Dnsmasq/EffectiveConfig/EffectiveConfigOptionItem.razor index 9fbc7e2..6ecbbe3 100644 --- a/src/DnsmasqWebUI/Components/EffectiveConfig/EffectiveConfigOptionItem.razor +++ b/src/DnsmasqWebUI/Components/Dnsmasq/EffectiveConfig/EffectiveConfigOptionItem.razor @@ -1,4 +1,4 @@ -@namespace DnsmasqWebUI.Components.EffectiveConfig +@namespace DnsmasqWebUI.Components.Dnsmasq.EffectiveConfig
  • @Label: @DisplayValue diff --git a/src/DnsmasqWebUI/Components/Dnsmasq/EffectiveConfig/EffectiveConfigSection.razor b/src/DnsmasqWebUI/Components/Dnsmasq/EffectiveConfig/EffectiveConfigSection.razor new file mode 100644 index 0000000..4cf33a5 --- /dev/null +++ b/src/DnsmasqWebUI/Components/Dnsmasq/EffectiveConfig/EffectiveConfigSection.razor @@ -0,0 +1,178 @@ +@namespace DnsmasqWebUI.Components.Dnsmasq.EffectiveConfig +@using DnsmasqWebUI.Models.Status +@using DnsmasqWebUI.Models.EffectiveConfig + +@if (Status != null) +{ +
    + @* Hosts *@ +
    + +
    +
    +
      + +
    • + addn-hosts: + @if (Status.AddnHostsPaths?.Count > 0) + { + var srcList = Status.EffectiveConfigSources?.AddnHostsPaths; + if (srcList != null && srcList.Count == Status.AddnHostsPaths.Count) + { + @foreach (var entry in srcList) + { + @entry.Path @(entry.Source?.IsReadOnly == true ? " (readonly)" : "") + } + } + else + { + var deduped = (Status.AddnHostsPaths ?? Array.Empty()).Distinct().ToList(); + @(deduped.Count == 1 ? deduped[0] : string.Join(", ", deduped)) + } + } + else { (none) } +
    • + @if (Status.ManagedHostsFilePath != null) {
    • Managed hosts file (editable): @Status.ManagedHostsFilePath @(Status.ManagedHostsPathExists ? "✓" : "(not created yet)")
    • } + @if (Status.SystemHostsPath != null) {
    • System hosts (read-only): @Status.SystemHostsPath @(Status.SystemHostsPathExists ? "✓" : "(missing)")
    • } +
    +
    +
    +
    + + @if (Status.EffectiveConfig != null) + { + var ec = Status.EffectiveConfig; + var src = Status.EffectiveConfigSources; + + @* Resolver / DNS *@ +
    + +
    +
    +
      + + + + + + "0 (no DNS listener)", _ => ec.Port.Value.ToString() })" Source="@(src?.Port)" /> + @if (ec.ServerLocalValues?.Count > 0) { } + @if (ec.AddressValues?.Count > 0) { } + @if (ec.ResolvFiles?.Count > 0) { } +
    +
    +
    +
    + + @* DHCP *@ +
    + +
    +
    +
      + + + + @if (ec.DhcpLeaseMax.HasValue) { } + @if (ec.DhcpTtl.HasValue) { } + @if (ec.DhcpRanges?.Count > 0) { } + @if (ec.DhcpHostLines?.Count > 0) { } + @if (ec.DhcpOptionLines?.Count > 0) { } +
    +
    +
    +
    + + @* Cache *@ +
    + +
    +
    +
      + @if (ec.CacheSize.HasValue) { } + @if (ec.LocalTtl.HasValue) { } + + @if (ec.NegTtl.HasValue) { } + @if (ec.MaxTtl.HasValue) { } + @if (ec.MaxCacheTtl.HasValue) { } + @if (ec.MinCacheTtl.HasValue) { } +
    +
    +
    +
    + + @* Process & networking *@ +
    + +
    +
    +
      + + + @if (ec.Interfaces?.Count > 0) { } + @if (ec.ListenAddresses?.Count > 0) { } + @if (ec.ExceptInterfaces?.Count > 0) { } + @if (ec.PidFilePath != null) { } + @if (ec.User != null) { } + @if (ec.Group != null) { } + @if (ec.LogFacility != null) { } +
    +
    +
    +
    + } +
    +} + +@code { + [Parameter] public DnsmasqServiceStatus? Status { get; set; } + + private HashSet _openPanels = new(); + + private bool IsOpen(string id) => _openPanels.Contains(id); + + private void TogglePanel(string id) + { + if (_openPanels.Contains(id)) + _openPanels.Remove(id); + else + _openPanels.Add(id); + } + + private static MarkupString RenderMultiValueList(IReadOnlyList? values, IReadOnlyList? sources) + { + if (values == null || values.Count == 0) + return (MarkupString)"(none)"; + if (sources != null && sources.Count == values.Count) + { + var parts = sources.Select(e => + { + var s = e.Source; + var valueEnc = System.Net.WebUtility.HtmlEncode(e.Value); + if (s?.IsReadOnly == true) + { + var tipEnc = System.Net.WebUtility.HtmlEncode(s.GetReadOnlyTooltip() ?? ""); + return $"{valueEnc} readonly"; + } + return valueEnc; + }); + return (MarkupString)string.Join(", ", parts); + } + return (MarkupString)System.Net.WebUtility.HtmlEncode(string.Join(", ", values)); + } +} diff --git a/src/DnsmasqWebUI/Components/Dnsmasq/EffectiveConfig/EffectiveConfigSection.razor.css b/src/DnsmasqWebUI/Components/Dnsmasq/EffectiveConfig/EffectiveConfigSection.razor.css new file mode 100644 index 0000000..f8ea30c --- /dev/null +++ b/src/DnsmasqWebUI/Components/Dnsmasq/EffectiveConfig/EffectiveConfigSection.razor.css @@ -0,0 +1,79 @@ +/* Effective config sections – mobile-first, touch-friendly */ +.ec-sections { + display: flex; + flex-direction: column; + gap: 0; +} + +.ec-section { + border-bottom: 1px solid var(--bs-border-color, #dee2e6); +} + +.ec-section:last-child { + border-bottom: none; +} + +/* Touch-friendly header: min 44px tap target (WCAG 2.5.5) */ +.ec-section-header { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + min-height: 2.75rem; + padding: 0.4rem 0; + margin: 0; + border: none; + background: transparent; + color: inherit; + text-align: left; + cursor: pointer; + font-size: inherit; + line-height: 1.3; + -webkit-tap-highlight-color: transparent; +} + +.ec-section-header:hover { + color: var(--bs-primary, #0d6efd); +} + +.ec-section-header:focus-visible { + outline: 2px solid var(--bs-primary, #0d6efd); + outline-offset: 2px; +} + +.ec-section-title { + flex: 1; + min-width: 0; +} + +.ec-section-chevron { + flex-shrink: 0; + font-size: 0.75rem; + opacity: 0.7; + margin-left: 0.25rem; +} + +.ec-section-body { + padding: 0.25rem 0 0.5rem 0; + padding-left: 0.5rem; + border-left: 2px solid var(--bs-border-color-translucent, rgba(0, 0, 0, 0.075)); + overflow-wrap: break-word; + word-break: break-word; +} + +/* Long paths/values wrap on small screens; badges stay inline */ +.ec-section-body ul { + overflow-wrap: break-word; + word-break: break-word; +} + +.ec-section-body li { + overflow-wrap: break-word; + word-break: break-word; +} + +@media (max-width: 767.98px) { + .ec-section-body { + padding-left: 0.35rem; + } +} diff --git a/src/DnsmasqWebUI/Components/Home/LogsSection.razor b/src/DnsmasqWebUI/Components/Dnsmasq/LogsSection.razor similarity index 100% rename from src/DnsmasqWebUI/Components/Home/LogsSection.razor rename to src/DnsmasqWebUI/Components/Dnsmasq/LogsSection.razor diff --git a/src/DnsmasqWebUI/Components/Home/LogsSection.razor.cs b/src/DnsmasqWebUI/Components/Dnsmasq/LogsSection.razor.cs similarity index 98% rename from src/DnsmasqWebUI/Components/Home/LogsSection.razor.cs rename to src/DnsmasqWebUI/Components/Dnsmasq/LogsSection.razor.cs index 8b4b54e..a06729e 100644 --- a/src/DnsmasqWebUI/Components/Home/LogsSection.razor.cs +++ b/src/DnsmasqWebUI/Components/Dnsmasq/LogsSection.razor.cs @@ -1,5 +1,5 @@ using DnsmasqWebUI.Client.Http.Abstractions; -using DnsmasqWebUI.Models; +using DnsmasqWebUI.Models.Status; using Microsoft.AspNetCore.Components; namespace DnsmasqWebUI.Components.Dnsmasq; diff --git a/src/DnsmasqWebUI/Components/Home/StatusSection.razor b/src/DnsmasqWebUI/Components/Dnsmasq/StatusSection.razor similarity index 69% rename from src/DnsmasqWebUI/Components/Home/StatusSection.razor rename to src/DnsmasqWebUI/Components/Dnsmasq/StatusSection.razor index 1748e1a..c5e11e2 100644 --- a/src/DnsmasqWebUI/Components/Home/StatusSection.razor +++ b/src/DnsmasqWebUI/Components/Dnsmasq/StatusSection.razor @@ -1,9 +1,15 @@ @namespace DnsmasqWebUI.Components.Dnsmasq @* Service status — output of StatusShowCommand; only this component re-renders on its timer *@ -@if (_status?.StatusShowConfigured == true) +@if (_status != null && _status.StatusShowConfigured) { -

    - Service status +

    + + Service status + @{ + var isActive = _status.DnsmasqStatus == "active"; + } + + diff --git a/src/DnsmasqWebUI/Components/Home/StatusSection.razor.cs b/src/DnsmasqWebUI/Components/Dnsmasq/StatusSection.razor.cs similarity index 98% rename from src/DnsmasqWebUI/Components/Home/StatusSection.razor.cs rename to src/DnsmasqWebUI/Components/Dnsmasq/StatusSection.razor.cs index c1742f2..662b23a 100644 --- a/src/DnsmasqWebUI/Components/Home/StatusSection.razor.cs +++ b/src/DnsmasqWebUI/Components/Dnsmasq/StatusSection.razor.cs @@ -1,5 +1,5 @@ using DnsmasqWebUI.Client.Http.Abstractions; -using DnsmasqWebUI.Models; +using DnsmasqWebUI.Models.Status; using Microsoft.AspNetCore.Components; namespace DnsmasqWebUI.Components.Dnsmasq; diff --git a/src/DnsmasqWebUI/Components/Hosts/HostsFileSection.razor b/src/DnsmasqWebUI/Components/Hosts/HostsFileSection.razor index 223aaee..8577b0a 100644 --- a/src/DnsmasqWebUI/Components/Hosts/HostsFileSection.razor +++ b/src/DnsmasqWebUI/Components/Hosts/HostsFileSection.razor @@ -1,3 +1,4 @@ +@namespace DnsmasqWebUI.Components.Hosts

    @Path @@ -17,27 +18,41 @@ #AddressNames - @foreach (var e in (Entries ?? Array.Empty()).Where(x => !x.IsPassthrough)) + @if (_visibleEntries.Count == 0) { - - @e.LineNumber - @e.Address - @string.Join(", ", e.Names) - + No host entries. + } + else + { + @foreach (var e in _visibleEntries) + { + + @e.LineNumber + @e.Address + @string.Join(", ", e.Names) + + } }

    - @foreach (var e in (Entries ?? Array.Empty()).Where(x => !x.IsPassthrough)) + @if (_visibleEntries.Count == 0) { -
    -
    -
    # @e.LineNumber
    -
    Address @e.Address
    -
    Names @string.Join(", ", e.Names)
    +

    No host entries.

    + } + else + { + @foreach (var e in _visibleEntries) + { +
    +
    +
    # @e.LineNumber
    +
    Address @e.Address
    +
    Names @string.Join(", ", e.Names)
    +
    -
    + } }
    @@ -48,4 +63,11 @@ [Parameter] public IReadOnlyList? Entries { get; set; } [Parameter] public bool IsEditable { get; set; } [Parameter] public bool IsFirst { get; set; } + + private IReadOnlyList _visibleEntries = Array.Empty(); + + protected override void OnParametersSet() + { + _visibleEntries = (Entries ?? Array.Empty()).Where(x => !x.IsPassthrough).ToList(); + } } diff --git a/src/DnsmasqWebUI/Components/Hosts/_Imports.razor b/src/DnsmasqWebUI/Components/Hosts/_Imports.razor deleted file mode 100644 index 9246dd6..0000000 --- a/src/DnsmasqWebUI/Components/Hosts/_Imports.razor +++ /dev/null @@ -1 +0,0 @@ -@namespace DnsmasqWebUI.Components.Hosts diff --git a/src/DnsmasqWebUI/Components/Pages/Dnsmasq.razor b/src/DnsmasqWebUI/Components/Pages/Dnsmasq.razor index cb95622..29def12 100644 --- a/src/DnsmasqWebUI/Components/Pages/Dnsmasq.razor +++ b/src/DnsmasqWebUI/Components/Pages/Dnsmasq.razor @@ -21,18 +21,33 @@ else if (_error != null) else if (_status != null) {
    -

    Service

    -
    - Dnsmasq: @GetStatusLabel(_status) - @if (_status.StatusCommandExitCode.HasValue) - { - (exit @_status.StatusCommandExitCode.Value) - } - @if (!string.IsNullOrEmpty(_status.StatusCommandStderr)) - { - — @_status.StatusCommandStderr - } -
    +

    Service configuration

    +
      +
    • Main config: @(_status.MainConfigPath ?? "—") @(_status.MainConfigPathExists ? "✓" : "(missing)")
    • +
    • Managed file: @(_status.ManagedFilePath ?? "—") @(_status.ManagedFilePathExists ? "✓" : "(not created yet)")
    • +
    + @if (_configSet != null && _configSet.Files?.Count > 0) + { +

    Config set (load order):

    +
      + @foreach (var f in _configSet.Files) + { +
    • + @if (f.IsManaged) + { + managed + } + else + { + @(f.Source.ToString().ToLowerInvariant()) + } + @f.FileName + (@f.Path) +
    • + } +
    + } + @if (_status.DnsmasqStatus != "active" && (_status.StatusCommandStdout != null || _status.StatusCommandStderr != null)) {
    @(_status.StatusCommandStdout != null ? _status.StatusCommandStdout : "")@(_status.StatusCommandStdout != null && _status.StatusCommandStderr != null ? "\n" : "")@(_status.StatusCommandStderr ?? "")
    @@ -63,135 +78,6 @@ else if (_status != null)
    - -
    -

    Effective config

    -

    What dnsmasq uses after reading all config files (main + conf-file + conf-dir). Values from a non-managed file are readonly (edit that file to change).

    -
      - -
    • - addn-hosts: - @if (_status.AddnHostsPaths?.Count > 0) - { - var srcList = _status.EffectiveConfigSources?.AddnHostsPaths; - if (srcList != null && srcList.Count == _status.AddnHostsPaths.Count) - { - @foreach (var entry in srcList) - { - @entry.Path @(entry.Source?.IsReadOnly == true ? " (readonly)" : "") - } - } - else - { - var deduped = (_status.AddnHostsPaths ?? Array.Empty()).Distinct().ToList(); - @(deduped.Count == 1 ? deduped[0] : string.Join(", ", deduped)) - } - } - else - { - (none) - } -
    • - @if (_status.ManagedHostsFilePath != null) - { -
    • Managed hosts file (editable): @_status.ManagedHostsFilePath @(_status.ManagedHostsPathExists ? "✓" : "(not created yet)")
    • - } - @if (_status.SystemHostsPath != null) - { -
    • System hosts (read-only): @_status.SystemHostsPath @(_status.SystemHostsPathExists ? "✓" : "(missing)")
    • - } -
    - @if (_status.EffectiveConfig != null) - { - var ec = _status.EffectiveConfig; - var src = _status.EffectiveConfigSources; -

    Options (last wins for single-value; readonly when from a non-managed file):

    -
      - - - - - - - - - - - - @if (ec.CacheSize.HasValue) { } - "0 (no DNS listener)", _ => ec.Port.Value.ToString() })" Source="@(src?.Port)" /> - @if (ec.LocalTtl.HasValue) { } - @if (ec.PidFilePath != null) { } - @if (ec.User != null) { } - @if (ec.Group != null) { } - @if (ec.LogFacility != null) { } - @if (ec.DhcpLeaseMax.HasValue) { } - @if (ec.NegTtl.HasValue) { } - @if (ec.MaxTtl.HasValue) { } - @if (ec.MaxCacheTtl.HasValue) { } - @if (ec.MinCacheTtl.HasValue) { } - @if (ec.DhcpTtl.HasValue) { } -
    -

    Multi-value options (server/local, address, interface, etc.; each with source when known):

    -
      - @if (ec.ServerLocalValues?.Count > 0) { } - @if (ec.AddressValues?.Count > 0) { } - @if (ec.Interfaces?.Count > 0) { } - @if (ec.ListenAddresses?.Count > 0) { } - @if (ec.ExceptInterfaces?.Count > 0) { } - @if (ec.DhcpRanges?.Count > 0) { } - @if (ec.DhcpHostLines?.Count > 0) { } - @if (ec.DhcpOptionLines?.Count > 0) { } - @if (ec.ResolvFiles?.Count > 0) { } -
    - } -
    - -
    -

    Config

    -
      -
    • Main config: @(_status.MainConfigPath ?? "—") @(_status.MainConfigPathExists ? "✓" : "(missing)")
    • -
    • Managed file: @(_status.ManagedFilePath ?? "—") @(_status.ManagedFilePathExists ? "✓" : "(not created yet)")
    • -
    - @if (_configSet != null && _configSet.Files?.Count > 0) - { -

    Config set (load order):

    -
      - @foreach (var f in _configSet.Files) - { -
    • - @if (f.IsManaged) - { - managed - } - else - { - @(f.Source.ToString().ToLowerInvariant()) - } - @f.FileName - (@f.Path) -
    • - } -
    - } -
    - -
    -

    Paths

    -
      -
    • - Leases file: @(_status.LeasesPath ?? "—") - @if (_status.LeasesPathConfigured) - { - @(_status.LeasesPathExists ? "✓" : "(missing)") - } - else - { - (not configured) - } -
    • -
    -
    } @code { @@ -254,18 +140,6 @@ else if (_status != null) private void OpenRecentLogsSettings() => OpenSettingsModal(SettingsModalContext.LogsPolling, "Recent logs polling"); - private static string GetStatusLabel(DnsmasqServiceStatus s) - { - return s.DnsmasqStatus switch - { - "active" => "Running", - "inactive" => "Not running", - "unknown" => "Unknown", - "notConfigured" => "Status check not configured", - _ => s.DnsmasqStatus ?? "—" - }; - } - private async Task ReloadDnsmasq() { if (_status == null || !_status.ReloadCommandConfigured || _reloading) return; @@ -331,25 +205,6 @@ else if (_status != null) StateHasChanged(); } - /// Renders a multi-value list with per-value source (readonly/editable + tooltip) when EffectiveConfigSources matches; otherwise comma-separated. Returns MarkupString so Blazor renders raw HTML. - private static MarkupString RenderMultiValueList(IReadOnlyList? values, IReadOnlyList? sources) - { - if (values == null || values.Count == 0) - return (MarkupString)"(none)"; - if (sources != null && sources.Count == values.Count) - { - var parts = sources.Select(e => - { - var src = e.Source; - var tip = src?.GetReadOnlyTooltip() ?? ""; - var label = src == null ? "?" : (src.IsReadOnly ? "from " + src.FileName + ", readonly" : "editable"); - return $"{System.Net.WebUtility.HtmlEncode(e.Value)} ({System.Net.WebUtility.HtmlEncode(label)})"; - }); - return (MarkupString)string.Join(", ", parts); - } - return (MarkupString)System.Net.WebUtility.HtmlEncode(string.Join(", ", values)); - } - public void Dispose() { _cts.Cancel(); diff --git a/src/DnsmasqWebUI/Components/Pages/Hosts.razor b/src/DnsmasqWebUI/Components/Pages/Hosts.razor index 1310192..85ae12e 100644 --- a/src/DnsmasqWebUI/Components/Pages/Hosts.razor +++ b/src/DnsmasqWebUI/Components/Pages/Hosts.razor @@ -38,10 +38,7 @@ else
    @_message
    } -

    Each hosts file below is rendered with the same component; only the managed file is editable. System hosts is excluded when no-hosts is set (dnsmasq does not read it then).

    - -

    @_status.ManagedHostsFilePath @(!_status.ManagedHostsPathExists ? "(not created yet; will be created on first save)" : "")

    @if (_readOnlyHosts?.Count > 0) diff --git a/src/DnsmasqWebUI/Components/_Imports.razor b/src/DnsmasqWebUI/Components/_Imports.razor index 5bdbf7a..e9fa274 100644 --- a/src/DnsmasqWebUI/Components/_Imports.razor +++ b/src/DnsmasqWebUI/Components/_Imports.razor @@ -11,10 +11,14 @@ @using DnsmasqWebUI.Client.Services.Abstractions @using DnsmasqWebUI.Components @using DnsmasqWebUI.Components.Modals -@using DnsmasqWebUI.Components.EffectiveConfig -@using DnsmasqWebUI.Components.Hosts @using DnsmasqWebUI.Components.Dnsmasq +@using DnsmasqWebUI.Components.Dnsmasq.EffectiveConfig +@using DnsmasqWebUI.Components.Hosts @using DnsmasqWebUI.Models +@using DnsmasqWebUI.Models.EffectiveConfig +@using DnsmasqWebUI.Models.Hosts +@using DnsmasqWebUI.Models.Dhcp +@using DnsmasqWebUI.Models.Status @using DnsmasqWebUI.Configuration @using DnsmasqWebUI.Client.Http.Abstractions @using DnsmasqWebUI.Services.Abstractions diff --git a/src/DnsmasqWebUI/Controllers/ConfigController.cs b/src/DnsmasqWebUI/Controllers/ConfigController.cs index d33aed6..6532090 100644 --- a/src/DnsmasqWebUI/Controllers/ConfigController.cs +++ b/src/DnsmasqWebUI/Controllers/ConfigController.cs @@ -1,5 +1,5 @@ -using DnsmasqWebUI.Models; using DnsmasqWebUI.Models.Config; +using DnsmasqWebUI.Models.EffectiveConfig; using DnsmasqWebUI.Services.Abstractions; using Microsoft.AspNetCore.Mvc; diff --git a/src/DnsmasqWebUI/Controllers/DhcpController.cs b/src/DnsmasqWebUI/Controllers/DhcpController.cs index c88e563..3b72b58 100644 --- a/src/DnsmasqWebUI/Controllers/DhcpController.cs +++ b/src/DnsmasqWebUI/Controllers/DhcpController.cs @@ -1,4 +1,5 @@ -using DnsmasqWebUI.Models; +using DnsmasqWebUI.Models.Dhcp; +using DnsmasqWebUI.Models.Status; using DnsmasqWebUI.Services.Abstractions; using Microsoft.AspNetCore.Mvc; diff --git a/src/DnsmasqWebUI/Controllers/HostsController.cs b/src/DnsmasqWebUI/Controllers/HostsController.cs index a2d6232..36b471e 100644 --- a/src/DnsmasqWebUI/Controllers/HostsController.cs +++ b/src/DnsmasqWebUI/Controllers/HostsController.cs @@ -1,6 +1,8 @@ using System.Text; using DnsmasqWebUI.Configuration; -using DnsmasqWebUI.Models; +using DnsmasqWebUI.Models.EffectiveConfig; +using DnsmasqWebUI.Models.Hosts; +using DnsmasqWebUI.Models.Status; using DnsmasqWebUI.Parsers; using DnsmasqWebUI.Services.Abstractions; using Microsoft.AspNetCore.Mvc; diff --git a/src/DnsmasqWebUI/Controllers/LeasesController.cs b/src/DnsmasqWebUI/Controllers/LeasesController.cs index 965c0a0..d86b03c 100644 --- a/src/DnsmasqWebUI/Controllers/LeasesController.cs +++ b/src/DnsmasqWebUI/Controllers/LeasesController.cs @@ -1,4 +1,4 @@ -using DnsmasqWebUI.Models; +using DnsmasqWebUI.Models.Dhcp; using DnsmasqWebUI.Services.Abstractions; using Microsoft.AspNetCore.Mvc; diff --git a/src/DnsmasqWebUI/Controllers/StatusController.cs b/src/DnsmasqWebUI/Controllers/StatusController.cs index 9fff292..5e3e7e1 100644 --- a/src/DnsmasqWebUI/Controllers/StatusController.cs +++ b/src/DnsmasqWebUI/Controllers/StatusController.cs @@ -1,4 +1,5 @@ -using DnsmasqWebUI.Models; +using DnsmasqWebUI.Models.EffectiveConfig; +using DnsmasqWebUI.Models.Status; using DnsmasqWebUI.Configuration; using DnsmasqWebUI.Services.Abstractions; using Microsoft.AspNetCore.Mvc; diff --git a/src/DnsmasqWebUI/Models/Config/DnsmasqConfLine.cs b/src/DnsmasqWebUI/Models/Config/DnsmasqConfLine.cs index b9fa2cf..3276208 100644 --- a/src/DnsmasqWebUI/Models/Config/DnsmasqConfLine.cs +++ b/src/DnsmasqWebUI/Models/Config/DnsmasqConfLine.cs @@ -1,5 +1,5 @@ using System.Text.Json.Serialization; -using DnsmasqWebUI.Models; +using DnsmasqWebUI.Models.Dhcp; namespace DnsmasqWebUI.Models.Config; diff --git a/src/DnsmasqWebUI/Models/DhcpHostEntry.cs b/src/DnsmasqWebUI/Models/Dhcp/DhcpHostEntry.cs similarity index 97% rename from src/DnsmasqWebUI/Models/DhcpHostEntry.cs rename to src/DnsmasqWebUI/Models/Dhcp/DhcpHostEntry.cs index 82ce104..cec86db 100644 --- a/src/DnsmasqWebUI/Models/DhcpHostEntry.cs +++ b/src/DnsmasqWebUI/Models/Dhcp/DhcpHostEntry.cs @@ -1,4 +1,4 @@ -namespace DnsmasqWebUI.Models; +namespace DnsmasqWebUI.Models.Dhcp; public class DhcpHostEntry { diff --git a/src/DnsmasqWebUI/Models/LeaseEntry.cs b/src/DnsmasqWebUI/Models/Dhcp/LeaseEntry.cs similarity index 90% rename from src/DnsmasqWebUI/Models/LeaseEntry.cs rename to src/DnsmasqWebUI/Models/Dhcp/LeaseEntry.cs index cfd0cf9..bb06479 100644 --- a/src/DnsmasqWebUI/Models/LeaseEntry.cs +++ b/src/DnsmasqWebUI/Models/Dhcp/LeaseEntry.cs @@ -1,4 +1,4 @@ -namespace DnsmasqWebUI.Models; +namespace DnsmasqWebUI.Models.Dhcp; public class LeaseEntry { diff --git a/src/DnsmasqWebUI/Models/LeasesResult.cs b/src/DnsmasqWebUI/Models/Dhcp/LeasesResult.cs similarity index 84% rename from src/DnsmasqWebUI/Models/LeasesResult.cs rename to src/DnsmasqWebUI/Models/Dhcp/LeasesResult.cs index 41f5ca6..39c6c99 100644 --- a/src/DnsmasqWebUI/Models/LeasesResult.cs +++ b/src/DnsmasqWebUI/Models/Dhcp/LeasesResult.cs @@ -1,4 +1,4 @@ -namespace DnsmasqWebUI.Models; +namespace DnsmasqWebUI.Models.Dhcp; /// Result of GET api/leases: whether leases are available and the list of entries. public record LeasesResult(bool Available, IReadOnlyList? Entries, string? Message); diff --git a/src/DnsmasqWebUI/Models/ConfigValueSource.cs b/src/DnsmasqWebUI/Models/EffectiveConfig/ConfigValueSource.cs similarity index 97% rename from src/DnsmasqWebUI/Models/ConfigValueSource.cs rename to src/DnsmasqWebUI/Models/EffectiveConfig/ConfigValueSource.cs index 05db0c0..b8ddaba 100644 --- a/src/DnsmasqWebUI/Models/ConfigValueSource.cs +++ b/src/DnsmasqWebUI/Models/EffectiveConfig/ConfigValueSource.cs @@ -1,4 +1,4 @@ -namespace DnsmasqWebUI.Models; +namespace DnsmasqWebUI.Models.EffectiveConfig; /// /// Where an effective config value came from (which file). Used so the UI can show exactly which file diff --git a/src/DnsmasqWebUI/Models/DnsmasqConfFileSource.cs b/src/DnsmasqWebUI/Models/EffectiveConfig/DnsmasqConfFileSource.cs similarity index 79% rename from src/DnsmasqWebUI/Models/DnsmasqConfFileSource.cs rename to src/DnsmasqWebUI/Models/EffectiveConfig/DnsmasqConfFileSource.cs index da8cdae..13921bd 100644 --- a/src/DnsmasqWebUI/Models/DnsmasqConfFileSource.cs +++ b/src/DnsmasqWebUI/Models/EffectiveConfig/DnsmasqConfFileSource.cs @@ -1,4 +1,4 @@ -namespace DnsmasqWebUI.Models; +namespace DnsmasqWebUI.Models.EffectiveConfig; /// Source of a file in the dnsmasq config set: main config, conf-file=, or conf-dir=. public enum DnsmasqConfFileSource diff --git a/src/DnsmasqWebUI/Models/DnsmasqConfigSet.cs b/src/DnsmasqWebUI/Models/EffectiveConfig/DnsmasqConfigSet.cs similarity index 88% rename from src/DnsmasqWebUI/Models/DnsmasqConfigSet.cs rename to src/DnsmasqWebUI/Models/EffectiveConfig/DnsmasqConfigSet.cs index 97776ff..80babae 100644 --- a/src/DnsmasqWebUI/Models/DnsmasqConfigSet.cs +++ b/src/DnsmasqWebUI/Models/EffectiveConfig/DnsmasqConfigSet.cs @@ -1,4 +1,4 @@ -namespace DnsmasqWebUI.Models; +namespace DnsmasqWebUI.Models.EffectiveConfig; /// Ordered set of dnsmasq config files (main + conf-file + conf-dir). ManagedFilePath is the single config file we read/write; ManagedHostsFilePath is the single hosts file we read/write. public record DnsmasqConfigSet( diff --git a/src/DnsmasqWebUI/Models/DnsmasqConfigSetEntry.cs b/src/DnsmasqWebUI/Models/EffectiveConfig/DnsmasqConfigSetEntry.cs similarity index 83% rename from src/DnsmasqWebUI/Models/DnsmasqConfigSetEntry.cs rename to src/DnsmasqWebUI/Models/EffectiveConfig/DnsmasqConfigSetEntry.cs index 926a186..d198ccd 100644 --- a/src/DnsmasqWebUI/Models/DnsmasqConfigSetEntry.cs +++ b/src/DnsmasqWebUI/Models/EffectiveConfig/DnsmasqConfigSetEntry.cs @@ -1,4 +1,4 @@ -namespace DnsmasqWebUI.Models; +namespace DnsmasqWebUI.Models.EffectiveConfig; /// One file in the dnsmasq config set (main or included). IsManaged is true only for the app-managed file. public record DnsmasqConfigSetEntry(string Path, string FileName, DnsmasqConfFileSource Source, bool IsManaged); diff --git a/src/DnsmasqWebUI/Models/EffectiveConfigSources.cs b/src/DnsmasqWebUI/Models/EffectiveConfig/EffectiveConfigSources.cs similarity index 98% rename from src/DnsmasqWebUI/Models/EffectiveConfigSources.cs rename to src/DnsmasqWebUI/Models/EffectiveConfig/EffectiveConfigSources.cs index f2b3de0..8ee3392 100644 --- a/src/DnsmasqWebUI/Models/EffectiveConfigSources.cs +++ b/src/DnsmasqWebUI/Models/EffectiveConfig/EffectiveConfigSources.cs @@ -1,4 +1,4 @@ -namespace DnsmasqWebUI.Models; +namespace DnsmasqWebUI.Models.EffectiveConfig; /// /// Source file for each field in . diff --git a/src/DnsmasqWebUI/Models/EffectiveDnsmasqConfig.cs b/src/DnsmasqWebUI/Models/EffectiveConfig/EffectiveDnsmasqConfig.cs similarity index 98% rename from src/DnsmasqWebUI/Models/EffectiveDnsmasqConfig.cs rename to src/DnsmasqWebUI/Models/EffectiveConfig/EffectiveDnsmasqConfig.cs index 4a5c2a3..c369f05 100644 --- a/src/DnsmasqWebUI/Models/EffectiveDnsmasqConfig.cs +++ b/src/DnsmasqWebUI/Models/EffectiveConfig/EffectiveDnsmasqConfig.cs @@ -1,4 +1,4 @@ -namespace DnsmasqWebUI.Models; +namespace DnsmasqWebUI.Models.EffectiveConfig; /// /// Effective dnsmasq config after reading all config files (main + conf-file + conf-dir) diff --git a/src/DnsmasqWebUI/Models/HostEntry.cs b/src/DnsmasqWebUI/Models/Hosts/HostEntry.cs similarity index 94% rename from src/DnsmasqWebUI/Models/HostEntry.cs rename to src/DnsmasqWebUI/Models/Hosts/HostEntry.cs index 4dda033..faeea3e 100644 --- a/src/DnsmasqWebUI/Models/HostEntry.cs +++ b/src/DnsmasqWebUI/Models/Hosts/HostEntry.cs @@ -1,4 +1,4 @@ -namespace DnsmasqWebUI.Models; +namespace DnsmasqWebUI.Models.Hosts; public class HostEntry { diff --git a/src/DnsmasqWebUI/Models/ReadOnlyHostsFile.cs b/src/DnsmasqWebUI/Models/Hosts/ReadOnlyHostsFile.cs similarity index 83% rename from src/DnsmasqWebUI/Models/ReadOnlyHostsFile.cs rename to src/DnsmasqWebUI/Models/Hosts/ReadOnlyHostsFile.cs index 3f065d5..a1bfc00 100644 --- a/src/DnsmasqWebUI/Models/ReadOnlyHostsFile.cs +++ b/src/DnsmasqWebUI/Models/Hosts/ReadOnlyHostsFile.cs @@ -1,4 +1,4 @@ -namespace DnsmasqWebUI.Models; +namespace DnsmasqWebUI.Models.Hosts; /// Path and parsed entries for a read-only addn-hosts file (not the managed hosts file). public record ReadOnlyHostsFile(string Path, IReadOnlyList Entries); diff --git a/src/DnsmasqWebUI/Models/DnsmasqServiceStatus.cs b/src/DnsmasqWebUI/Models/Status/DnsmasqServiceStatus.cs similarity index 98% rename from src/DnsmasqWebUI/Models/DnsmasqServiceStatus.cs rename to src/DnsmasqWebUI/Models/Status/DnsmasqServiceStatus.cs index 0700129..6bbd4b2 100644 --- a/src/DnsmasqWebUI/Models/DnsmasqServiceStatus.cs +++ b/src/DnsmasqWebUI/Models/Status/DnsmasqServiceStatus.cs @@ -1,4 +1,6 @@ -namespace DnsmasqWebUI.Models; +using DnsmasqWebUI.Models.EffectiveConfig; + +namespace DnsmasqWebUI.Models.Status; /// /// Dnsmasq service and config status returned by GET api/status. diff --git a/src/DnsmasqWebUI/Models/SaveWithReloadResult.cs b/src/DnsmasqWebUI/Models/Status/SaveWithReloadResult.cs similarity index 86% rename from src/DnsmasqWebUI/Models/SaveWithReloadResult.cs rename to src/DnsmasqWebUI/Models/Status/SaveWithReloadResult.cs index 3307a4a..39b14e3 100644 --- a/src/DnsmasqWebUI/Models/SaveWithReloadResult.cs +++ b/src/DnsmasqWebUI/Models/Status/SaveWithReloadResult.cs @@ -1,6 +1,6 @@ using DnsmasqWebUI.Services.Abstractions; -namespace DnsmasqWebUI.Models; +namespace DnsmasqWebUI.Models.Status; /// Result of a save operation that triggers a dnsmasq reload (e.g. PUT api/hosts, PUT api/dhcp/hosts). public record SaveWithReloadResult(bool Saved, ReloadResult Reload); diff --git a/src/DnsmasqWebUI/Parsers/ConfParserHelpers.cs b/src/DnsmasqWebUI/Parsers/ConfParserHelpers.cs index 2171b87..bde42ef 100644 --- a/src/DnsmasqWebUI/Parsers/ConfParserHelpers.cs +++ b/src/DnsmasqWebUI/Parsers/ConfParserHelpers.cs @@ -27,4 +27,46 @@ public static class ConfParserHelpers .Or(Character.EqualTo('#').Value(Unit.Value)) .OptionalOrDefault(Unit.Value) .Select(_ => Unit.Value); + + /// + /// Dnsmasq-style: content before first '#' that is at word start (after whitespace). Consumes to EOL. + /// Returns the trimmed content (no comment). Use for directive lines that allow inline # comment. + /// + public static readonly TextParser StripCommentContent = span => + { + var source = span.Source ?? ""; + var i = span.Position.Absolute; + var end = Math.Min(i + span.Length, source.Length); + var white = true; + var len = 0; + while (i + len < end) + { + var c = source[i + len]; + if (char.IsWhiteSpace(c)) + white = true; + else if (white && c == '#') + break; + else + white = false; + len++; + } + var consumed = span.First(len); + var content = consumed.ToStringValue().TrimEnd(); + var remainder = span.Skip(len); + return Result.Value(content, consumed, remainder); + }; + + /// + /// Key (no '='), optional spaces, optional '=value'. Full line. Reusable for key=value config lines. + /// + public static readonly TextParser<(string key, string value)> KeyValueLine = + Character.Matching(c => c != '=' && c != '\r' && c != '\n', "key character") + .AtLeastOnce() + .Text() + .Then(k => Character.WhiteSpace.Many() + .IgnoreThen(Character.EqualTo('=') + .IgnoreThen(Character.AnyChar.Many().Text().Select(s => s.Trim())) + .OptionalOrDefault("")) + .AtEnd() + .Select(v => (k.TrimEnd().Trim(), v))); } diff --git a/src/DnsmasqWebUI/Parsers/DnsmasqConfDhcpHostLineParser.cs b/src/DnsmasqWebUI/Parsers/DnsmasqConfDhcpHostLineParser.cs index ad69b0d..47c76c5 100644 --- a/src/DnsmasqWebUI/Parsers/DnsmasqConfDhcpHostLineParser.cs +++ b/src/DnsmasqWebUI/Parsers/DnsmasqConfDhcpHostLineParser.cs @@ -1,5 +1,4 @@ -using System.Text.RegularExpressions; -using DnsmasqWebUI.Models; +using DnsmasqWebUI.Models.Dhcp; using Superpower; using Superpower.Model; using Superpower.Parsers; @@ -13,7 +12,32 @@ namespace DnsmasqWebUI.Parsers; /// public static class DnsmasqConfDhcpHostLineParser { - private static readonly Regex HostnameRegex = new(@"^[a-zA-Z][-_a-zA-Z0-9]*$", RegexOptions.Compiled); + // Hostname: letter then (letter/digit/_/-)*. Full string. + private static readonly TextParser HostnameParser = + Character.Letter.Then(first => (Character.LetterOrDigit.Or(Character.In('_', '-')).Many().Text()) + .OptionalOrDefault("").Select(rest => first + rest)).AtEnd().Named("hostname"); + + // IPv4: four octets 0-255 with '.'. Full string. Range checked after parse. + private static readonly TextParser<(int a, int b, int c, int d)> Ipv4OctetsParser = + Numerics.IntegerInt32.Then(a => Character.EqualTo('.').IgnoreThen(Numerics.IntegerInt32) + .Then(b => Character.EqualTo('.').IgnoreThen(Numerics.IntegerInt32) + .Then(c => Character.EqualTo('.').IgnoreThen(Numerics.IntegerInt32).Select(d => (a, b, c, d))))).AtEnd(); + + // MAC: six hex pairs xx:xx:xx:xx:xx:xx. Full string. + private static readonly TextParser MacParser = + Span.MatchedBy(Character.HexDigit.Repeat(2)) + .IgnoreThen(Character.EqualTo(':')) + .IgnoreThen(Span.MatchedBy(Character.HexDigit.Repeat(2))) + .IgnoreThen(Character.EqualTo(':')) + .IgnoreThen(Span.MatchedBy(Character.HexDigit.Repeat(2))) + .IgnoreThen(Character.EqualTo(':')) + .IgnoreThen(Span.MatchedBy(Character.HexDigit.Repeat(2))) + .IgnoreThen(Character.EqualTo(':')) + .IgnoreThen(Span.MatchedBy(Character.HexDigit.Repeat(2))) + .IgnoreThen(Character.EqualTo(':')) + .IgnoreThen(Span.MatchedBy(Character.HexDigit.Repeat(2))) + .AtEnd() + .Value(Unit.Value); // Optional ## or # at start (Try so that single # backtracks and we can match one #) private static readonly TextParser<(bool isComment, bool isDeleted)> Prefix = @@ -85,7 +109,7 @@ public static class DnsmasqConfDhcpHostLineParser extraList.Add(field); else if (field.StartsWith("set:", StringComparison.OrdinalIgnoreCase)) extraList.Add(field); - else if (HostnameRegex.IsMatch(field)) + else if (TryParseHostname(field)) host.Name = field; else extraList.Add(field); @@ -129,15 +153,15 @@ public static class DnsmasqConfDhcpHostLineParser h.MacAddresses.Count > 0 || !string.IsNullOrEmpty(h.Name) || !string.IsNullOrEmpty(h.Address) || h.Extra.Count > 0 || !string.IsNullOrEmpty(h.Lease) || h.Ignore; - private static bool IsMac(string s) - { - var parts = s.Split(':'); - return parts.Length == 6 && parts.All(p => p.Length == 2 && p.All(c => char.IsAsciiHexDigit(c))); - } + private static bool IsMac(string s) => !string.IsNullOrEmpty(s) && MacParser.TryParse(s).HasValue; private static bool IsIpv4(string s) { - var parts = s.Split('.'); - return parts.Length == 4 && parts.All(p => p.Length > 0 && p.All(char.IsDigit) && int.TryParse(p, out var n) && n >= 0 && n <= 255); + var r = Ipv4OctetsParser.TryParse(s); + if (!r.HasValue) return false; + var (a, b, c, d) = r.Value; + return a >= 0 && a <= 255 && b >= 0 && b <= 255 && c >= 0 && c <= 255 && d >= 0 && d <= 255; } + + private static bool TryParseHostname(string s) => !string.IsNullOrEmpty(s) && HostnameParser.TryParse(s).HasValue; } diff --git a/src/DnsmasqWebUI/Parsers/DnsmasqConfDirectiveParser.cs b/src/DnsmasqWebUI/Parsers/DnsmasqConfDirectiveParser.cs index cbab4f6..1753091 100644 --- a/src/DnsmasqWebUI/Parsers/DnsmasqConfDirectiveParser.cs +++ b/src/DnsmasqWebUI/Parsers/DnsmasqConfDirectiveParser.cs @@ -1,5 +1,5 @@ -using DnsmasqWebUI.Models; using DnsmasqWebUI.Models.Config; +using DnsmasqWebUI.Models.EffectiveConfig; using Superpower; using Superpower.Model; using Superpower.Parsers; @@ -13,40 +13,15 @@ namespace DnsmasqWebUI.Parsers; /// public static class DnsmasqConfDirectiveParser { - // Key: non-= chars, trimmed - private static readonly TextParser Key = - Character.Matching(c => c != '=' && c != '\r' && c != '\n', "key character") - .AtLeastOnce().Text() - .Select(s => s.TrimEnd()); - - // Key, optional spaces, optional '=value' (dnsmasq: key-only lines are valid for flags). - private static readonly TextParser<(string key, string value)> KeyValue = - from k in Key - from _ in Character.WhiteSpace.Many() - from v in Character.EqualTo('=') - .IgnoreThen(Character.AnyChar.Many().Text()) - .Select(s => s.Trim()) - .OptionalOrDefault("") - select (k.Trim(), v); - - // Full line: key=value or key only + // Key=value line using shared helper (Then/IgnoreThen for hot path). Full line. private static readonly TextParser<(string key, string value)> DirectiveLine = - KeyValue.AtEnd(); + ConfParserHelpers.KeyValueLine.Named("key=value directive"); /// Strip dnsmasq-style comment: from first '#' that is at word start (after whitespace) to end of line. public static string StripComment(string line) { - bool white = true; - for (int i = 0; i < line.Length; i++) - { - if (char.IsWhiteSpace(line[i])) - white = true; - else if (white && line[i] == '#') - return line[..i].TrimEnd(); - else - white = false; - } - return line; + var result = ConfParserHelpers.StripCommentContent.TryParse(line); + return result.HasValue ? result.Value : line.TrimEnd(); } /// Parse a non-comment line into key and value. Returns null for empty or comment-only lines. @@ -67,6 +42,29 @@ public static class DnsmasqConfDirectiveParser return (key, value); } + /// Like but when parsing fails returns the Superpower error message and position (line/column). + public static bool TryParseKeyValue(string line, out (string key, string value)? kv, out string? error, out Position errorPosition) + { + kv = null; + error = null; + errorPosition = Position.Empty; + var t = StripComment(line).TrimStart(); + if (string.IsNullOrEmpty(t) || t.StartsWith("#", StringComparison.Ordinal)) + return true; // not a directive line, no error + var result = DirectiveLine.TryParse(t); + if (result.HasValue) + { + var (key, value) = result.Value; + if (string.IsNullOrWhiteSpace(key)) + return true; + kv = (key, value); + return true; + } + error = result.ToString(); + errorPosition = result.ErrorPosition; + return false; + } + /// Parse one non-blank, non-comment .conf line into a typed directive. Returns null if line is empty or comment. public static DnsmasqConfDirective? ParseLine(string line, int lineNumber, string sourceFilePath) { diff --git a/src/DnsmasqWebUI/Parsers/DnsmasqConfFileLineParser.cs b/src/DnsmasqWebUI/Parsers/DnsmasqConfFileLineParser.cs index c073511..383fc71 100644 --- a/src/DnsmasqWebUI/Parsers/DnsmasqConfFileLineParser.cs +++ b/src/DnsmasqWebUI/Parsers/DnsmasqConfFileLineParser.cs @@ -1,5 +1,5 @@ -using DnsmasqWebUI.Models; using DnsmasqWebUI.Models.Config; +using DnsmasqWebUI.Models.EffectiveConfig; using Superpower; using Superpower.Model; using Superpower.Parsers; @@ -16,24 +16,24 @@ public static class DnsmasqConfFileLineParser private enum ConfLineParseKind { Blank, Comment, AddnHosts, DhcpHostCandidate, Other } private static readonly TextParser<(ConfLineParseKind kind, string content)> Blank = - Character.WhiteSpace.Many().AtEnd().Select(_ => (ConfLineParseKind.Blank, "")); + Character.WhiteSpace.Many().AtEnd().Select(_ => (ConfLineParseKind.Blank, "")).Named("blank line"); private static readonly TextParser<(ConfLineParseKind kind, string content)> Comment = Character.EqualTo('#').IgnoreThen(Character.AnyChar.Many().Text()) - .Select(_ => (ConfLineParseKind.Comment, "")); + .Select(_ => (ConfLineParseKind.Comment, "")).Named("comment"); private static readonly TextParser<(ConfLineParseKind kind, string content)> AddnHosts = ConfParserHelpers.OptionalCommentPrefix.IgnoreThen(Span.EqualTo("addn-hosts=")) .IgnoreThen(Character.AnyChar.Many().Text()) - .Select(s => (ConfLineParseKind.AddnHosts, s.Trim())); + .Select(s => (ConfLineParseKind.AddnHosts, s.Trim())).Named("addn-hosts line"); private static readonly TextParser<(ConfLineParseKind kind, string content)> DhcpHostCandidate = ConfParserHelpers.OptionalCommentPrefix.IgnoreThen(Span.EqualTo("dhcp-host=")) .IgnoreThen(Character.AnyChar.Many()) - .Select(_ => (ConfLineParseKind.DhcpHostCandidate, "")); + .Select(_ => (ConfLineParseKind.DhcpHostCandidate, "")).Named("dhcp-host line"); private static readonly TextParser<(ConfLineParseKind kind, string content)> Other = - Character.AnyChar.Many().Text().Select(s => (ConfLineParseKind.Other, s)); + Character.AnyChar.Many().Text().Select(s => (ConfLineParseKind.Other, s)).Named("directive or other"); private static readonly TextParser<(ConfLineParseKind kind, string content)> LineParser = Blank.Try().Or(Comment.Try()).Or(AddnHosts.Try()).Or(DhcpHostCandidate.Try()).Or(Other).AtEnd(); diff --git a/src/DnsmasqWebUI/Parsers/DnsmasqConfIncludeParser.cs b/src/DnsmasqWebUI/Parsers/DnsmasqConfIncludeParser.cs index 1b89d97..9567183 100644 --- a/src/DnsmasqWebUI/Parsers/DnsmasqConfIncludeParser.cs +++ b/src/DnsmasqWebUI/Parsers/DnsmasqConfIncludeParser.cs @@ -1,4 +1,7 @@ -using DnsmasqWebUI.Models; +using DnsmasqWebUI.Models.EffectiveConfig; +using Superpower; +using Superpower.Model; +using Superpower.Parsers; namespace DnsmasqWebUI.Parsers; @@ -106,19 +109,33 @@ public static class DnsmasqConfIncludeParser } } + // conf-dir value: comma-delimited fields (dir, optional *suffix, optional ignore suffixes). Superpower grammar. + private static readonly TextParser ConfDirField = + Character.Matching(c => c != ',' && c != '\r' && c != '\n', "field character").AtLeastOnce().Text() + .Then(s => Character.WhiteSpace.Many().IgnoreThen(Parse.Return(s.Trim()))); + + private static readonly TextParser> ConfDirValueParser = + ConfDirField.AtLeastOnceDelimitedBy(ConfParserHelpers.Token(Character.EqualTo(','))) + .AtEnd() + .Select(list => list.ToList()) + .Named("conf-dir value"); + private static (string? directory, List? matchSuffix, List? ignoreSuffix) ParseConfDirValue(string value) { - var parts = value.Split(',').Select(p => p.Trim()).Where(p => p.Length > 0).ToList(); - if (parts.Count == 0) return (null, null, null); + var parsed = ConfDirValueParser.TryParse(value.Trim()); + if (!parsed.HasValue || parsed.Value.Count == 0) + return (null, null, null); + var parts = parsed.Value; var dir = parts[0]; var matchSuffix = new List(); var ignoreSuffix = new List(); for (var i = 1; i < parts.Count; i++) { - if (parts[i].StartsWith('*')) - matchSuffix.Add(parts[i].Length > 1 ? parts[i][1..] : ""); + var p = parts[i]; + if (p.StartsWith('*')) + matchSuffix.Add(p.Length > 1 ? p[1..] : ""); else - ignoreSuffix.Add(parts[i]); + ignoreSuffix.Add(p); } return (dir, matchSuffix.Count > 0 ? matchSuffix : null, ignoreSuffix.Count > 0 ? ignoreSuffix : null); } diff --git a/src/DnsmasqWebUI/Parsers/DnsmasqLeasesFileLineParser.cs b/src/DnsmasqWebUI/Parsers/DnsmasqLeasesFileLineParser.cs index 767c28c..ce7ae85 100644 --- a/src/DnsmasqWebUI/Parsers/DnsmasqLeasesFileLineParser.cs +++ b/src/DnsmasqWebUI/Parsers/DnsmasqLeasesFileLineParser.cs @@ -1,4 +1,4 @@ -using DnsmasqWebUI.Models; +using DnsmasqWebUI.Models.Dhcp; using Superpower; using Superpower.Model; using Superpower.Parsers; diff --git a/src/DnsmasqWebUI/Parsers/HostsFileLineParser.cs b/src/DnsmasqWebUI/Parsers/HostsFileLineParser.cs index 8539ec4..ebdba59 100644 --- a/src/DnsmasqWebUI/Parsers/HostsFileLineParser.cs +++ b/src/DnsmasqWebUI/Parsers/HostsFileLineParser.cs @@ -1,4 +1,4 @@ -using DnsmasqWebUI.Models; +using DnsmasqWebUI.Models.Hosts; using Superpower; using Superpower.Model; using Superpower.Parsers; diff --git a/src/DnsmasqWebUI/Services/Abstractions/IDnsmasqConfigService.cs b/src/DnsmasqWebUI/Services/Abstractions/IDnsmasqConfigService.cs index b767323..1e20303 100644 --- a/src/DnsmasqWebUI/Services/Abstractions/IDnsmasqConfigService.cs +++ b/src/DnsmasqWebUI/Services/Abstractions/IDnsmasqConfigService.cs @@ -1,5 +1,6 @@ -using DnsmasqWebUI.Models; using DnsmasqWebUI.Models.Config; +using DnsmasqWebUI.Models.Dhcp; +using DnsmasqWebUI.Models.EffectiveConfig; namespace DnsmasqWebUI.Services.Abstractions; diff --git a/src/DnsmasqWebUI/Services/Abstractions/IDnsmasqConfigSetService.cs b/src/DnsmasqWebUI/Services/Abstractions/IDnsmasqConfigSetService.cs index e1ae33b..0c321e8 100644 --- a/src/DnsmasqWebUI/Services/Abstractions/IDnsmasqConfigSetService.cs +++ b/src/DnsmasqWebUI/Services/Abstractions/IDnsmasqConfigSetService.cs @@ -1,4 +1,4 @@ -using DnsmasqWebUI.Models; +using DnsmasqWebUI.Models.EffectiveConfig; namespace DnsmasqWebUI.Services.Abstractions; diff --git a/src/DnsmasqWebUI/Services/Abstractions/IHostsFileService.cs b/src/DnsmasqWebUI/Services/Abstractions/IHostsFileService.cs index 880181d..5417bcf 100644 --- a/src/DnsmasqWebUI/Services/Abstractions/IHostsFileService.cs +++ b/src/DnsmasqWebUI/Services/Abstractions/IHostsFileService.cs @@ -1,4 +1,4 @@ -using DnsmasqWebUI.Models; +using DnsmasqWebUI.Models.Hosts; namespace DnsmasqWebUI.Services.Abstractions; diff --git a/src/DnsmasqWebUI/Services/Abstractions/ILeasesCache.cs b/src/DnsmasqWebUI/Services/Abstractions/ILeasesCache.cs index 0075315..d39dabd 100644 --- a/src/DnsmasqWebUI/Services/Abstractions/ILeasesCache.cs +++ b/src/DnsmasqWebUI/Services/Abstractions/ILeasesCache.cs @@ -1,4 +1,4 @@ -using DnsmasqWebUI.Models; +using DnsmasqWebUI.Models.Dhcp; namespace DnsmasqWebUI.Services.Abstractions; diff --git a/src/DnsmasqWebUI/Services/Abstractions/ILeasesFileService.cs b/src/DnsmasqWebUI/Services/Abstractions/ILeasesFileService.cs index 8777110..0d2ed9e 100644 --- a/src/DnsmasqWebUI/Services/Abstractions/ILeasesFileService.cs +++ b/src/DnsmasqWebUI/Services/Abstractions/ILeasesFileService.cs @@ -1,4 +1,4 @@ -using DnsmasqWebUI.Models; +using DnsmasqWebUI.Models.Dhcp; namespace DnsmasqWebUI.Services.Abstractions; diff --git a/src/DnsmasqWebUI/Services/DnsmasqConfigService.cs b/src/DnsmasqWebUI/Services/DnsmasqConfigService.cs index c710b11..93c3ec8 100644 --- a/src/DnsmasqWebUI/Services/DnsmasqConfigService.cs +++ b/src/DnsmasqWebUI/Services/DnsmasqConfigService.cs @@ -1,6 +1,7 @@ using System.Text; -using DnsmasqWebUI.Models; using DnsmasqWebUI.Models.Config; +using DnsmasqWebUI.Models.Dhcp; +using DnsmasqWebUI.Models.EffectiveConfig; using DnsmasqWebUI.Parsers; using DnsmasqWebUI.Services.Abstractions; diff --git a/src/DnsmasqWebUI/Services/DnsmasqConfigSetService.cs b/src/DnsmasqWebUI/Services/DnsmasqConfigSetService.cs index 3419394..bdb19f5 100644 --- a/src/DnsmasqWebUI/Services/DnsmasqConfigSetService.cs +++ b/src/DnsmasqWebUI/Services/DnsmasqConfigSetService.cs @@ -1,4 +1,4 @@ -using DnsmasqWebUI.Models; +using DnsmasqWebUI.Models.EffectiveConfig; using DnsmasqWebUI.Configuration; using DnsmasqWebUI.Parsers; using DnsmasqWebUI.Services.Abstractions; diff --git a/src/DnsmasqWebUI/Services/EnsureManagedConfigHostedService.cs b/src/DnsmasqWebUI/Services/EnsureManagedConfigHostedService.cs index 57c4659..36e6307 100644 --- a/src/DnsmasqWebUI/Services/EnsureManagedConfigHostedService.cs +++ b/src/DnsmasqWebUI/Services/EnsureManagedConfigHostedService.cs @@ -1,6 +1,6 @@ using System.Text; -using DnsmasqWebUI.Models; using DnsmasqWebUI.Models.Config; +using DnsmasqWebUI.Models.EffectiveConfig; using DnsmasqWebUI.Configuration; using DnsmasqWebUI.Parsers; using DnsmasqWebUI.Services.Abstractions; diff --git a/src/DnsmasqWebUI/Services/HostsFileService.cs b/src/DnsmasqWebUI/Services/HostsFileService.cs index 7ad69fe..aefce6b 100644 --- a/src/DnsmasqWebUI/Services/HostsFileService.cs +++ b/src/DnsmasqWebUI/Services/HostsFileService.cs @@ -1,5 +1,6 @@ using System.Text; -using DnsmasqWebUI.Models; +using DnsmasqWebUI.Models.EffectiveConfig; +using DnsmasqWebUI.Models.Hosts; using DnsmasqWebUI.Parsers; using DnsmasqWebUI.Services.Abstractions; diff --git a/src/DnsmasqWebUI/Services/LeasesCache.cs b/src/DnsmasqWebUI/Services/LeasesCache.cs index 91d0e82..85fa603 100644 --- a/src/DnsmasqWebUI/Services/LeasesCache.cs +++ b/src/DnsmasqWebUI/Services/LeasesCache.cs @@ -1,5 +1,5 @@ using System.Text; -using DnsmasqWebUI.Models; +using DnsmasqWebUI.Models.Dhcp; using DnsmasqWebUI.Parsers; using DnsmasqWebUI.Services.Abstractions; diff --git a/src/DnsmasqWebUI/Services/LeasesFileService.cs b/src/DnsmasqWebUI/Services/LeasesFileService.cs index d36e1aa..f8538a4 100644 --- a/src/DnsmasqWebUI/Services/LeasesFileService.cs +++ b/src/DnsmasqWebUI/Services/LeasesFileService.cs @@ -1,4 +1,4 @@ -using DnsmasqWebUI.Models; +using DnsmasqWebUI.Models.Dhcp; using DnsmasqWebUI.Services.Abstractions; namespace DnsmasqWebUI.Services; diff --git a/testdata/README b/testdata/README index dda1ae9..18f3567 100644 --- a/testdata/README +++ b/testdata/README @@ -17,6 +17,6 @@ Source for the Docker test harness: `scripts/prepare-test-mount.sh` syncs this d | **hosts** | First addn-hosts file; system hosts when `Dnsmasq:SystemHostsPath` points here. | | **hosts.extra** | Second addn-hosts file. | | **resolv.dnsmasq** | Minimal file for resolv-file= in 03-more.conf; must exist so dnsmasq does not fail/warn. | -| **leases** | Sample DHCP leases for harness leases table. | +| **leases** | Sample DHCP leases for unit tests only; not synced to the harness. The harness uses the path from config (`dhcp-leasefile=` in dnsmasq-test.conf); the app discovers and monitors that file; dnsmasq creates it when DHCP clients obtain addresses. | Harness usage (start/stop/tidy): see **agents.md** in the repo root.