diff --git a/src/DnsmasqWebUI/Components/Dnsmasq/LogsSection.razor.cs b/src/DnsmasqWebUI/Components/Dnsmasq/LogsSection.razor.cs
index a06729e..93f7e11 100644
--- a/src/DnsmasqWebUI/Components/Dnsmasq/LogsSection.razor.cs
+++ b/src/DnsmasqWebUI/Components/Dnsmasq/LogsSection.razor.cs
@@ -1,5 +1,5 @@
-using DnsmasqWebUI.Client.Http.Abstractions;
-using DnsmasqWebUI.Models.Status;
+using DnsmasqWebUI.Infrastructure.Client.Abstractions;
+using DnsmasqWebUI.Models.Dnsmasq;
using Microsoft.AspNetCore.Components;
namespace DnsmasqWebUI.Components.Dnsmasq;
diff --git a/src/DnsmasqWebUI/Components/Dnsmasq/StatusSection.razor.cs b/src/DnsmasqWebUI/Components/Dnsmasq/StatusSection.razor.cs
index 662b23a..5b57d69 100644
--- a/src/DnsmasqWebUI/Components/Dnsmasq/StatusSection.razor.cs
+++ b/src/DnsmasqWebUI/Components/Dnsmasq/StatusSection.razor.cs
@@ -1,5 +1,5 @@
-using DnsmasqWebUI.Client.Http.Abstractions;
-using DnsmasqWebUI.Models.Status;
+using DnsmasqWebUI.Infrastructure.Client.Abstractions;
+using DnsmasqWebUI.Models.Dnsmasq;
using Microsoft.AspNetCore.Components;
namespace DnsmasqWebUI.Components.Dnsmasq;
diff --git a/src/DnsmasqWebUI/Components/Modals/SettingsModal.razor.cs b/src/DnsmasqWebUI/Components/Modals/SettingsModal.razor.cs
index d204954..be77fca 100644
--- a/src/DnsmasqWebUI/Components/Modals/SettingsModal.razor.cs
+++ b/src/DnsmasqWebUI/Components/Modals/SettingsModal.razor.cs
@@ -1,5 +1,5 @@
-using DnsmasqWebUI.Client.Models;
-using DnsmasqWebUI.Client.Services.Abstractions;
+using DnsmasqWebUI.Models.Client;
+using DnsmasqWebUI.Infrastructure.Services.Abstractions;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.JSInterop;
diff --git a/src/DnsmasqWebUI/Components/Pages/Dnsmasq.razor b/src/DnsmasqWebUI/Components/Pages/Dnsmasq.razor
index e93e238..b3a0dbb 100644
--- a/src/DnsmasqWebUI/Components/Pages/Dnsmasq.razor
+++ b/src/DnsmasqWebUI/Components/Pages/Dnsmasq.razor
@@ -24,9 +24,12 @@ else if (_status != null)
Service configuration
Config files (load order):
- @if (_configSet != null && _configSet.Files?.Count > 0)
+ @{
+ var configFileList = _status.ConfigFiles?.Count > 0 ? _status.ConfigFiles : _configSet?.Files;
+ }
+ @if (configFileList != null && configFileList.Count > 0)
{
- @foreach (var f in _configSet.Files)
+ @foreach (var f in configFileList)
{
var isMain = string.Equals(f.Path, _status.MainConfigPath, StringComparison.Ordinal);
var isManaged = f.IsManaged;
diff --git a/src/DnsmasqWebUI/Components/_Imports.razor b/src/DnsmasqWebUI/Components/_Imports.razor
index e9fa274..a32a2d2 100644
--- a/src/DnsmasqWebUI/Components/_Imports.razor
+++ b/src/DnsmasqWebUI/Components/_Imports.razor
@@ -7,19 +7,20 @@
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using DnsmasqWebUI
-@using DnsmasqWebUI.Client.Models
-@using DnsmasqWebUI.Client.Services.Abstractions
+@using DnsmasqWebUI.Models.Client
+@using DnsmasqWebUI.Infrastructure.Services.Abstractions
@using DnsmasqWebUI.Components
@using DnsmasqWebUI.Components.Modals
@using DnsmasqWebUI.Components.Dnsmasq
@using DnsmasqWebUI.Components.Dnsmasq.EffectiveConfig
@using DnsmasqWebUI.Components.Hosts
+@using DnsmasqWebUI.Infrastructure
+@using DnsmasqWebUI.Infrastructure.Helpers.Config
@using DnsmasqWebUI.Models
-@using DnsmasqWebUI.Models.EffectiveConfig
+@using DnsmasqWebUI.Models.Dnsmasq
+@using DnsmasqWebUI.Models.Dnsmasq.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
+@using DnsmasqWebUI.Models.Config
+@using DnsmasqWebUI.Infrastructure.Client.Abstractions
@using Microsoft.Extensions.Options
diff --git a/src/DnsmasqWebUI/Controllers/ConfigController.cs b/src/DnsmasqWebUI/Controllers/ConfigController.cs
index 6532090..1494750 100644
--- a/src/DnsmasqWebUI/Controllers/ConfigController.cs
+++ b/src/DnsmasqWebUI/Controllers/ConfigController.cs
@@ -1,6 +1,8 @@
using DnsmasqWebUI.Models.Config;
-using DnsmasqWebUI.Models.EffectiveConfig;
-using DnsmasqWebUI.Services.Abstractions;
+using DnsmasqWebUI.Models.Dnsmasq;
+using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
+using DnsmasqWebUI.Models.Contracts;
+using DnsmasqWebUI.Infrastructure.Services.Abstractions;
using Microsoft.AspNetCore.Mvc;
namespace DnsmasqWebUI.Controllers;
diff --git a/src/DnsmasqWebUI/Controllers/DhcpController.cs b/src/DnsmasqWebUI/Controllers/DhcpController.cs
index 3b72b58..a008e6f 100644
--- a/src/DnsmasqWebUI/Controllers/DhcpController.cs
+++ b/src/DnsmasqWebUI/Controllers/DhcpController.cs
@@ -1,6 +1,6 @@
using DnsmasqWebUI.Models.Dhcp;
-using DnsmasqWebUI.Models.Status;
-using DnsmasqWebUI.Services.Abstractions;
+using DnsmasqWebUI.Models.Dnsmasq;
+using DnsmasqWebUI.Infrastructure.Services.Abstractions;
using Microsoft.AspNetCore.Mvc;
namespace DnsmasqWebUI.Controllers;
diff --git a/src/DnsmasqWebUI/Controllers/HostsController.cs b/src/DnsmasqWebUI/Controllers/HostsController.cs
index 72f7dda..532b53e 100644
--- a/src/DnsmasqWebUI/Controllers/HostsController.cs
+++ b/src/DnsmasqWebUI/Controllers/HostsController.cs
@@ -1,6 +1,6 @@
using DnsmasqWebUI.Models.Hosts;
-using DnsmasqWebUI.Models.Status;
-using DnsmasqWebUI.Services.Abstractions;
+using DnsmasqWebUI.Models.Dnsmasq;
+using DnsmasqWebUI.Infrastructure.Services.Abstractions;
using Microsoft.AspNetCore.Mvc;
namespace DnsmasqWebUI.Controllers;
diff --git a/src/DnsmasqWebUI/Controllers/LeasesController.cs b/src/DnsmasqWebUI/Controllers/LeasesController.cs
index 27e2f07..6c0e5f5 100644
--- a/src/DnsmasqWebUI/Controllers/LeasesController.cs
+++ b/src/DnsmasqWebUI/Controllers/LeasesController.cs
@@ -1,5 +1,5 @@
using DnsmasqWebUI.Models.Dhcp;
-using DnsmasqWebUI.Services.Abstractions;
+using DnsmasqWebUI.Infrastructure.Services.Abstractions;
using Microsoft.AspNetCore.Mvc;
namespace DnsmasqWebUI.Controllers;
diff --git a/src/DnsmasqWebUI/Controllers/ReloadController.cs b/src/DnsmasqWebUI/Controllers/ReloadController.cs
index 184ee8d..2fb2d83 100644
--- a/src/DnsmasqWebUI/Controllers/ReloadController.cs
+++ b/src/DnsmasqWebUI/Controllers/ReloadController.cs
@@ -1,4 +1,4 @@
-using DnsmasqWebUI.Services.Abstractions;
+using DnsmasqWebUI.Infrastructure.Services.Abstractions;
using Microsoft.AspNetCore.Mvc;
namespace DnsmasqWebUI.Controllers;
diff --git a/src/DnsmasqWebUI/Controllers/StatusController.cs b/src/DnsmasqWebUI/Controllers/StatusController.cs
index 5e3e7e1..179398f 100644
--- a/src/DnsmasqWebUI/Controllers/StatusController.cs
+++ b/src/DnsmasqWebUI/Controllers/StatusController.cs
@@ -1,7 +1,8 @@
-using DnsmasqWebUI.Models.EffectiveConfig;
-using DnsmasqWebUI.Models.Status;
-using DnsmasqWebUI.Configuration;
-using DnsmasqWebUI.Services.Abstractions;
+using DnsmasqWebUI.Models.Dnsmasq;
+using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
+using DnsmasqWebUI.Models.Config;
+using DnsmasqWebUI.Models.Contracts;
+using DnsmasqWebUI.Infrastructure.Services.Abstractions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
@@ -76,6 +77,7 @@ public class StatusController : ControllerBase
ManagedFilePathExists: !string.IsNullOrEmpty(set.ManagedFilePath) && System.IO.File.Exists(set.ManagedFilePath),
LeasesPathConfigured: !string.IsNullOrEmpty(effectiveLeasesPath),
LeasesPathExists: !string.IsNullOrEmpty(effectiveLeasesPath) && System.IO.File.Exists(effectiveLeasesPath),
+ ConfigFiles: set.Files.Count > 0 ? set.Files : null,
ReloadCommandConfigured: !string.IsNullOrWhiteSpace(_options.ReloadCommand),
StatusCommandConfigured: !string.IsNullOrWhiteSpace(_options.StatusCommand),
StatusShowConfigured: !string.IsNullOrWhiteSpace(_options.StatusShowCommand),
diff --git a/src/DnsmasqWebUI/Extensions/ServiceCollectionExtensions.cs b/src/DnsmasqWebUI/Extensions/ServiceCollectionExtensions.cs
index f4d5812..ac756b1 100644
--- a/src/DnsmasqWebUI/Extensions/ServiceCollectionExtensions.cs
+++ b/src/DnsmasqWebUI/Extensions/ServiceCollectionExtensions.cs
@@ -1,7 +1,7 @@
using System.Reflection;
-using DnsmasqWebUI.Client.Http;
-using DnsmasqWebUI.Client.Http.Abstractions;
-using DnsmasqWebUI.Services.Abstractions;
+using DnsmasqWebUI.Infrastructure.Client;
+using DnsmasqWebUI.Infrastructure.Client.Abstractions;
+using DnsmasqWebUI.Infrastructure.Services.Abstractions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
diff --git a/src/DnsmasqWebUI/Client/Http/Abstractions/IConfigSetClient.cs b/src/DnsmasqWebUI/Infrastructure/Client/Abstractions/IConfigSetClient.cs
similarity index 61%
rename from src/DnsmasqWebUI/Client/Http/Abstractions/IConfigSetClient.cs
rename to src/DnsmasqWebUI/Infrastructure/Client/Abstractions/IConfigSetClient.cs
index 0c54917..38154d2 100644
--- a/src/DnsmasqWebUI/Client/Http/Abstractions/IConfigSetClient.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Client/Abstractions/IConfigSetClient.cs
@@ -1,6 +1,6 @@
-using DnsmasqWebUI.Models.EffectiveConfig;
+using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
-namespace DnsmasqWebUI.Client.Http.Abstractions;
+namespace DnsmasqWebUI.Infrastructure.Client.Abstractions;
/// Typed client for GET api/config/set.
public interface IConfigSetClient
diff --git a/src/DnsmasqWebUI/Client/Http/Abstractions/IDhcpHostsClient.cs b/src/DnsmasqWebUI/Infrastructure/Client/Abstractions/IDhcpHostsClient.cs
similarity index 70%
rename from src/DnsmasqWebUI/Client/Http/Abstractions/IDhcpHostsClient.cs
rename to src/DnsmasqWebUI/Infrastructure/Client/Abstractions/IDhcpHostsClient.cs
index b0c7214..9012d78 100644
--- a/src/DnsmasqWebUI/Client/Http/Abstractions/IDhcpHostsClient.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Client/Abstractions/IDhcpHostsClient.cs
@@ -1,7 +1,8 @@
using DnsmasqWebUI.Models.Dhcp;
-using DnsmasqWebUI.Models.Status;
+using DnsmasqWebUI.Models.Dnsmasq;
+using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
-namespace DnsmasqWebUI.Client.Http.Abstractions;
+namespace DnsmasqWebUI.Infrastructure.Client.Abstractions;
/// Typed client for GET/PUT api/dhcp/hosts.
public interface IDhcpHostsClient
diff --git a/src/DnsmasqWebUI/Client/Http/Abstractions/IHostsClient.cs b/src/DnsmasqWebUI/Infrastructure/Client/Abstractions/IHostsClient.cs
similarity index 74%
rename from src/DnsmasqWebUI/Client/Http/Abstractions/IHostsClient.cs
rename to src/DnsmasqWebUI/Infrastructure/Client/Abstractions/IHostsClient.cs
index d438886..847c405 100644
--- a/src/DnsmasqWebUI/Client/Http/Abstractions/IHostsClient.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Client/Abstractions/IHostsClient.cs
@@ -1,7 +1,8 @@
using DnsmasqWebUI.Models.Hosts;
-using DnsmasqWebUI.Models.Status;
+using DnsmasqWebUI.Models.Dnsmasq;
+using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
-namespace DnsmasqWebUI.Client.Http.Abstractions;
+namespace DnsmasqWebUI.Infrastructure.Client.Abstractions;
/// Typed client for GET/PUT api/hosts.
public interface IHostsClient
diff --git a/src/DnsmasqWebUI/Client/Http/Abstractions/ILeasesClient.cs b/src/DnsmasqWebUI/Infrastructure/Client/Abstractions/ILeasesClient.cs
similarity index 86%
rename from src/DnsmasqWebUI/Client/Http/Abstractions/ILeasesClient.cs
rename to src/DnsmasqWebUI/Infrastructure/Client/Abstractions/ILeasesClient.cs
index 6482daf..989f0f3 100644
--- a/src/DnsmasqWebUI/Client/Http/Abstractions/ILeasesClient.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Client/Abstractions/ILeasesClient.cs
@@ -1,6 +1,6 @@
using DnsmasqWebUI.Models.Dhcp;
-namespace DnsmasqWebUI.Client.Http.Abstractions;
+namespace DnsmasqWebUI.Infrastructure.Client.Abstractions;
/// Typed client for GET api/leases.
public interface ILeasesClient
diff --git a/src/DnsmasqWebUI/Client/Http/Abstractions/IReloadClient.cs b/src/DnsmasqWebUI/Infrastructure/Client/Abstractions/IReloadClient.cs
similarity index 58%
rename from src/DnsmasqWebUI/Client/Http/Abstractions/IReloadClient.cs
rename to src/DnsmasqWebUI/Infrastructure/Client/Abstractions/IReloadClient.cs
index d324505..502786f 100644
--- a/src/DnsmasqWebUI/Client/Http/Abstractions/IReloadClient.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Client/Abstractions/IReloadClient.cs
@@ -1,6 +1,6 @@
-using DnsmasqWebUI.Services.Abstractions;
+using DnsmasqWebUI.Infrastructure.Services.Abstractions;
-namespace DnsmasqWebUI.Client.Http.Abstractions;
+namespace DnsmasqWebUI.Infrastructure.Client.Abstractions;
/// Typed client for POST api/reload.
public interface IReloadClient
diff --git a/src/DnsmasqWebUI/Client/Http/Abstractions/IStatusClient.cs b/src/DnsmasqWebUI/Infrastructure/Client/Abstractions/IStatusClient.cs
similarity index 64%
rename from src/DnsmasqWebUI/Client/Http/Abstractions/IStatusClient.cs
rename to src/DnsmasqWebUI/Infrastructure/Client/Abstractions/IStatusClient.cs
index a5ca13e..2c527b2 100644
--- a/src/DnsmasqWebUI/Client/Http/Abstractions/IStatusClient.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Client/Abstractions/IStatusClient.cs
@@ -1,6 +1,6 @@
-using DnsmasqWebUI.Models.Status;
+using DnsmasqWebUI.Models.Dnsmasq;
-namespace DnsmasqWebUI.Client.Http.Abstractions;
+namespace DnsmasqWebUI.Infrastructure.Client.Abstractions;
/// Typed client for GET api/status.
public interface IStatusClient
diff --git a/src/DnsmasqWebUI/Client/Http/ConfigSetClient.cs b/src/DnsmasqWebUI/Infrastructure/Client/ConfigSetClient.cs
similarity index 61%
rename from src/DnsmasqWebUI/Client/Http/ConfigSetClient.cs
rename to src/DnsmasqWebUI/Infrastructure/Client/ConfigSetClient.cs
index 3113ab4..ca731e4 100644
--- a/src/DnsmasqWebUI/Client/Http/ConfigSetClient.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Client/ConfigSetClient.cs
@@ -1,8 +1,10 @@
-using DnsmasqWebUI.Client.Http.Abstractions;
-using DnsmasqWebUI.Models.EffectiveConfig;
+using DnsmasqWebUI.Infrastructure.Client.Abstractions;
+using DnsmasqWebUI.Infrastructure.Helpers.Http;
+using DnsmasqWebUI.Models.Dnsmasq;
+using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
using System.Net.Http.Json;
-namespace DnsmasqWebUI.Client.Http;
+namespace DnsmasqWebUI.Infrastructure.Client;
public sealed class ConfigSetClient : IConfigSetClient
{
@@ -11,6 +13,6 @@ public sealed class ConfigSetClient : IConfigSetClient
public ConfigSetClient(HttpClient http) => _http = http;
public async Task GetConfigSetAsync(CancellationToken ct = default) =>
- await _http.GetFromJsonAsync("api/config/set", ct)
+ await _http.GetFromJsonAsync("api/config/set", ApiJsonOptions.ClientOptions, ct)
?? throw new InvalidOperationException("Unexpected null from api/config/set.");
}
diff --git a/src/DnsmasqWebUI/Client/Http/DhcpHostsClient.cs b/src/DnsmasqWebUI/Infrastructure/Client/DhcpHostsClient.cs
similarity index 78%
rename from src/DnsmasqWebUI/Client/Http/DhcpHostsClient.cs
rename to src/DnsmasqWebUI/Infrastructure/Client/DhcpHostsClient.cs
index 366a5c1..7471624 100644
--- a/src/DnsmasqWebUI/Client/Http/DhcpHostsClient.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Client/DhcpHostsClient.cs
@@ -1,10 +1,12 @@
using System.Net.Http.Json;
using System.Text.Json.Serialization;
-using DnsmasqWebUI.Client.Http.Abstractions;
+using DnsmasqWebUI.Infrastructure.Client.Abstractions;
+using DnsmasqWebUI.Infrastructure.Helpers.Http;
using DnsmasqWebUI.Models.Dhcp;
-using DnsmasqWebUI.Models.Status;
+using DnsmasqWebUI.Models.Dnsmasq;
+using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
-namespace DnsmasqWebUI.Client.Http;
+namespace DnsmasqWebUI.Infrastructure.Client;
public sealed class DhcpHostsClient : IDhcpHostsClient
{
@@ -14,13 +16,13 @@ public sealed class DhcpHostsClient : IDhcpHostsClient
public async Task> GetDhcpHostsAsync(CancellationToken ct = default)
{
- var list = await _http.GetFromJsonAsync>("api/dhcp/hosts", ct);
+ var list = await _http.GetFromJsonAsync>("api/dhcp/hosts", ApiJsonOptions.ClientOptions, ct);
return list ?? new List();
}
public async Task SaveDhcpHostsAsync(IReadOnlyList entries, CancellationToken ct = default)
{
- var response = await _http.PutAsJsonAsync("api/dhcp/hosts", entries, ct);
+ var response = await _http.PutAsJsonAsync("api/dhcp/hosts", entries, ApiJsonOptions.ClientOptions, ct);
if (!response.IsSuccessStatusCode)
{
var body = await response.Content.ReadAsStringAsync(ct);
@@ -29,7 +31,7 @@ public sealed class DhcpHostsClient : IDhcpHostsClient
{
try
{
- var err = System.Text.Json.JsonSerializer.Deserialize(body);
+ var err = System.Text.Json.JsonSerializer.Deserialize(body, ApiJsonOptions.ClientOptions);
if (!string.IsNullOrEmpty(err?.Error))
msg = err.Error;
}
@@ -37,7 +39,7 @@ public sealed class DhcpHostsClient : IDhcpHostsClient
}
throw new HttpRequestException(msg);
}
- return await response.Content.ReadFromJsonAsync(ct)
+ return await response.Content.ReadFromJsonAsync(ApiJsonOptions.ClientOptions, ct)
?? throw new InvalidOperationException("Unexpected null from api/dhcp/hosts.");
}
diff --git a/src/DnsmasqWebUI/Client/Http/HostsClient.cs b/src/DnsmasqWebUI/Infrastructure/Client/HostsClient.cs
similarity index 70%
rename from src/DnsmasqWebUI/Client/Http/HostsClient.cs
rename to src/DnsmasqWebUI/Infrastructure/Client/HostsClient.cs
index 7b23fde..54a6c18 100644
--- a/src/DnsmasqWebUI/Client/Http/HostsClient.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Client/HostsClient.cs
@@ -1,9 +1,11 @@
-using DnsmasqWebUI.Client.Http.Abstractions;
+using DnsmasqWebUI.Infrastructure.Client.Abstractions;
+using DnsmasqWebUI.Infrastructure.Helpers.Http;
using DnsmasqWebUI.Models.Hosts;
-using DnsmasqWebUI.Models.Status;
+using DnsmasqWebUI.Models.Dnsmasq;
+using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
using System.Net.Http.Json;
-namespace DnsmasqWebUI.Client.Http;
+namespace DnsmasqWebUI.Infrastructure.Client;
public sealed class HostsClient : IHostsClient
{
@@ -13,21 +15,21 @@ public sealed class HostsClient : IHostsClient
public async Task> GetHostsAsync(CancellationToken ct = default)
{
- var list = await _http.GetFromJsonAsync>("api/hosts", ct);
+ var list = await _http.GetFromJsonAsync>("api/hosts", ApiJsonOptions.ClientOptions, ct);
return list ?? new List();
}
public async Task> GetReadOnlyHostsAsync(CancellationToken ct = default)
{
- var list = await _http.GetFromJsonAsync>("api/hosts/readonly", ct);
+ var list = await _http.GetFromJsonAsync>("api/hosts/readonly", ApiJsonOptions.ClientOptions, ct);
return list ?? new List();
}
public async Task SaveHostsAsync(IReadOnlyList entries, CancellationToken ct = default)
{
- var response = await _http.PutAsJsonAsync("api/hosts", entries, ct);
+ var response = await _http.PutAsJsonAsync("api/hosts", entries, ApiJsonOptions.ClientOptions, ct);
response.EnsureSuccessStatusCode();
- return await response.Content.ReadFromJsonAsync(ct)
+ return await response.Content.ReadFromJsonAsync(ApiJsonOptions.ClientOptions, ct)
?? throw new InvalidOperationException("Unexpected null from api/hosts.");
}
}
diff --git a/src/DnsmasqWebUI/Client/Http/LeasesClient.cs b/src/DnsmasqWebUI/Infrastructure/Client/LeasesClient.cs
similarity index 66%
rename from src/DnsmasqWebUI/Client/Http/LeasesClient.cs
rename to src/DnsmasqWebUI/Infrastructure/Client/LeasesClient.cs
index 1a7df25..e763bfe 100644
--- a/src/DnsmasqWebUI/Client/Http/LeasesClient.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Client/LeasesClient.cs
@@ -1,8 +1,9 @@
-using DnsmasqWebUI.Client.Http.Abstractions;
+using DnsmasqWebUI.Infrastructure.Client.Abstractions;
+using DnsmasqWebUI.Infrastructure.Helpers.Http;
using DnsmasqWebUI.Models.Dhcp;
using System.Net.Http.Json;
-namespace DnsmasqWebUI.Client.Http;
+namespace DnsmasqWebUI.Infrastructure.Client;
public sealed class LeasesClient : ILeasesClient
{
@@ -13,7 +14,7 @@ public sealed class LeasesClient : ILeasesClient
public async Task GetLeasesAsync(bool forceRefresh = false, CancellationToken ct = default)
{
var url = forceRefresh ? "api/leases?refresh=true" : "api/leases";
- return await _http.GetFromJsonAsync(url, ct)
+ return await _http.GetFromJsonAsync(url, ApiJsonOptions.ClientOptions, ct)
?? throw new InvalidOperationException("Unexpected null from api/leases.");
}
}
diff --git a/src/DnsmasqWebUI/Client/Http/ReloadClient.cs b/src/DnsmasqWebUI/Infrastructure/Client/ReloadClient.cs
similarity index 68%
rename from src/DnsmasqWebUI/Client/Http/ReloadClient.cs
rename to src/DnsmasqWebUI/Infrastructure/Client/ReloadClient.cs
index d866e89..c5d2d6b 100644
--- a/src/DnsmasqWebUI/Client/Http/ReloadClient.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Client/ReloadClient.cs
@@ -1,8 +1,9 @@
-using DnsmasqWebUI.Client.Http.Abstractions;
-using DnsmasqWebUI.Services.Abstractions;
+using DnsmasqWebUI.Infrastructure.Client.Abstractions;
+using DnsmasqWebUI.Infrastructure.Helpers.Http;
+using DnsmasqWebUI.Infrastructure.Services.Abstractions;
using System.Net.Http.Json;
-namespace DnsmasqWebUI.Client.Http;
+namespace DnsmasqWebUI.Infrastructure.Client;
public sealed class ReloadClient : IReloadClient
{
@@ -14,7 +15,7 @@ public sealed class ReloadClient : IReloadClient
{
var response = await _http.PostAsync("api/reload", null, ct);
response.EnsureSuccessStatusCode();
- return await response.Content.ReadFromJsonAsync(ct)
+ return await response.Content.ReadFromJsonAsync(ApiJsonOptions.ClientOptions, ct)
?? throw new InvalidOperationException("Unexpected null from api/reload.");
}
}
diff --git a/src/DnsmasqWebUI/Client/Http/SameHostBaseAddressHandler.cs b/src/DnsmasqWebUI/Infrastructure/Client/SameHostBaseAddressHandler.cs
similarity index 92%
rename from src/DnsmasqWebUI/Client/Http/SameHostBaseAddressHandler.cs
rename to src/DnsmasqWebUI/Infrastructure/Client/SameHostBaseAddressHandler.cs
index 16820e2..1dbfcf8 100644
--- a/src/DnsmasqWebUI/Client/Http/SameHostBaseAddressHandler.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Client/SameHostBaseAddressHandler.cs
@@ -1,7 +1,7 @@
using System.Net;
-using DnsmasqWebUI.Services.Abstractions;
+using DnsmasqWebUI.Infrastructure.Services.Abstractions;
-namespace DnsmasqWebUI.Client.Http;
+namespace DnsmasqWebUI.Infrastructure.Client;
///
/// DelegatingHandler that rewrites relative request URIs to the current request's scheme, host, and path base.
diff --git a/src/DnsmasqWebUI/Infrastructure/Client/StatusClient.cs b/src/DnsmasqWebUI/Infrastructure/Client/StatusClient.cs
new file mode 100644
index 0000000..60c8637
--- /dev/null
+++ b/src/DnsmasqWebUI/Infrastructure/Client/StatusClient.cs
@@ -0,0 +1,18 @@
+using DnsmasqWebUI.Infrastructure.Client.Abstractions;
+using DnsmasqWebUI.Infrastructure.Helpers.Http;
+using DnsmasqWebUI.Models.Dnsmasq;
+using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
+using System.Net.Http.Json;
+
+namespace DnsmasqWebUI.Infrastructure.Client;
+
+public sealed class StatusClient : IStatusClient
+{
+ private readonly HttpClient _http;
+
+ public StatusClient(HttpClient http) => _http = http;
+
+ public async Task GetStatusAsync(CancellationToken ct = default) =>
+ await _http.GetFromJsonAsync("api/status", ApiJsonOptions.ClientOptions, ct)
+ ?? throw new InvalidOperationException("Unexpected null from api/status.");
+}
diff --git a/src/DnsmasqWebUI/Infrastructure/DnsmasqFileEncoding.cs b/src/DnsmasqWebUI/Infrastructure/DnsmasqFileEncoding.cs
deleted file mode 100644
index c9886cd..0000000
--- a/src/DnsmasqWebUI/Infrastructure/DnsmasqFileEncoding.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-using System.Text;
-
-namespace DnsmasqWebUI.Infrastructure;
-
-/// Encoding for dnsmasq config and hosts files. UTF-8 without BOM so dnsmasq does not see a BOM as "bad option" on line 1.
-public static class DnsmasqFileEncoding
-{
- public static readonly Encoding Utf8NoBom = new UTF8Encoding(false);
-}
diff --git a/src/DnsmasqWebUI/Models/Config/DnsmasqConfKeys.cs b/src/DnsmasqWebUI/Infrastructure/Helpers/Config/DnsmasqConfKeys.cs
similarity index 98%
rename from src/DnsmasqWebUI/Models/Config/DnsmasqConfKeys.cs
rename to src/DnsmasqWebUI/Infrastructure/Helpers/Config/DnsmasqConfKeys.cs
index 7471be8..0287c55 100644
--- a/src/DnsmasqWebUI/Models/Config/DnsmasqConfKeys.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Helpers/Config/DnsmasqConfKeys.cs
@@ -1,4 +1,4 @@
-namespace DnsmasqWebUI.Models.Config;
+namespace DnsmasqWebUI.Infrastructure.Helpers.Config;
///
/// Literal dnsmasq .conf option names (config keys). Use these everywhere we reference option names
diff --git a/src/DnsmasqWebUI/Infrastructure/Helpers/Config/DnsmasqFileEncoding.cs b/src/DnsmasqWebUI/Infrastructure/Helpers/Config/DnsmasqFileEncoding.cs
new file mode 100644
index 0000000..45aa110
--- /dev/null
+++ b/src/DnsmasqWebUI/Infrastructure/Helpers/Config/DnsmasqFileEncoding.cs
@@ -0,0 +1,28 @@
+using System.Text;
+
+namespace DnsmasqWebUI.Infrastructure.Helpers.Config;
+
+/// Encoding and reading helpers for dnsmasq config and hosts files. UTF-8 without BOM so dnsmasq does not see a BOM as "bad option" on line 1.
+public static class DnsmasqFileEncoding
+{
+ private const char Utf8BomChar = '\uFEFF';
+
+ public static readonly Encoding Utf8NoBom = new UTF8Encoding(false);
+
+ /// Read config file as UTF-8 and strip UTF-8 BOM from first line if present, so conf-dir= etc. are recognized.
+ public static string[] ReadConfigLines(string path)
+ {
+ var lines = File.ReadAllLines(path, Encoding.UTF8);
+ StripBomFromFirstLine(lines);
+ return lines;
+ }
+
+ /// Strip UTF-8 BOM from first line if present (mutates list). Use so we do not perpetuate BOM when writing.
+ public static void StripBomFromFirstLine(IList lines)
+ {
+ if (lines.Count == 0) return;
+ var first = lines[0];
+ if (first.Length > 0 && first[0] == Utf8BomChar)
+ lines[0] = first[1..];
+ }
+}
diff --git a/src/DnsmasqWebUI/Models/Config/DnsmasqOptionTooltips.cs b/src/DnsmasqWebUI/Infrastructure/Helpers/Config/DnsmasqOptionTooltips.cs
similarity index 98%
rename from src/DnsmasqWebUI/Models/Config/DnsmasqOptionTooltips.cs
rename to src/DnsmasqWebUI/Infrastructure/Helpers/Config/DnsmasqOptionTooltips.cs
index 0ec3c66..4d57ea8 100644
--- a/src/DnsmasqWebUI/Models/Config/DnsmasqOptionTooltips.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Helpers/Config/DnsmasqOptionTooltips.cs
@@ -1,6 +1,6 @@
using System.Collections.Frozen;
-namespace DnsmasqWebUI.Models.Config;
+namespace DnsmasqWebUI.Infrastructure.Helpers.Config;
///
/// Short explainer tooltips for dnsmasq option names. Shown on hover over the field label.
diff --git a/src/DnsmasqWebUI/Models/EffectiveConfig/EffectiveConfigFieldBuilder.cs b/src/DnsmasqWebUI/Infrastructure/Helpers/Config/EffectiveConfigFieldBuilder.cs
similarity index 98%
rename from src/DnsmasqWebUI/Models/EffectiveConfig/EffectiveConfigFieldBuilder.cs
rename to src/DnsmasqWebUI/Infrastructure/Helpers/Config/EffectiveConfigFieldBuilder.cs
index 97e9892..e80b2d5 100644
--- a/src/DnsmasqWebUI/Models/EffectiveConfig/EffectiveConfigFieldBuilder.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Helpers/Config/EffectiveConfigFieldBuilder.cs
@@ -1,7 +1,7 @@
-using DnsmasqWebUI.Models.Config;
-using DnsmasqWebUI.Models.Status;
+using DnsmasqWebUI.Models.Dnsmasq;
+using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
-namespace DnsmasqWebUI.Models.EffectiveConfig;
+namespace DnsmasqWebUI.Infrastructure.Helpers.Config;
///
/// Builds field descriptors for viewing and editing. Mapping is done once here via delegates passed to each descriptor.
diff --git a/src/DnsmasqWebUI/Infrastructure/Helpers/Http/ApiJsonOptions.cs b/src/DnsmasqWebUI/Infrastructure/Helpers/Http/ApiJsonOptions.cs
new file mode 100644
index 0000000..40d4a9e
--- /dev/null
+++ b/src/DnsmasqWebUI/Infrastructure/Helpers/Http/ApiJsonOptions.cs
@@ -0,0 +1,26 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace DnsmasqWebUI.Infrastructure.Helpers.Http;
+
+///
+/// Shared JSON options for API serialization (Controllers) and client deserialization (HTTP clients).
+/// Keeps camelCase and enum-as-string in one place so server and client stay in sync.
+///
+public static class ApiJsonOptions
+{
+ /// Options for Controllers (AddJsonOptions). Apply via ConfigureServer(JsonSerializerOptions).
+ public static void ConfigureServer(JsonSerializerOptions options)
+ {
+ options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
+ options.Converters.Add(new JsonStringEnumConverter());
+ }
+
+ /// Options for HTTP clients when deserializing API responses (camelCase, case-insensitive, enum as string).
+ public static JsonSerializerOptions ClientOptions { get; } = new()
+ {
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ PropertyNameCaseInsensitive = true,
+ Converters = { new JsonStringEnumConverter() }
+ };
+}
diff --git a/src/DnsmasqWebUI/Parsers/ConfParserHelpers.cs b/src/DnsmasqWebUI/Infrastructure/Parsers/ConfParserHelpers.cs
similarity index 98%
rename from src/DnsmasqWebUI/Parsers/ConfParserHelpers.cs
rename to src/DnsmasqWebUI/Infrastructure/Parsers/ConfParserHelpers.cs
index bde42ef..93b8a72 100644
--- a/src/DnsmasqWebUI/Parsers/ConfParserHelpers.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Parsers/ConfParserHelpers.cs
@@ -2,7 +2,7 @@ using Superpower;
using Superpower.Model;
using Superpower.Parsers;
-namespace DnsmasqWebUI.Parsers;
+namespace DnsmasqWebUI.Infrastructure.Parsers;
///
/// Shared Superpower text-parser helpers used by .conf and related parsers.
diff --git a/src/DnsmasqWebUI/Parsers/DnsmasqConfDhcpHostLineParser.cs b/src/DnsmasqWebUI/Infrastructure/Parsers/DnsmasqConfDhcpHostLineParser.cs
similarity index 99%
rename from src/DnsmasqWebUI/Parsers/DnsmasqConfDhcpHostLineParser.cs
rename to src/DnsmasqWebUI/Infrastructure/Parsers/DnsmasqConfDhcpHostLineParser.cs
index 47c76c5..d3f668d 100644
--- a/src/DnsmasqWebUI/Parsers/DnsmasqConfDhcpHostLineParser.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Parsers/DnsmasqConfDhcpHostLineParser.cs
@@ -3,7 +3,7 @@ using Superpower;
using Superpower.Model;
using Superpower.Parsers;
-namespace DnsmasqWebUI.Parsers;
+namespace DnsmasqWebUI.Infrastructure.Parsers;
///
/// Parses a single dhcp-host= line from a dnsmasq .conf file. Same format as long option without --.
diff --git a/src/DnsmasqWebUI/Parsers/DnsmasqConfDirectiveParser.cs b/src/DnsmasqWebUI/Infrastructure/Parsers/DnsmasqConfDirectiveParser.cs
similarity index 98%
rename from src/DnsmasqWebUI/Parsers/DnsmasqConfDirectiveParser.cs
rename to src/DnsmasqWebUI/Infrastructure/Parsers/DnsmasqConfDirectiveParser.cs
index 7368016..b03b78f 100644
--- a/src/DnsmasqWebUI/Parsers/DnsmasqConfDirectiveParser.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Parsers/DnsmasqConfDirectiveParser.cs
@@ -2,7 +2,7 @@ using Superpower;
using Superpower.Model;
using Superpower.Parsers;
-namespace DnsmasqWebUI.Parsers;
+namespace DnsmasqWebUI.Infrastructure.Parsers;
///
/// Key/value parsing for dnsmasq .conf lines. Used by to build effective config.
diff --git a/src/DnsmasqWebUI/Parsers/DnsmasqConfFileLineParser.cs b/src/DnsmasqWebUI/Infrastructure/Parsers/DnsmasqConfFileLineParser.cs
similarity index 97%
rename from src/DnsmasqWebUI/Parsers/DnsmasqConfFileLineParser.cs
rename to src/DnsmasqWebUI/Infrastructure/Parsers/DnsmasqConfFileLineParser.cs
index 383fc71..ea6c1c7 100644
--- a/src/DnsmasqWebUI/Parsers/DnsmasqConfFileLineParser.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Parsers/DnsmasqConfFileLineParser.cs
@@ -1,10 +1,10 @@
using DnsmasqWebUI.Models.Config;
-using DnsmasqWebUI.Models.EffectiveConfig;
+using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
using Superpower;
using Superpower.Model;
using Superpower.Parsers;
-namespace DnsmasqWebUI.Parsers;
+namespace DnsmasqWebUI.Infrastructure.Parsers;
///
/// Parses a dnsmasq .conf file line-by-line into variants (BlankLine, CommentLine, DhcpHostLine, OtherLine).
diff --git a/src/DnsmasqWebUI/Parsers/DnsmasqConfIncludeParser.cs b/src/DnsmasqWebUI/Infrastructure/Parsers/DnsmasqConfIncludeParser.cs
similarity index 99%
rename from src/DnsmasqWebUI/Parsers/DnsmasqConfIncludeParser.cs
rename to src/DnsmasqWebUI/Infrastructure/Parsers/DnsmasqConfIncludeParser.cs
index 1bace32..1d54498 100644
--- a/src/DnsmasqWebUI/Parsers/DnsmasqConfIncludeParser.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Parsers/DnsmasqConfIncludeParser.cs
@@ -1,10 +1,11 @@
-using DnsmasqWebUI.Models.Config;
-using DnsmasqWebUI.Models.EffectiveConfig;
+using DnsmasqWebUI.Infrastructure.Helpers.Config;
+using DnsmasqWebUI.Models.Dnsmasq;
+using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
using Superpower;
using Superpower.Model;
using Superpower.Parsers;
-namespace DnsmasqWebUI.Parsers;
+namespace DnsmasqWebUI.Infrastructure.Parsers;
///
/// Parses the main dnsmasq .conf file only for conf-file= and conf-dir= directives
@@ -21,6 +22,7 @@ public static class DnsmasqConfIncludeParser
private static string[] GetLines(string path, IReadOnlyDictionary pathToLines) =>
pathToLines.TryGetValue(path, out var L) ? L : Array.Empty();
+
///
/// Returns the ordered list of absolute config file paths dnsmasq loads, in the exact order
/// dnsmasq reads them (main interleaved with conf-file and conf-dir).
@@ -71,7 +73,7 @@ public static class DnsmasqConfIncludeParser
if (!seenPaths.Add(canonicalPath))
yield break; // dnsmasq skips re-reading same file (by inode; we use path)
- var lines = File.ReadAllLines(currentFilePath);
+ var lines = DnsmasqFileEncoding.ReadConfigLines(currentFilePath);
var dir = Path.GetDirectoryName(canonicalPath) ?? "";
// Yield "file started" so callers (e.g. GetIncludedPathsWithSource) can add this file even if it only has conf-file/conf-dir lines.
@@ -100,6 +102,7 @@ public static class DnsmasqConfIncludeParser
var (directory, matchSuffix, ignoreSuffix) = ParseConfDirValue(value);
if (string.IsNullOrEmpty(directory)) continue;
var resolvedDir = ResolvePath(directory, dir) ?? directory;
+ resolvedDir = Path.GetFullPath(resolvedDir);
if (!Directory.Exists(resolvedDir)) continue;
var files = GetConfDirFilesSorted(resolvedDir, matchSuffix, ignoreSuffix);
foreach (var f in files)
@@ -191,7 +194,7 @@ public static class DnsmasqConfIncludeParser
if (!File.Exists(mainFull))
return null;
- foreach (var line in File.ReadAllLines(mainFull))
+ foreach (var line in DnsmasqFileEncoding.ReadConfigLines(mainFull))
{
var kv = DnsmasqConfDirectiveParser.TryParseKeyValue(line);
if (kv == null)
diff --git a/src/DnsmasqWebUI/Parsers/DnsmasqLeasesFileLineParser.cs b/src/DnsmasqWebUI/Infrastructure/Parsers/DnsmasqLeasesFileLineParser.cs
similarity index 97%
rename from src/DnsmasqWebUI/Parsers/DnsmasqLeasesFileLineParser.cs
rename to src/DnsmasqWebUI/Infrastructure/Parsers/DnsmasqLeasesFileLineParser.cs
index ce7ae85..dafeb83 100644
--- a/src/DnsmasqWebUI/Parsers/DnsmasqLeasesFileLineParser.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Parsers/DnsmasqLeasesFileLineParser.cs
@@ -3,7 +3,7 @@ using Superpower;
using Superpower.Model;
using Superpower.Parsers;
-namespace DnsmasqWebUI.Parsers;
+namespace DnsmasqWebUI.Infrastructure.Parsers;
///
/// Parses a single line of the dnsmasq DHCPv4 lease file (e.g. /var/lib/misc/dnsmasq.leases).
diff --git a/src/DnsmasqWebUI/Parsers/HostsFileLineParser.cs b/src/DnsmasqWebUI/Infrastructure/Parsers/HostsFileLineParser.cs
similarity index 98%
rename from src/DnsmasqWebUI/Parsers/HostsFileLineParser.cs
rename to src/DnsmasqWebUI/Infrastructure/Parsers/HostsFileLineParser.cs
index ebdba59..07c07f9 100644
--- a/src/DnsmasqWebUI/Parsers/HostsFileLineParser.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Parsers/HostsFileLineParser.cs
@@ -3,7 +3,7 @@ using Superpower;
using Superpower.Model;
using Superpower.Parsers;
-namespace DnsmasqWebUI.Parsers;
+namespace DnsmasqWebUI.Infrastructure.Parsers;
///
/// Parses a single line of an /etc/hosts-style file. Format: IP_address canonical_hostname [aliases...]
diff --git a/src/DnsmasqWebUI/Services/Abstractions/IApplicationHostedService.cs b/src/DnsmasqWebUI/Infrastructure/Services/Abstractions/IApplicationHostedService.cs
similarity index 84%
rename from src/DnsmasqWebUI/Services/Abstractions/IApplicationHostedService.cs
rename to src/DnsmasqWebUI/Infrastructure/Services/Abstractions/IApplicationHostedService.cs
index 2fcec5e..b3e06d5 100644
--- a/src/DnsmasqWebUI/Services/Abstractions/IApplicationHostedService.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Services/Abstractions/IApplicationHostedService.cs
@@ -1,6 +1,6 @@
using Microsoft.Extensions.Hosting;
-namespace DnsmasqWebUI.Services.Abstractions;
+namespace DnsmasqWebUI.Infrastructure.Services.Abstractions;
///
/// Marker interface for application hosted services that are registered via assembly scanning.
diff --git a/src/DnsmasqWebUI/Services/Abstractions/IApplicationScopedConcrete.cs b/src/DnsmasqWebUI/Infrastructure/Services/Abstractions/IApplicationScopedConcrete.cs
similarity index 79%
rename from src/DnsmasqWebUI/Services/Abstractions/IApplicationScopedConcrete.cs
rename to src/DnsmasqWebUI/Infrastructure/Services/Abstractions/IApplicationScopedConcrete.cs
index ffe1125..e4e367f 100644
--- a/src/DnsmasqWebUI/Services/Abstractions/IApplicationScopedConcrete.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Services/Abstractions/IApplicationScopedConcrete.cs
@@ -1,4 +1,4 @@
-namespace DnsmasqWebUI.Services.Abstractions;
+namespace DnsmasqWebUI.Infrastructure.Services.Abstractions;
///
/// Marker for types that are registered as their own concrete type with scoped lifetime.
diff --git a/src/DnsmasqWebUI/Services/Abstractions/IApplicationScopedService.cs b/src/DnsmasqWebUI/Infrastructure/Services/Abstractions/IApplicationScopedService.cs
similarity index 77%
rename from src/DnsmasqWebUI/Services/Abstractions/IApplicationScopedService.cs
rename to src/DnsmasqWebUI/Infrastructure/Services/Abstractions/IApplicationScopedService.cs
index 59c1d38..cfc92be 100644
--- a/src/DnsmasqWebUI/Services/Abstractions/IApplicationScopedService.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Services/Abstractions/IApplicationScopedService.cs
@@ -1,4 +1,4 @@
-namespace DnsmasqWebUI.Services.Abstractions;
+namespace DnsmasqWebUI.Infrastructure.Services.Abstractions;
///
/// Marker interface for application services that are registered as scoped.
diff --git a/src/DnsmasqWebUI/Services/Abstractions/IApplicationSingleton.cs b/src/DnsmasqWebUI/Infrastructure/Services/Abstractions/IApplicationSingleton.cs
similarity index 77%
rename from src/DnsmasqWebUI/Services/Abstractions/IApplicationSingleton.cs
rename to src/DnsmasqWebUI/Infrastructure/Services/Abstractions/IApplicationSingleton.cs
index 8c6d485..0cbf4aa 100644
--- a/src/DnsmasqWebUI/Services/Abstractions/IApplicationSingleton.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Services/Abstractions/IApplicationSingleton.cs
@@ -1,4 +1,4 @@
-namespace DnsmasqWebUI.Services.Abstractions;
+namespace DnsmasqWebUI.Infrastructure.Services.Abstractions;
///
/// Marker interface for application services that are registered as singleton.
diff --git a/src/DnsmasqWebUI/Client/Services/Abstractions/IClientSettingsService.cs b/src/DnsmasqWebUI/Infrastructure/Services/Abstractions/IClientSettingsService.cs
similarity index 81%
rename from src/DnsmasqWebUI/Client/Services/Abstractions/IClientSettingsService.cs
rename to src/DnsmasqWebUI/Infrastructure/Services/Abstractions/IClientSettingsService.cs
index a34ec08..76e6484 100644
--- a/src/DnsmasqWebUI/Client/Services/Abstractions/IClientSettingsService.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Services/Abstractions/IClientSettingsService.cs
@@ -1,7 +1,6 @@
-using DnsmasqWebUI.Client.Models;
-using DnsmasqWebUI.Services.Abstractions;
+using DnsmasqWebUI.Models.Client;
-namespace DnsmasqWebUI.Client.Services.Abstractions;
+namespace DnsmasqWebUI.Infrastructure.Services.Abstractions;
///
/// Loads and saves client-side preferences (e.g. polling intervals) to browser local storage.
diff --git a/src/DnsmasqWebUI/Services/Abstractions/IConfigSetCache.cs b/src/DnsmasqWebUI/Infrastructure/Services/Abstractions/IConfigSetCache.cs
similarity index 91%
rename from src/DnsmasqWebUI/Services/Abstractions/IConfigSetCache.cs
rename to src/DnsmasqWebUI/Infrastructure/Services/Abstractions/IConfigSetCache.cs
index 0e1adf5..fe80d32 100644
--- a/src/DnsmasqWebUI/Services/Abstractions/IConfigSetCache.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Services/Abstractions/IConfigSetCache.cs
@@ -1,4 +1,6 @@
-namespace DnsmasqWebUI.Services.Abstractions;
+using DnsmasqWebUI.Models.Contracts;
+
+namespace DnsmasqWebUI.Infrastructure.Services.Abstractions;
///
/// Singleton cache for the config set (main + conf-dir + managed file). Invalidates on file watchers, staleness, or manual Invalidate.
diff --git a/src/DnsmasqWebUI/Services/Abstractions/IDnsmasqConfigService.cs b/src/DnsmasqWebUI/Infrastructure/Services/Abstractions/IDnsmasqConfigService.cs
similarity index 61%
rename from src/DnsmasqWebUI/Services/Abstractions/IDnsmasqConfigService.cs
rename to src/DnsmasqWebUI/Infrastructure/Services/Abstractions/IDnsmasqConfigService.cs
index 1e20303..6205c02 100644
--- a/src/DnsmasqWebUI/Services/Abstractions/IDnsmasqConfigService.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Services/Abstractions/IDnsmasqConfigService.cs
@@ -1,8 +1,10 @@
using DnsmasqWebUI.Models.Config;
+using DnsmasqWebUI.Models.Contracts;
using DnsmasqWebUI.Models.Dhcp;
-using DnsmasqWebUI.Models.EffectiveConfig;
+using DnsmasqWebUI.Models.Dnsmasq;
+using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
-namespace DnsmasqWebUI.Services.Abstractions;
+namespace DnsmasqWebUI.Infrastructure.Services.Abstractions;
public interface IDnsmasqConfigService : IApplicationScopedService
{
@@ -11,6 +13,3 @@ public interface IDnsmasqConfigService : IApplicationScopedService
Task ReadManagedConfigAsync(CancellationToken ct = default);
Task WriteManagedConfigAsync(IReadOnlyList lines, CancellationToken ct = default);
}
-
-/// Full managed file content. EffectiveHostsPathInFile is parsed from AddnHosts line if present (for display only).
-public record ManagedConfigContent(IReadOnlyList Lines, string EffectiveHostsPathInFile);
diff --git a/src/DnsmasqWebUI/Services/Abstractions/IDnsmasqConfigSetService.cs b/src/DnsmasqWebUI/Infrastructure/Services/Abstractions/IDnsmasqConfigSetService.cs
similarity index 89%
rename from src/DnsmasqWebUI/Services/Abstractions/IDnsmasqConfigSetService.cs
rename to src/DnsmasqWebUI/Infrastructure/Services/Abstractions/IDnsmasqConfigSetService.cs
index 0c321e8..0266518 100644
--- a/src/DnsmasqWebUI/Services/Abstractions/IDnsmasqConfigSetService.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Services/Abstractions/IDnsmasqConfigSetService.cs
@@ -1,6 +1,7 @@
-using DnsmasqWebUI.Models.EffectiveConfig;
+using DnsmasqWebUI.Models.Dnsmasq;
+using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
-namespace DnsmasqWebUI.Services.Abstractions;
+namespace DnsmasqWebUI.Infrastructure.Services.Abstractions;
/// Discovers the dnsmasq config set (main + conf-file/conf-dir) and the managed file path.
public interface IDnsmasqConfigSetService : IApplicationSingleton
diff --git a/src/DnsmasqWebUI/Services/Abstractions/IEffectiveConfigRenderFragmentRegistry.cs b/src/DnsmasqWebUI/Infrastructure/Services/Abstractions/IEffectiveConfigRenderFragmentRegistry.cs
similarity index 82%
rename from src/DnsmasqWebUI/Services/Abstractions/IEffectiveConfigRenderFragmentRegistry.cs
rename to src/DnsmasqWebUI/Infrastructure/Services/Abstractions/IEffectiveConfigRenderFragmentRegistry.cs
index f3a5f78..f47c024 100644
--- a/src/DnsmasqWebUI/Services/Abstractions/IEffectiveConfigRenderFragmentRegistry.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Services/Abstractions/IEffectiveConfigRenderFragmentRegistry.cs
@@ -1,7 +1,8 @@
using Microsoft.AspNetCore.Components;
-using DnsmasqWebUI.Models.EffectiveConfig;
+using DnsmasqWebUI.Models.Dnsmasq;
+using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
-namespace DnsmasqWebUI.Services.Abstractions;
+namespace DnsmasqWebUI.Infrastructure.Services.Abstractions;
///
/// Lookup for custom display fragments per effective-config field (section + option).
diff --git a/src/DnsmasqWebUI/Services/Abstractions/IHostsCache.cs b/src/DnsmasqWebUI/Infrastructure/Services/Abstractions/IHostsCache.cs
similarity index 85%
rename from src/DnsmasqWebUI/Services/Abstractions/IHostsCache.cs
rename to src/DnsmasqWebUI/Infrastructure/Services/Abstractions/IHostsCache.cs
index 25912ed..46b8f92 100644
--- a/src/DnsmasqWebUI/Services/Abstractions/IHostsCache.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Services/Abstractions/IHostsCache.cs
@@ -1,6 +1,7 @@
+using DnsmasqWebUI.Models.Contracts;
using DnsmasqWebUI.Models.Hosts;
-namespace DnsmasqWebUI.Services.Abstractions;
+namespace DnsmasqWebUI.Infrastructure.Services.Abstractions;
///
/// Singleton cache for hosts: managed hosts file + read-only (system + addn-hosts). Invalidates on watchers, staleness, or Invalidate.
diff --git a/src/DnsmasqWebUI/Services/Abstractions/IHostsFileService.cs b/src/DnsmasqWebUI/Infrastructure/Services/Abstractions/IHostsFileService.cs
similarity index 81%
rename from src/DnsmasqWebUI/Services/Abstractions/IHostsFileService.cs
rename to src/DnsmasqWebUI/Infrastructure/Services/Abstractions/IHostsFileService.cs
index 5417bcf..de8ce7e 100644
--- a/src/DnsmasqWebUI/Services/Abstractions/IHostsFileService.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Services/Abstractions/IHostsFileService.cs
@@ -1,6 +1,6 @@
using DnsmasqWebUI.Models.Hosts;
-namespace DnsmasqWebUI.Services.Abstractions;
+namespace DnsmasqWebUI.Infrastructure.Services.Abstractions;
public interface IHostsFileService : IApplicationScopedService
{
diff --git a/src/DnsmasqWebUI/Services/Abstractions/ILeasesCache.cs b/src/DnsmasqWebUI/Infrastructure/Services/Abstractions/ILeasesCache.cs
similarity index 89%
rename from src/DnsmasqWebUI/Services/Abstractions/ILeasesCache.cs
rename to src/DnsmasqWebUI/Infrastructure/Services/Abstractions/ILeasesCache.cs
index 2c54f3e..d70da22 100644
--- a/src/DnsmasqWebUI/Services/Abstractions/ILeasesCache.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Services/Abstractions/ILeasesCache.cs
@@ -1,6 +1,6 @@
using DnsmasqWebUI.Models.Dhcp;
-namespace DnsmasqWebUI.Services.Abstractions;
+namespace DnsmasqWebUI.Infrastructure.Services.Abstractions;
///
/// Singleton cache for the leases file; invalidates when the file changes (e.g. via file watcher).
diff --git a/src/DnsmasqWebUI/Services/Abstractions/ILeasesFileService.cs b/src/DnsmasqWebUI/Infrastructure/Services/Abstractions/ILeasesFileService.cs
similarity index 82%
rename from src/DnsmasqWebUI/Services/Abstractions/ILeasesFileService.cs
rename to src/DnsmasqWebUI/Infrastructure/Services/Abstractions/ILeasesFileService.cs
index 0d2ed9e..9b6f45d 100644
--- a/src/DnsmasqWebUI/Services/Abstractions/ILeasesFileService.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Services/Abstractions/ILeasesFileService.cs
@@ -1,6 +1,6 @@
using DnsmasqWebUI.Models.Dhcp;
-namespace DnsmasqWebUI.Services.Abstractions;
+namespace DnsmasqWebUI.Infrastructure.Services.Abstractions;
public interface ILeasesFileService : IApplicationScopedService
{
diff --git a/src/DnsmasqWebUI/Services/Abstractions/IProcessRunner.cs b/src/DnsmasqWebUI/Infrastructure/Services/Abstractions/IProcessRunner.cs
similarity index 83%
rename from src/DnsmasqWebUI/Services/Abstractions/IProcessRunner.cs
rename to src/DnsmasqWebUI/Infrastructure/Services/Abstractions/IProcessRunner.cs
index aaee890..26b539a 100644
--- a/src/DnsmasqWebUI/Services/Abstractions/IProcessRunner.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Services/Abstractions/IProcessRunner.cs
@@ -1,4 +1,6 @@
-namespace DnsmasqWebUI.Services.Abstractions;
+using DnsmasqWebUI.Models.Contracts;
+
+namespace DnsmasqWebUI.Infrastructure.Services.Abstractions;
/// Runs a shell command with timeout and cancellation. Used for status, reload, status show, and logs commands.
public interface IProcessRunner : IApplicationScopedService
diff --git a/src/DnsmasqWebUI/Services/Abstractions/IReloadService.cs b/src/DnsmasqWebUI/Infrastructure/Services/Abstractions/IReloadService.cs
similarity index 78%
rename from src/DnsmasqWebUI/Services/Abstractions/IReloadService.cs
rename to src/DnsmasqWebUI/Infrastructure/Services/Abstractions/IReloadService.cs
index a61ca70..b1d8bbd 100644
--- a/src/DnsmasqWebUI/Services/Abstractions/IReloadService.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Services/Abstractions/IReloadService.cs
@@ -1,4 +1,4 @@
-namespace DnsmasqWebUI.Services.Abstractions;
+namespace DnsmasqWebUI.Infrastructure.Services.Abstractions;
public interface IReloadService : IApplicationScopedService
{
diff --git a/src/DnsmasqWebUI/Client/Services/ClientSettingsService.cs b/src/DnsmasqWebUI/Infrastructure/Services/ClientSettingsService.cs
similarity index 93%
rename from src/DnsmasqWebUI/Client/Services/ClientSettingsService.cs
rename to src/DnsmasqWebUI/Infrastructure/Services/ClientSettingsService.cs
index 528b6be..2d42405 100644
--- a/src/DnsmasqWebUI/Client/Services/ClientSettingsService.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Services/ClientSettingsService.cs
@@ -1,9 +1,9 @@
using System.Text.Json;
-using DnsmasqWebUI.Client.Models;
-using DnsmasqWebUI.Client.Services.Abstractions;
+using DnsmasqWebUI.Models.Client;
+using DnsmasqWebUI.Infrastructure.Services.Abstractions;
using Microsoft.JSInterop;
-namespace DnsmasqWebUI.Client.Services;
+namespace DnsmasqWebUI.Infrastructure.Services;
///
/// Loads and saves client-side settings to browser local storage via JS interop.
diff --git a/src/DnsmasqWebUI/Services/ConfigSetCache.cs b/src/DnsmasqWebUI/Infrastructure/Services/ConfigSetCache.cs
similarity index 98%
rename from src/DnsmasqWebUI/Services/ConfigSetCache.cs
rename to src/DnsmasqWebUI/Infrastructure/Services/ConfigSetCache.cs
index 72615ca..51802d4 100644
--- a/src/DnsmasqWebUI/Services/ConfigSetCache.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Services/ConfigSetCache.cs
@@ -1,13 +1,14 @@
-using System.Text;
-using DnsmasqWebUI.Configuration;
using DnsmasqWebUI.Models.Config;
+using DnsmasqWebUI.Models.Contracts;
+using DnsmasqWebUI.Infrastructure.Helpers.Config;
using DnsmasqWebUI.Models.Dhcp;
-using DnsmasqWebUI.Models.EffectiveConfig;
-using DnsmasqWebUI.Parsers;
-using DnsmasqWebUI.Services.Abstractions;
+using DnsmasqWebUI.Models.Dnsmasq;
+using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
+using DnsmasqWebUI.Infrastructure.Parsers;
+using DnsmasqWebUI.Infrastructure.Services.Abstractions;
using Microsoft.Extensions.Options;
-namespace DnsmasqWebUI.Services;
+namespace DnsmasqWebUI.Infrastructure.Services;
///
/// Singleton cache for the config set (main + conf-dir + managed file). Reads all files once per refresh,
@@ -203,7 +204,7 @@ public sealed class ConfigSetCache : IConfigSetCache, IDisposable
}
try
{
- dict[canonical] = File.ReadAllLines(path, Encoding.UTF8);
+ dict[canonical] = DnsmasqFileEncoding.ReadConfigLines(path);
}
catch
{
diff --git a/src/DnsmasqWebUI/Services/DnsmasqConfigService.cs b/src/DnsmasqWebUI/Infrastructure/Services/DnsmasqConfigService.cs
similarity index 93%
rename from src/DnsmasqWebUI/Services/DnsmasqConfigService.cs
rename to src/DnsmasqWebUI/Infrastructure/Services/DnsmasqConfigService.cs
index 0bfae0f..f19bd53 100644
--- a/src/DnsmasqWebUI/Services/DnsmasqConfigService.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Services/DnsmasqConfigService.cs
@@ -1,12 +1,14 @@
using System.Text;
-using DnsmasqWebUI.Infrastructure;
+using DnsmasqWebUI.Infrastructure.Helpers.Config;
using DnsmasqWebUI.Models.Config;
+using DnsmasqWebUI.Models.Contracts;
using DnsmasqWebUI.Models.Dhcp;
-using DnsmasqWebUI.Models.EffectiveConfig;
-using DnsmasqWebUI.Parsers;
-using DnsmasqWebUI.Services.Abstractions;
+using DnsmasqWebUI.Models.Dnsmasq;
+using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
+using DnsmasqWebUI.Infrastructure.Parsers;
+using DnsmasqWebUI.Infrastructure.Services.Abstractions;
-namespace DnsmasqWebUI.Services;
+namespace DnsmasqWebUI.Infrastructure.Services;
public class DnsmasqConfigService : IDnsmasqConfigService
{
@@ -113,7 +115,9 @@ public class DnsmasqConfigService : IDnsmasqConfigService
if (File.Exists(path))
{
var read = await File.ReadAllLinesAsync(path, DnsmasqFileEncoding.Utf8NoBom, ct);
- rawLines = StripBomFromFirstLine(read);
+ var list = read.ToList();
+ DnsmasqFileEncoding.StripBomFromFirstLine(list);
+ rawLines = list;
}
else
rawLines = Array.Empty();
@@ -203,17 +207,4 @@ public class DnsmasqConfigService : IDnsmasqConfigService
_logger.LogInformation("Wrote managed config file: {Path}", path);
}
- /// Strip UTF-8 BOM from first line if present.
- private static IReadOnlyList StripBomFromFirstLine(IReadOnlyList lines)
- {
- if (lines.Count == 0) return lines;
- var first = lines[0];
- if (first.Length > 0 && first[0] == '\uFEFF')
- {
- var list = lines.ToList();
- list[0] = first[1..];
- return list;
- }
- return lines;
- }
}
diff --git a/src/DnsmasqWebUI/Services/DnsmasqConfigSetService.cs b/src/DnsmasqWebUI/Infrastructure/Services/DnsmasqConfigSetService.cs
similarity index 92%
rename from src/DnsmasqWebUI/Services/DnsmasqConfigSetService.cs
rename to src/DnsmasqWebUI/Infrastructure/Services/DnsmasqConfigSetService.cs
index 259e2d1..f7b4ea3 100644
--- a/src/DnsmasqWebUI/Services/DnsmasqConfigSetService.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Services/DnsmasqConfigSetService.cs
@@ -1,7 +1,9 @@
-using DnsmasqWebUI.Models.EffectiveConfig;
-using DnsmasqWebUI.Services.Abstractions;
+using DnsmasqWebUI.Models.Dnsmasq;
+using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
+using DnsmasqWebUI.Models.Contracts;
+using DnsmasqWebUI.Infrastructure.Services.Abstractions;
-namespace DnsmasqWebUI.Services;
+namespace DnsmasqWebUI.Infrastructure.Services;
public class DnsmasqConfigSetService : IDnsmasqConfigSetService
{
diff --git a/src/DnsmasqWebUI/Services/EffectiveConfigRenderFragmentRegistry.cs b/src/DnsmasqWebUI/Infrastructure/Services/EffectiveConfigRenderFragmentRegistry.cs
similarity index 91%
rename from src/DnsmasqWebUI/Services/EffectiveConfigRenderFragmentRegistry.cs
rename to src/DnsmasqWebUI/Infrastructure/Services/EffectiveConfigRenderFragmentRegistry.cs
index 05d8ee4..80b24fc 100644
--- a/src/DnsmasqWebUI/Services/EffectiveConfigRenderFragmentRegistry.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Services/EffectiveConfigRenderFragmentRegistry.cs
@@ -1,10 +1,11 @@
using Microsoft.AspNetCore.Components;
-using DnsmasqWebUI.Models.Config;
-using DnsmasqWebUI.Models.EffectiveConfig;
-using DnsmasqWebUI.Services.Abstractions;
+using DnsmasqWebUI.Infrastructure.Helpers.Config;
+using DnsmasqWebUI.Models.Dnsmasq;
+using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
+using DnsmasqWebUI.Infrastructure.Services.Abstractions;
using DnsmasqWebUI.Components.Dnsmasq.EffectiveConfig.CustomDisplays;
-namespace DnsmasqWebUI.Services;
+namespace DnsmasqWebUI.Infrastructure.Services;
///
/// Maps (sectionId, optionName) to custom effective-config value display component types and builds
diff --git a/src/DnsmasqWebUI/Services/EnsureManagedConfigHostedService.cs b/src/DnsmasqWebUI/Infrastructure/Services/EnsureManagedConfigHostedService.cs
similarity index 88%
rename from src/DnsmasqWebUI/Services/EnsureManagedConfigHostedService.cs
rename to src/DnsmasqWebUI/Infrastructure/Services/EnsureManagedConfigHostedService.cs
index e949376..a564722 100644
--- a/src/DnsmasqWebUI/Services/EnsureManagedConfigHostedService.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Services/EnsureManagedConfigHostedService.cs
@@ -1,13 +1,12 @@
using System.Text;
-using DnsmasqWebUI.Infrastructure;
+using DnsmasqWebUI.Infrastructure.Helpers.Config;
using DnsmasqWebUI.Models.Config;
-using DnsmasqWebUI.Models.EffectiveConfig;
-using DnsmasqWebUI.Configuration;
-using DnsmasqWebUI.Parsers;
-using DnsmasqWebUI.Services.Abstractions;
+using DnsmasqWebUI.Models.Dnsmasq;
+using DnsmasqWebUI.Infrastructure.Parsers;
+using DnsmasqWebUI.Infrastructure.Services.Abstractions;
using Microsoft.Extensions.Options;
-namespace DnsmasqWebUI.Services;
+namespace DnsmasqWebUI.Infrastructure.Services;
///
/// Runs once at startup: ensures the managed file is included only by a conf-file= directive (never conf-dir=),
@@ -52,7 +51,7 @@ public class EnsureManagedConfigHostedService : IApplicationHostedService
var lines = File.Exists(mainFull)
? (await File.ReadAllLinesAsync(mainFull, DnsmasqFileEncoding.Utf8NoBom, cancellationToken)).ToList()
: new List();
- StripBomFromFirstLine(lines);
+ DnsmasqFileEncoding.StripBomFromFirstLine(lines);
// Remove any conf-file= line that points to our managed path (we will add it as the last line).
for (var i = lines.Count - 1; i >= 0; i--)
@@ -96,14 +95,5 @@ public class EnsureManagedConfigHostedService : IApplicationHostedService
await configService.WriteManagedConfigAsync(Array.Empty(), cancellationToken);
}
- /// Strip UTF-8 BOM from first line if present, so we do not perpetuate it when writing.
- private static void StripBomFromFirstLine(List lines)
- {
- if (lines.Count == 0) return;
- var first = lines[0];
- if (first.Length > 0 && first[0] == '\uFEFF')
- lines[0] = first[1..];
- }
-
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}
diff --git a/src/DnsmasqWebUI/Services/HostsCache.cs b/src/DnsmasqWebUI/Infrastructure/Services/HostsCache.cs
similarity index 97%
rename from src/DnsmasqWebUI/Services/HostsCache.cs
rename to src/DnsmasqWebUI/Infrastructure/Services/HostsCache.cs
index 5a6c72f..be7d69b 100644
--- a/src/DnsmasqWebUI/Services/HostsCache.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Services/HostsCache.cs
@@ -1,11 +1,12 @@
using System.Text;
-using DnsmasqWebUI.Configuration;
+using DnsmasqWebUI.Models.Config;
+using DnsmasqWebUI.Models.Contracts;
using DnsmasqWebUI.Models.Hosts;
-using DnsmasqWebUI.Parsers;
-using DnsmasqWebUI.Services.Abstractions;
+using DnsmasqWebUI.Infrastructure.Parsers;
+using DnsmasqWebUI.Infrastructure.Services.Abstractions;
using Microsoft.Extensions.Options;
-namespace DnsmasqWebUI.Services;
+namespace DnsmasqWebUI.Infrastructure.Services;
///
/// Singleton cache for hosts: managed hosts file + read-only (system + addn-hosts). Invalidates on watchers (managed + system path), staleness, or Invalidate.
diff --git a/src/DnsmasqWebUI/Services/HostsFileService.cs b/src/DnsmasqWebUI/Infrastructure/Services/HostsFileService.cs
similarity index 89%
rename from src/DnsmasqWebUI/Services/HostsFileService.cs
rename to src/DnsmasqWebUI/Infrastructure/Services/HostsFileService.cs
index 78fe5e4..2ac3487 100644
--- a/src/DnsmasqWebUI/Services/HostsFileService.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Services/HostsFileService.cs
@@ -1,9 +1,9 @@
-using DnsmasqWebUI.Infrastructure;
+using DnsmasqWebUI.Infrastructure.Helpers.Config;
using DnsmasqWebUI.Models.Hosts;
-using DnsmasqWebUI.Parsers;
-using DnsmasqWebUI.Services.Abstractions;
+using DnsmasqWebUI.Infrastructure.Parsers;
+using DnsmasqWebUI.Infrastructure.Services.Abstractions;
-namespace DnsmasqWebUI.Services;
+namespace DnsmasqWebUI.Infrastructure.Services;
public class HostsFileService : IHostsFileService
{
diff --git a/src/DnsmasqWebUI/Services/LeasesCache.cs b/src/DnsmasqWebUI/Infrastructure/Services/LeasesCache.cs
similarity index 96%
rename from src/DnsmasqWebUI/Services/LeasesCache.cs
rename to src/DnsmasqWebUI/Infrastructure/Services/LeasesCache.cs
index 2545149..197dcc9 100644
--- a/src/DnsmasqWebUI/Services/LeasesCache.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Services/LeasesCache.cs
@@ -1,9 +1,9 @@
using System.Text;
using DnsmasqWebUI.Models.Dhcp;
-using DnsmasqWebUI.Parsers;
-using DnsmasqWebUI.Services.Abstractions;
+using DnsmasqWebUI.Infrastructure.Parsers;
+using DnsmasqWebUI.Infrastructure.Services.Abstractions;
-namespace DnsmasqWebUI.Services;
+namespace DnsmasqWebUI.Infrastructure.Services;
///
/// Singleton cache for the leases file with a so we re-read only when the file changes.
diff --git a/src/DnsmasqWebUI/Services/LeasesFileService.cs b/src/DnsmasqWebUI/Infrastructure/Services/LeasesFileService.cs
similarity index 85%
rename from src/DnsmasqWebUI/Services/LeasesFileService.cs
rename to src/DnsmasqWebUI/Infrastructure/Services/LeasesFileService.cs
index f8538a4..9e76972 100644
--- a/src/DnsmasqWebUI/Services/LeasesFileService.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Services/LeasesFileService.cs
@@ -1,7 +1,7 @@
using DnsmasqWebUI.Models.Dhcp;
-using DnsmasqWebUI.Services.Abstractions;
+using DnsmasqWebUI.Infrastructure.Services.Abstractions;
-namespace DnsmasqWebUI.Services;
+namespace DnsmasqWebUI.Infrastructure.Services;
public class LeasesFileService : ILeasesFileService
{
diff --git a/src/DnsmasqWebUI/Services/ProcessRunner.cs b/src/DnsmasqWebUI/Infrastructure/Services/ProcessRunner.cs
similarity index 95%
rename from src/DnsmasqWebUI/Services/ProcessRunner.cs
rename to src/DnsmasqWebUI/Infrastructure/Services/ProcessRunner.cs
index 3c7436f..b39c0ac 100644
--- a/src/DnsmasqWebUI/Services/ProcessRunner.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Services/ProcessRunner.cs
@@ -1,9 +1,10 @@
using System.Diagnostics;
using System.Text;
-using DnsmasqWebUI.Services.Abstractions;
+using DnsmasqWebUI.Models.Contracts;
+using DnsmasqWebUI.Infrastructure.Services.Abstractions;
using Microsoft.Extensions.Logging;
-namespace DnsmasqWebUI.Services;
+namespace DnsmasqWebUI.Infrastructure.Services;
/// Runs shell commands via /bin/sh with async output capture and timeout. Used by StatusController and ReloadService.
public sealed class ProcessRunner : IProcessRunner
diff --git a/src/DnsmasqWebUI/Services/ReloadService.cs b/src/DnsmasqWebUI/Infrastructure/Services/ReloadService.cs
similarity index 94%
rename from src/DnsmasqWebUI/Services/ReloadService.cs
rename to src/DnsmasqWebUI/Infrastructure/Services/ReloadService.cs
index 211589d..977088b 100644
--- a/src/DnsmasqWebUI/Services/ReloadService.cs
+++ b/src/DnsmasqWebUI/Infrastructure/Services/ReloadService.cs
@@ -1,8 +1,8 @@
-using DnsmasqWebUI.Configuration;
-using DnsmasqWebUI.Services.Abstractions;
+using DnsmasqWebUI.Models.Config;
+using DnsmasqWebUI.Infrastructure.Services.Abstractions;
using Microsoft.Extensions.Options;
-namespace DnsmasqWebUI.Services;
+namespace DnsmasqWebUI.Infrastructure.Services;
public class ReloadService : IReloadService
{
diff --git a/src/DnsmasqWebUI/Client/Models/ClientSettings.cs b/src/DnsmasqWebUI/Models/Client/ClientSettings.cs
similarity index 93%
rename from src/DnsmasqWebUI/Client/Models/ClientSettings.cs
rename to src/DnsmasqWebUI/Models/Client/ClientSettings.cs
index 6b4b59d..e355ede 100644
--- a/src/DnsmasqWebUI/Client/Models/ClientSettings.cs
+++ b/src/DnsmasqWebUI/Models/Client/ClientSettings.cs
@@ -1,4 +1,4 @@
-namespace DnsmasqWebUI.Client.Models;
+namespace DnsmasqWebUI.Models.Client;
///
/// Client-side preferences persisted in browser local storage.
diff --git a/src/DnsmasqWebUI/Client/Models/SettingsModalContext.cs b/src/DnsmasqWebUI/Models/Client/SettingsModalContext.cs
similarity index 92%
rename from src/DnsmasqWebUI/Client/Models/SettingsModalContext.cs
rename to src/DnsmasqWebUI/Models/Client/SettingsModalContext.cs
index 9dcd389..a500a9c 100644
--- a/src/DnsmasqWebUI/Client/Models/SettingsModalContext.cs
+++ b/src/DnsmasqWebUI/Models/Client/SettingsModalContext.cs
@@ -1,4 +1,4 @@
-namespace DnsmasqWebUI.Client.Models;
+namespace DnsmasqWebUI.Models.Client;
///
/// Which subset of client settings to show in the settings modal.
diff --git a/src/DnsmasqWebUI/Configuration/ApplicationOptions.cs b/src/DnsmasqWebUI/Models/Config/ApplicationOptions.cs
similarity index 95%
rename from src/DnsmasqWebUI/Configuration/ApplicationOptions.cs
rename to src/DnsmasqWebUI/Models/Config/ApplicationOptions.cs
index 4714f2a..af898e0 100644
--- a/src/DnsmasqWebUI/Configuration/ApplicationOptions.cs
+++ b/src/DnsmasqWebUI/Models/Config/ApplicationOptions.cs
@@ -1,4 +1,4 @@
-namespace DnsmasqWebUI.Configuration;
+namespace DnsmasqWebUI.Models.Config;
///
/// Application-level settings (title, etc.). Bound from "Application" config section
diff --git a/src/DnsmasqWebUI/Configuration/DnsmasqOptions.cs b/src/DnsmasqWebUI/Models/Config/DnsmasqOptions.cs
similarity index 98%
rename from src/DnsmasqWebUI/Configuration/DnsmasqOptions.cs
rename to src/DnsmasqWebUI/Models/Config/DnsmasqOptions.cs
index 05d5c58..05c6314 100644
--- a/src/DnsmasqWebUI/Configuration/DnsmasqOptions.cs
+++ b/src/DnsmasqWebUI/Models/Config/DnsmasqOptions.cs
@@ -1,4 +1,4 @@
-namespace DnsmasqWebUI.Configuration;
+namespace DnsmasqWebUI.Models.Config;
///
/// Configuration for dnsmasq paths and reload/status commands.
diff --git a/src/DnsmasqWebUI/Configuration/DnsmasqOptionsValidator.cs b/src/DnsmasqWebUI/Models/Config/DnsmasqOptionsValidator.cs
similarity index 98%
rename from src/DnsmasqWebUI/Configuration/DnsmasqOptionsValidator.cs
rename to src/DnsmasqWebUI/Models/Config/DnsmasqOptionsValidator.cs
index 3c2a31e..28805b7 100644
--- a/src/DnsmasqWebUI/Configuration/DnsmasqOptionsValidator.cs
+++ b/src/DnsmasqWebUI/Models/Config/DnsmasqOptionsValidator.cs
@@ -1,6 +1,6 @@
using Microsoft.Extensions.Options;
-namespace DnsmasqWebUI.Configuration;
+namespace DnsmasqWebUI.Models.Config;
///
/// Validates required dnsmasq options at startup. If config is missing or default paths don't point at existing files,
diff --git a/src/DnsmasqWebUI/Services/Abstractions/ConfigSetSnapshot.cs b/src/DnsmasqWebUI/Models/Contracts/ConfigSetSnapshot.cs
similarity index 77%
rename from src/DnsmasqWebUI/Services/Abstractions/ConfigSetSnapshot.cs
rename to src/DnsmasqWebUI/Models/Contracts/ConfigSetSnapshot.cs
index a5d946d..18fbc4a 100644
--- a/src/DnsmasqWebUI/Services/Abstractions/ConfigSetSnapshot.cs
+++ b/src/DnsmasqWebUI/Models/Contracts/ConfigSetSnapshot.cs
@@ -1,7 +1,8 @@
using DnsmasqWebUI.Models.Dhcp;
-using DnsmasqWebUI.Models.EffectiveConfig;
+using DnsmasqWebUI.Models.Dnsmasq;
+using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
-namespace DnsmasqWebUI.Services.Abstractions;
+namespace DnsmasqWebUI.Models.Contracts;
/// Immutable snapshot from the config set cache: config set, effective config, sources, managed file content, and DHCP host entries (one read per refresh).
public record ConfigSetSnapshot(
diff --git a/src/DnsmasqWebUI/Services/Abstractions/HostsSnapshot.cs b/src/DnsmasqWebUI/Models/Contracts/HostsSnapshot.cs
similarity index 86%
rename from src/DnsmasqWebUI/Services/Abstractions/HostsSnapshot.cs
rename to src/DnsmasqWebUI/Models/Contracts/HostsSnapshot.cs
index 08b8441..7a6a4c6 100644
--- a/src/DnsmasqWebUI/Services/Abstractions/HostsSnapshot.cs
+++ b/src/DnsmasqWebUI/Models/Contracts/HostsSnapshot.cs
@@ -1,6 +1,6 @@
using DnsmasqWebUI.Models.Hosts;
-namespace DnsmasqWebUI.Services.Abstractions;
+namespace DnsmasqWebUI.Models.Contracts;
/// Immutable snapshot from the hosts cache: managed hosts file entries and read-only hosts files (system + addn-hosts).
public record HostsSnapshot(
diff --git a/src/DnsmasqWebUI/Models/Contracts/ManagedConfigContent.cs b/src/DnsmasqWebUI/Models/Contracts/ManagedConfigContent.cs
new file mode 100644
index 0000000..8961628
--- /dev/null
+++ b/src/DnsmasqWebUI/Models/Contracts/ManagedConfigContent.cs
@@ -0,0 +1,6 @@
+using DnsmasqWebUI.Models.Config;
+
+namespace DnsmasqWebUI.Models.Contracts;
+
+/// Full managed file content. EffectiveHostsPathInFile is parsed from AddnHosts line if present (for display only).
+public record ManagedConfigContent(IReadOnlyList Lines, string EffectiveHostsPathInFile);
diff --git a/src/DnsmasqWebUI/Services/Abstractions/ProcessRunResult.cs b/src/DnsmasqWebUI/Models/Contracts/ProcessRunResult.cs
similarity index 79%
rename from src/DnsmasqWebUI/Services/Abstractions/ProcessRunResult.cs
rename to src/DnsmasqWebUI/Models/Contracts/ProcessRunResult.cs
index 9aef926..5b7754a 100644
--- a/src/DnsmasqWebUI/Services/Abstractions/ProcessRunResult.cs
+++ b/src/DnsmasqWebUI/Models/Contracts/ProcessRunResult.cs
@@ -1,6 +1,6 @@
-namespace DnsmasqWebUI.Services.Abstractions;
+namespace DnsmasqWebUI.Models.Contracts;
-/// Result of running a shell command via .
+/// Result of running a shell command via IProcessRunner.
/// Process exit code, or null if timed out or failed to start.
/// Standard output.
/// Standard error.
diff --git a/src/DnsmasqWebUI/Models/Status/DnsmasqServiceStatus.cs b/src/DnsmasqWebUI/Models/Dnsmasq/DnsmasqServiceStatus.cs
similarity index 93%
rename from src/DnsmasqWebUI/Models/Status/DnsmasqServiceStatus.cs
rename to src/DnsmasqWebUI/Models/Dnsmasq/DnsmasqServiceStatus.cs
index 6bbd4b2..487a05e 100644
--- a/src/DnsmasqWebUI/Models/Status/DnsmasqServiceStatus.cs
+++ b/src/DnsmasqWebUI/Models/Dnsmasq/DnsmasqServiceStatus.cs
@@ -1,6 +1,6 @@
-using DnsmasqWebUI.Models.EffectiveConfig;
+using DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
-namespace DnsmasqWebUI.Models.Status;
+namespace DnsmasqWebUI.Models.Dnsmasq;
///
/// Dnsmasq service and config status returned by GET api/status.
@@ -21,6 +21,7 @@ namespace DnsmasqWebUI.Models.Status;
/// True if exists on disk.
/// True when dhcp-leasefile is set in effective config.
/// True if is set and the file exists on disk.
+/// Ordered list of config files (main + conf-file + conf-dir) for "Config files (load order)" in the UI. Null or empty when config set is unavailable.
/// True when ReloadCommand is set in app config (reload after save).
/// True when StatusCommand is set (used to determine dnsmasq active/inactive).
/// True when StatusShowCommand is set (full service status output on Dnsmasq page).
@@ -52,6 +53,7 @@ public record DnsmasqServiceStatus(
bool ManagedFilePathExists,
bool LeasesPathConfigured,
bool LeasesPathExists,
+ IReadOnlyList? ConfigFiles,
bool ReloadCommandConfigured,
bool StatusCommandConfigured,
bool StatusShowConfigured,
diff --git a/src/DnsmasqWebUI/Models/EffectiveConfig/ConfigValueSource.cs b/src/DnsmasqWebUI/Models/Dnsmasq/EffectiveConfig/ConfigValueSource.cs
similarity index 97%
rename from src/DnsmasqWebUI/Models/EffectiveConfig/ConfigValueSource.cs
rename to src/DnsmasqWebUI/Models/Dnsmasq/EffectiveConfig/ConfigValueSource.cs
index 94357cc..34a31b6 100644
--- a/src/DnsmasqWebUI/Models/EffectiveConfig/ConfigValueSource.cs
+++ b/src/DnsmasqWebUI/Models/Dnsmasq/EffectiveConfig/ConfigValueSource.cs
@@ -1,4 +1,4 @@
-namespace DnsmasqWebUI.Models.EffectiveConfig;
+namespace DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
///
/// Where an effective config value came from (which file and line). Used so the UI can show exactly which file
diff --git a/src/DnsmasqWebUI/Models/EffectiveConfig/DnsmasqConfFileSource.cs b/src/DnsmasqWebUI/Models/Dnsmasq/EffectiveConfig/DnsmasqConfFileSource.cs
similarity index 76%
rename from src/DnsmasqWebUI/Models/EffectiveConfig/DnsmasqConfFileSource.cs
rename to src/DnsmasqWebUI/Models/Dnsmasq/EffectiveConfig/DnsmasqConfFileSource.cs
index 13921bd..923fbb0 100644
--- a/src/DnsmasqWebUI/Models/EffectiveConfig/DnsmasqConfFileSource.cs
+++ b/src/DnsmasqWebUI/Models/Dnsmasq/EffectiveConfig/DnsmasqConfFileSource.cs
@@ -1,4 +1,4 @@
-namespace DnsmasqWebUI.Models.EffectiveConfig;
+namespace DnsmasqWebUI.Models.Dnsmasq.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/EffectiveConfig/DnsmasqConfigSet.cs b/src/DnsmasqWebUI/Models/Dnsmasq/EffectiveConfig/DnsmasqConfigSet.cs
similarity index 87%
rename from src/DnsmasqWebUI/Models/EffectiveConfig/DnsmasqConfigSet.cs
rename to src/DnsmasqWebUI/Models/Dnsmasq/EffectiveConfig/DnsmasqConfigSet.cs
index 80babae..fbb6834 100644
--- a/src/DnsmasqWebUI/Models/EffectiveConfig/DnsmasqConfigSet.cs
+++ b/src/DnsmasqWebUI/Models/Dnsmasq/EffectiveConfig/DnsmasqConfigSet.cs
@@ -1,4 +1,4 @@
-namespace DnsmasqWebUI.Models.EffectiveConfig;
+namespace DnsmasqWebUI.Models.Dnsmasq.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/EffectiveConfig/DnsmasqConfigSetEntry.cs b/src/DnsmasqWebUI/Models/Dnsmasq/EffectiveConfig/DnsmasqConfigSetEntry.cs
similarity index 81%
rename from src/DnsmasqWebUI/Models/EffectiveConfig/DnsmasqConfigSetEntry.cs
rename to src/DnsmasqWebUI/Models/Dnsmasq/EffectiveConfig/DnsmasqConfigSetEntry.cs
index d198ccd..3fb7113 100644
--- a/src/DnsmasqWebUI/Models/EffectiveConfig/DnsmasqConfigSetEntry.cs
+++ b/src/DnsmasqWebUI/Models/Dnsmasq/EffectiveConfig/DnsmasqConfigSetEntry.cs
@@ -1,4 +1,4 @@
-namespace DnsmasqWebUI.Models.EffectiveConfig;
+namespace DnsmasqWebUI.Models.Dnsmasq.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/EffectiveConfig/EffectiveConfigDisplayMode.cs b/src/DnsmasqWebUI/Models/Dnsmasq/EffectiveConfig/EffectiveConfigDisplayMode.cs
similarity index 88%
rename from src/DnsmasqWebUI/Models/EffectiveConfig/EffectiveConfigDisplayMode.cs
rename to src/DnsmasqWebUI/Models/Dnsmasq/EffectiveConfig/EffectiveConfigDisplayMode.cs
index 71f7a0d..a5a38f4 100644
--- a/src/DnsmasqWebUI/Models/EffectiveConfig/EffectiveConfigDisplayMode.cs
+++ b/src/DnsmasqWebUI/Models/Dnsmasq/EffectiveConfig/EffectiveConfigDisplayMode.cs
@@ -1,4 +1,4 @@
-namespace DnsmasqWebUI.Models.EffectiveConfig;
+namespace DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
///
/// Display mode for effective-config fields. Cascaded from the parent so that
diff --git a/src/DnsmasqWebUI/Models/EffectiveConfig/EffectiveConfigFieldDescriptor.cs b/src/DnsmasqWebUI/Models/Dnsmasq/EffectiveConfig/EffectiveConfigFieldDescriptor.cs
similarity index 89%
rename from src/DnsmasqWebUI/Models/EffectiveConfig/EffectiveConfigFieldDescriptor.cs
rename to src/DnsmasqWebUI/Models/Dnsmasq/EffectiveConfig/EffectiveConfigFieldDescriptor.cs
index 3824096..00d6c6a 100644
--- a/src/DnsmasqWebUI/Models/EffectiveConfig/EffectiveConfigFieldDescriptor.cs
+++ b/src/DnsmasqWebUI/Models/Dnsmasq/EffectiveConfig/EffectiveConfigFieldDescriptor.cs
@@ -1,6 +1,6 @@
-using DnsmasqWebUI.Models.Status;
+using DnsmasqWebUI.Models.Dnsmasq;
-namespace DnsmasqWebUI.Models.EffectiveConfig;
+namespace DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
///
/// Glue for rendering one effective-config field: metadata (section, option name) plus delegates that resolve value, source, and items from Status at render time.
diff --git a/src/DnsmasqWebUI/Models/EffectiveConfig/EffectiveConfigSources.cs b/src/DnsmasqWebUI/Models/Dnsmasq/EffectiveConfig/EffectiveConfigSources.cs
similarity index 97%
rename from src/DnsmasqWebUI/Models/EffectiveConfig/EffectiveConfigSources.cs
rename to src/DnsmasqWebUI/Models/Dnsmasq/EffectiveConfig/EffectiveConfigSources.cs
index 8ee3392..b715fe6 100644
--- a/src/DnsmasqWebUI/Models/EffectiveConfig/EffectiveConfigSources.cs
+++ b/src/DnsmasqWebUI/Models/Dnsmasq/EffectiveConfig/EffectiveConfigSources.cs
@@ -1,4 +1,4 @@
-namespace DnsmasqWebUI.Models.EffectiveConfig;
+namespace DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
///
/// Source file for each field in .
diff --git a/src/DnsmasqWebUI/Models/EffectiveConfig/EffectiveDnsmasqConfig.cs b/src/DnsmasqWebUI/Models/Dnsmasq/EffectiveConfig/EffectiveDnsmasqConfig.cs
similarity index 98%
rename from src/DnsmasqWebUI/Models/EffectiveConfig/EffectiveDnsmasqConfig.cs
rename to src/DnsmasqWebUI/Models/Dnsmasq/EffectiveConfig/EffectiveDnsmasqConfig.cs
index c369f05..5846d0d 100644
--- a/src/DnsmasqWebUI/Models/EffectiveConfig/EffectiveDnsmasqConfig.cs
+++ b/src/DnsmasqWebUI/Models/Dnsmasq/EffectiveConfig/EffectiveDnsmasqConfig.cs
@@ -1,4 +1,4 @@
-namespace DnsmasqWebUI.Models.EffectiveConfig;
+namespace DnsmasqWebUI.Models.Dnsmasq.EffectiveConfig;
///
/// Effective dnsmasq config after reading all config files (main + conf-file + conf-dir)
diff --git a/src/DnsmasqWebUI/Models/Status/SaveWithReloadResult.cs b/src/DnsmasqWebUI/Models/Dnsmasq/SaveWithReloadResult.cs
similarity index 66%
rename from src/DnsmasqWebUI/Models/Status/SaveWithReloadResult.cs
rename to src/DnsmasqWebUI/Models/Dnsmasq/SaveWithReloadResult.cs
index 39b14e3..4caadda 100644
--- a/src/DnsmasqWebUI/Models/Status/SaveWithReloadResult.cs
+++ b/src/DnsmasqWebUI/Models/Dnsmasq/SaveWithReloadResult.cs
@@ -1,6 +1,6 @@
-using DnsmasqWebUI.Services.Abstractions;
+using DnsmasqWebUI.Infrastructure.Services.Abstractions;
-namespace DnsmasqWebUI.Models.Status;
+namespace DnsmasqWebUI.Models.Dnsmasq;
/// 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/Program.cs b/src/DnsmasqWebUI/Program.cs
index 82b5e03..045eaf1 100644
--- a/src/DnsmasqWebUI/Program.cs
+++ b/src/DnsmasqWebUI/Program.cs
@@ -1,6 +1,7 @@
using DnsmasqWebUI.Components;
+using DnsmasqWebUI.Models.Config;
using DnsmasqWebUI.Extensions;
-using DnsmasqWebUI.Configuration;
+using DnsmasqWebUI.Infrastructure.Helpers.Http;
using Microsoft.Extensions.Options;
// When not in Development, use the app's directory (not CWD) so static assets work when run via symlink or from any CWD.
@@ -25,11 +26,7 @@ builder.Services.AddHttpContextAccessor();
builder.Services.AddDnsmasqApiHttpClients();
builder.Services.AddControllers()
- .AddJsonOptions(o =>
- {
- o.JsonSerializerOptions.PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase;
- o.JsonSerializerOptions.Converters.Add(new System.Text.Json.Serialization.JsonStringEnumConverter());
- });
+ .AddJsonOptions(o => ApiJsonOptions.ConfigureServer(o.JsonSerializerOptions));
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
diff --git a/testdata/README b/testdata/README
index 18f3567..89fbcc8 100644
--- a/testdata/README
+++ b/testdata/README
@@ -9,7 +9,7 @@ Source for the Docker test harness: `scripts/prepare-test-mount.sh` syncs this d
| **dnsmasq-test.conf** | Harness main config (`/data/dnsmasq-test.conf`). DNS + DHCP; `conf-dir=/data/dnsmasq.d`, multiple server/addn-hosts/address/dhcp-range. |
| **dnsmasq.conf** | Minimal main (conf-dir only). Used by unit tests when conf-dir is absent. |
| **dnsmasq.conf.example** | Commented example; not used by harness or tests. |
-| **dnsmasq.d/** | Included configs (alphabetical order). |
+| **dnsmasq.d/** | Included configs (conf-dir; alphabetical order). In the harness, "Config files (load order)" should list main, then these, then zz-dnsmasq-webui.conf. |
| **dnsmasq.d/01-other.conf** | `domain=local`. Not managed. |
| **dnsmasq.d/02-servers.conf** | Extra `server=` / `local=` (readonly in UI). |
| **dnsmasq.d/03-more.conf** | `resolv-file=`, `dhcp-option=`. Not managed. |
@@ -20,3 +20,5 @@ Source for the Docker test harness: `scripts/prepare-test-mount.sh` syncs this d
| **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.
+
+**Manual check for conf-dir fix:** After starting the harness, open the app and confirm "Config files (load order)" shows: main (`/data/dnsmasq-test.conf`), then conf-dir files (`01-other.conf`, `02-servers.conf`, `03-more.conf`, `dhcp.conf`), then managed (`zz-dnsmasq-webui.conf`). Unit test `GetIncludedPathsWithSource_ConfDirAndConfFile_ReturnsAllFilesInLoadOrderWithSources` asserts the same layout (main + conf-dir + conf-file) with sources.
diff --git a/testdata/zz-dnsmasq-webui.conf b/testdata/zz-dnsmasq-webui.conf
new file mode 100644
index 0000000..acc2481
--- /dev/null
+++ b/testdata/zz-dnsmasq-webui.conf
@@ -0,0 +1 @@
+addn-hosts=/home/alex/Documents/Projects/servers/dnsmasq-webui/testdata/zz-dnsmasq-webui.hosts
diff --git a/testdata/zz-dnsmasq-webui.hosts b/testdata/zz-dnsmasq-webui.hosts
new file mode 100644
index 0000000..e69de29