DHCP: read-only hosts from other configs, combobox inputs, polling, UI fixes

- Static hosts: read dhcp-host from all config files; show external as read-only
  with Source column; IsEditable + SourcePath on DhcpHostEntry; reject save if
  managed entry duplicates MAC from another file (400 + message)
- Lease field: single input + datalist (type or select), remove Other dropdown
- MAC field: single input + datalist from current leases, TryFillFromLeaseByMac
- Add host: default Name=*, suggest next free IP from DHCP range
- Leases: configurable refresh polling; Settings modal for interval (client-side)
- DhcpHostsClient: surface 400 error body message on save failure
- App title config (ApplicationOptions), nav/layout/error/hosts tweaks
This commit is contained in:
2026-02-02 00:40:53 +10:00
parent 92dfa2b170
commit a60d0e8db8
26 changed files with 803 additions and 106 deletions
@@ -32,6 +32,7 @@ public class StatusController : ControllerBase
var set = await _configSetService.GetConfigSetAsync(ct);
var effectiveLeasesPath = _configSetService.GetLeasesPath();
var effectiveConfig = _configSetService.GetEffectiveConfig();
var (dhcpRangeStart, dhcpRangeEnd) = _configSetService.GetDhcpRange();
var systemHostsPath = _options.SystemHostsPath?.Trim();
var statusResult = await _processRunner.RunAsync(_options.StatusCommand, TimeSpan.FromSeconds(5), ct);
@@ -52,7 +53,7 @@ public class StatusController : ControllerBase
var showResult = await showTask;
var logsResult = await logsTask;
var statusShowOutput = !string.IsNullOrWhiteSpace(_options.StatusShowCommand)
? showResult.Stdout + (showResult.TimedOut ? "\n(Command timed out.)" : "")
? FormatStatusShowOutput(showResult.Stdout + (showResult.TimedOut ? "\n(Command timed out.)" : ""))
: null;
var logsOutput = !string.IsNullOrWhiteSpace(_options.LogsCommand)
? logsResult.Stdout + (logsResult.TimedOut ? "\n(Command timed out.)" : "")
@@ -83,7 +84,9 @@ public class StatusController : ControllerBase
StatusCommandStdout: dnsmasqStatus != "active" ? statusCommandStdout : null,
StatusCommandStderr: dnsmasqStatus != "active" ? statusCommandStderr : null,
StatusShowOutput: statusShowOutput,
LogsOutput: logsOutput
LogsOutput: logsOutput,
DhcpRangeStart: dhcpRangeStart,
DhcpRangeEnd: dhcpRangeEnd
);
return Ok(status);
}
@@ -93,6 +96,33 @@ public class StatusController : ControllerBase
}
}
/// <summary>
/// Reformats StatusShow output: Active line gets a line break after the semicolon;
/// continuation (e.g. "4 min ago") is indented to align with the value after "Active: ".
/// </summary>
private static string FormatStatusShowOutput(string raw)
{
if (string.IsNullOrWhiteSpace(raw)) return raw;
var lines = raw.Split('\n');
for (var i = 0; i < lines.Length; i++)
{
var line = lines[i];
var activeIdx = line.IndexOf("Active:", StringComparison.Ordinal);
if (activeIdx < 0) continue;
var semiIdx = line.IndexOf(';', activeIdx);
if (semiIdx < 0) continue;
var afterSemi = semiIdx + 1;
while (afterSemi < line.Length && line[afterSemi] == ' ') afterSemi++;
var continuation = afterSemi < line.Length ? line[afterSemi..].TrimEnd() : "";
var indentLength = activeIdx + "Active: ".Length;
var indent = new string(' ', indentLength);
lines[i] = continuation.Length > 0
? line[..(semiIdx + 1)] + "\n" + indent + continuation
: line[..(semiIdx + 1)];
}
return string.Join("\n", lines);
}
/// <summary>Returns the full log file from the path in effective config (log-facility). Untruncated.</summary>
[HttpGet("logs/download")]
public async Task<IActionResult> GetLogsDownload(CancellationToken ct)