Install script, static assets, UI and release workflow updates

- install.sh: default dirs, --update/--service/--uninstall/--purge, in-dir dnsmasq-webui symlink, ContentRootPath note
- UseStaticFiles instead of MapStaticAssets (fix 0-byte assets)
- Program: ContentRootPath when not Development for symlink/CWD
- App.razor: direct asset paths
- Dnsmasq page: remove redundant status pre, margin above Reload button
- Sidebar: logo 64px via app.css, hamburger vertically centered, toggler inside top-row
- ApplicationTitle documented in README
- publish/release: RIDs, Docker build for glibc
This commit is contained in:
2026-02-05 02:00:36 +10:00
parent 4ff9c552eb
commit 2f049feed2
13 changed files with 228 additions and 63 deletions
+14 -4
View File
@@ -1,5 +1,7 @@
# Build self-contained binaries for multiple RIDs when a version tag is pushed,
# Build self-contained binaries for portable Linux RIDs when a version tag is pushed,
# then create/update the release and attach the zips as release assets.
# linux-x64 and linux-arm64 are built inside a container (Debian-based SDK image) so
# the resulting binaries work across glibc-based distros (Ubuntu, Debian, etc.).
# Run: push a tag (e.g. v1.0.0); the workflow builds that commit and uploads assets.
name: Release
@@ -16,20 +18,28 @@ jobs:
rid:
- linux-x64
- linux-arm64
- ubuntu.24.04-x64
- ubuntu.22.04-x64
- linux-musl-x64
- linux-musl-arm64
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Publish for ${{ matrix.rid }} (glibc in Docker)
if: matrix.rid == 'linux-x64' || matrix.rid == 'linux-arm64'
run: |
docker run --rm \
-v "${{ github.workspace }}:/src" -w /src \
mcr.microsoft.com/dotnet/sdk:9.0 \
./scripts/publish-self-contained.sh ${{ matrix.rid }}
- name: Setup .NET
if: matrix.rid == 'linux-musl-x64' || matrix.rid == 'linux-musl-arm64'
uses: actions/setup-dotnet@v4
with:
dotnet-version: '9.0.x'
- name: Publish for ${{ matrix.rid }}
- name: Publish for ${{ matrix.rid }} (musl on host)
if: matrix.rid == 'linux-musl-x64' || matrix.rid == 'linux-musl-arm64'
run: ./scripts/publish-self-contained.sh ${{ matrix.rid }}
- name: Zip publish output
+14 -2
View File
@@ -50,7 +50,7 @@ By default the script installs to **`~/.local/share/dnsmasq-webui`** and creates
```bash
dnsmasq-webui
# or
~/.local/share/dnsmasq-webui/DnsmasqWebUI
~/.local/share/dnsmasq-webui/dnsmasq-webui
```
**System-wide install (requires sudo):**
@@ -98,6 +98,8 @@ sudo ./scripts/install.sh --uninstall --purge --system # Also remove /opt/dnsm
**Installing from a fork:** Pass the repo explicitly: `./scripts/install.sh --repo owner/dnsmasq-webui` or set `GITHUB_REPO=owner/dnsmasq-webui`. The default repo is `alexhopeoconnor/dnsmasq-webui`.
**If the binary fails to run** (e.g. `TypeLoadException` or glibc errors): use the install script with `--build-from-source` so the app is built for your machine. This requires the .NET SDK and a **git clone** (it does not work when installing via `curl ... | sh`). Clone the repo, then run `./scripts/install.sh --build-from-source`. The script checks that .NET is installed, detects your OS/arch, and builds (on Ubuntu it may use a distro-specific RID for a better match). Other distros use the portable RID and may still hit runtime issues; building from source on the target machine is the most reliable.
**Configuration:** Set at least `Dnsmasq__MainConfigPath` to your main dnsmasq config. See [Configuration](#configuration) for all options.
<!-- SUGGESTED SCREENSHOT: TERMINAL SHOWING INSTALL.SH OUTPUT AND "Run: dnsmasq-webui" -->
@@ -200,9 +202,19 @@ The app is configured via **appsettings.json**, **environment variables**, and *
| `StatusShowCommand` | Optional: full status output (e.g. systemctl status) | `systemctl status dnsmasq --no-pager` |
| `LogsCommand` | Optional: recent logs (e.g. journalctl) | `journalctl -u dnsmasq -n 100 --no-pager` |
**Application options** (use `Application__` prefix for env, `Application` section in JSON):
| Option | Description | Example |
|--------|-------------|---------|
| `ApplicationTitle` | Title shown in the sidebar brand and browser tab (default: "Local DNS") | `"ApplicationTitle": "My DNS"` or `Application__ApplicationTitle=My DNS` |
**Host / URLs:** `ASPNETCORE_URLS=http://0.0.0.0:8080` or `--urls=http://0.0.0.0:8080` to bind to a specific address and port.
Place `appsettings.json` in the same directory as the executable (or use the default project paths when running with `dotnet run`).
Place `appsettings.json` in the same directory as the executable (or use the default project paths when running with `dotnet run`). You can override any option via environment variables using the `Dnsmasq__` prefix (e.g. `Dnsmasq__MainConfigPath=/path/to/dnsmasq.conf`). To use the repo test config:
```bash
Dnsmasq__MainConfigPath=/path/to/dnsmasq-webui/testdata/dnsmasq-test.conf dnsmasq-webui
```
---
+144 -23
View File
@@ -16,6 +16,7 @@ SERVICE=false
UNINSTALL=false
PURGE=false
UPDATE=false
BUILD_FROM_SOURCE=false
# Default install dir (user-writable, no sudo). Overridden by --dir or --system.
default_install_dir() {
@@ -36,6 +37,7 @@ usage() {
echo " --list List available releases (tag, name, published_at) and exit."
echo " --version TAG Install from release TAG (e.g. v1.0.0). Default: latest."
echo " --update Reinstall latest into the default user directory (~/.local/share/dnsmasq-webui)."
echo " --build-from-source Build from source instead of downloading (requires .NET SDK and a git clone; not supported when run via curl | sh). Auto-detects RID for your machine. Use if the prebuilt binary fails to start (e.g. TypeLoadException)."
echo " -h, -?, --help Show this help."
echo ""
echo "Install location:"
@@ -67,8 +69,9 @@ usage() {
echo " $0 --uninstall --purge # Remove services, symlinks, and default install dir"
echo " sudo $0 --uninstall --purge --system # Also remove /opt/dnsmasq-webui"
echo " $0 --list # List releases"
echo " $0 --build-from-source # Build locally (if prebuilt binary fails on your system)"
echo ""
echo "After install, configure via appsettings.json or Dnsmasq__* environment variables, then run the binary (or dnsmasq-webui if symlink created). If you used --service, enable/start with systemctl."
echo "After install, configure via appsettings.json or Dnsmasq__* environment variables, then run the binary (or dnsmasq-webui if symlink created). If you used --service, enable/start with systemctl. If the app fails to start with TypeLoadException or a glibc error, try --build-from-source (requires .NET SDK)."
exit 0
}
@@ -113,17 +116,15 @@ detect_arch() {
esac
}
# Map to portable RIDs only (linux-x64, linux-arm64, linux-musl-*). Ubuntu and other
# glibc distros use linux-x64/linux-arm64; Alpine uses linux-musl-*. Used for downloading
# release assets (CI builds portable RIDs only).
detect_rid() {
local arch
arch="$(detect_arch)"
if [ -f /etc/os-release ]; then
. /etc/os-release
case "${ID:-}" in
ubuntu)
case "${VERSION_ID:-}" in
24.04) echo "ubuntu.24.04-$arch"; return ;;
22.04) echo "ubuntu.22.04-$arch"; return ;;
esac ;;
alpine)
case "$arch" in
x64) echo "linux-musl-x64"; return ;;
@@ -139,6 +140,18 @@ detect_rid() {
esac
}
# Print detected environment (OS, arch) for user feedback.
print_env_detection() {
local rid
rid="$1"
if [ -f /etc/os-release ]; then
. /etc/os-release
echo "Detected: ${ID:-unknown} ${VERSION_ID:-}, $(uname -m) -> RID $rid"
else
echo "Detected: $(uname -m) -> RID $rid"
fi
}
# GET release (latest or by tag). Output raw JSON to stdout.
fetch_release() {
local api_url
@@ -270,8 +283,97 @@ do_uninstall() {
exit 0
}
# Build from source and install to INSTALL_DIR. Requires .NET SDK and running from a git clone.
do_install_from_source() {
local script_dir repo_root publish_output rid publish_dir
script_dir="$(cd "$(dirname "$0")" && pwd)"
repo_root="$(cd "$script_dir/.." && pwd)"
if [ ! -f "$repo_root/scripts/publish-self-contained.sh" ]; then
echo "Error: --build-from-source requires a git clone (run from the repo directory). Not found: $repo_root/scripts/publish-self-contained.sh" >&2
echo "Do not use --build-from-source when installing via 'curl ... | sh'; clone the repo first, then run ./scripts/install.sh --build-from-source" >&2
exit 1
fi
if ! command -v dotnet >/dev/null 2>&1; then
echo "Error: --build-from-source requires the .NET SDK. Install from https://dotnet.microsoft.com/download or your distro (e.g. apt install dotnet-sdk-9.0)." >&2
exit 1
fi
if [ "$SYSTEM_INSTALL" = true ]; then
if [ "$(id -u)" -ne 0 ]; then
echo "Error: --system installs to /opt and requires root. Run with sudo: sudo $0 --system --build-from-source" >&2
exit 1
fi
INSTALL_DIR="/opt/dnsmasq-webui"
elif [ -z "$INSTALL_DIR" ]; then
INSTALL_DIR="$(default_install_dir)"
fi
echo "Building from source (auto-detecting RID for your machine)..."
publish_output="$(cd "$repo_root" && ./scripts/publish-self-contained.sh 2>&1)" || exit $?
echo "$publish_output"
rid="$(echo "$publish_output" | sed -n 's/^Detected RID: \([^ ]*\).*/\1/p')"
if [ -z "$rid" ]; then
rid="$(echo "$publish_output" | sed -n 's/^Publishing self-contained.* for \([^.]*\)\.\.\./\1/p')"
fi
if [ -z "$rid" ]; then
echo "Error: Could not determine RID from build output." >&2
exit 1
fi
publish_dir="$repo_root/src/DnsmasqWebUI/bin/Release/net9.0/$rid/publish"
if [ ! -d "$publish_dir" ] || [ ! -f "$publish_dir/DnsmasqWebUI" ]; then
echo "Error: Build output not found at $publish_dir" >&2
exit 1
fi
echo "Installing to $INSTALL_DIR ..."
mkdir -p "$INSTALL_DIR"
cp -a "$publish_dir"/* "$INSTALL_DIR/"
ln -sf DnsmasqWebUI "$INSTALL_DIR/dnsmasq-webui" 2>/dev/null || true
if [ "$SYSTEM_INSTALL" = true ]; then
ln -sf "$INSTALL_DIR/dnsmasq-webui" /usr/local/bin/dnsmasq-webui 2>/dev/null || true
echo ""
echo "Installed to $INSTALL_DIR (system-wide)"
echo "Run: dnsmasq-webui (or $INSTALL_DIR/dnsmasq-webui)"
else
LOCAL_BIN="${HOME:-}/.local/bin"
if [ -n "${HOME:-}" ] && [ -d "$(dirname "$LOCAL_BIN")" ]; then
mkdir -p "$LOCAL_BIN"
if [ -w "$LOCAL_BIN" ]; then
ln -sf "$INSTALL_DIR/dnsmasq-webui" "$LOCAL_BIN/dnsmasq-webui" 2>/dev/null && echo "Symlink: $LOCAL_BIN/dnsmasq-webui -> $INSTALL_DIR/dnsmasq-webui" || true
fi
fi
echo ""
echo "Installed to $INSTALL_DIR"
echo "Run: $INSTALL_DIR/dnsmasq-webui"
if [ -f "$LOCAL_BIN/dnsmasq-webui" ] 2>/dev/null; then
echo " or: dnsmasq-webui (if ~/.local/bin is in your PATH)"
fi
fi
if [ "$SERVICE" = true ]; then
if [ "$SYSTEM_INSTALL" = true ] && [ "$(id -u)" -ne 0 ]; then
echo "Error: Installing a system service (--service with --system) requires root." >&2
exit 1
fi
if ! command -v systemctl >/dev/null 2>&1; then
echo "Error: systemd not found. --service is only supported on systemd-based systems." >&2
exit 1
fi
install_systemd_unit "$INSTALL_DIR"
fi
echo "Configure via appsettings.json in that directory or Dnsmasq__* environment variables (e.g. Dnsmasq__MainConfigPath=/etc/dnsmasq.conf)."
}
# Main install: fetch release, find asset for RID, download, extract, optionally symlink.
do_install() {
if [ "$BUILD_FROM_SOURCE" = true ]; then
do_install_from_source
return
fi
check_jq
detect_repo
@@ -297,7 +399,7 @@ do_install() {
fi
rid="$(detect_rid)"
echo "Detected RID: $rid"
print_env_detection "$rid"
echo "Fetching release..."
release_json="$(fetch_release)"
tag="$(echo "$release_json" | jq -r '.tag_name')"
@@ -317,24 +419,25 @@ do_install() {
echo "Extracting to $INSTALL_DIR ..."
unzip -o -q "$tmpzip" -d "$INSTALL_DIR"
rm -f "$tmpzip"
ln -sf DnsmasqWebUI "$INSTALL_DIR/dnsmasq-webui" 2>/dev/null || true
if [ "$SYSTEM_INSTALL" = true ]; then
ln -sf "$INSTALL_DIR/DnsmasqWebUI" /usr/local/bin/dnsmasq-webui 2>/dev/null || true
ln -sf "$INSTALL_DIR/dnsmasq-webui" /usr/local/bin/dnsmasq-webui 2>/dev/null || true
echo ""
echo "Installed to $INSTALL_DIR (system-wide)"
echo "Run: dnsmasq-webui (or $INSTALL_DIR/DnsmasqWebUI)"
echo "Run: dnsmasq-webui (or $INSTALL_DIR/dnsmasq-webui)"
else
# User install: create ~/.local/bin symlink so `dnsmasq-webui` works if ~/.local/bin is in PATH
LOCAL_BIN="${HOME:-}/.local/bin"
if [ -n "${HOME:-}" ] && [ -d "$(dirname "$LOCAL_BIN")" ]; then
mkdir -p "$LOCAL_BIN"
if [ -w "$LOCAL_BIN" ]; then
ln -sf "$INSTALL_DIR/DnsmasqWebUI" "$LOCAL_BIN/dnsmasq-webui" 2>/dev/null && echo "Symlink: $LOCAL_BIN/dnsmasq-webui -> $INSTALL_DIR/DnsmasqWebUI" || true
ln -sf "$INSTALL_DIR/dnsmasq-webui" "$LOCAL_BIN/dnsmasq-webui" 2>/dev/null && echo "Symlink: $LOCAL_BIN/dnsmasq-webui -> $INSTALL_DIR/dnsmasq-webui" || true
fi
fi
echo ""
echo "Installed to $INSTALL_DIR"
echo "Run: $INSTALL_DIR/DnsmasqWebUI"
echo "Run: $INSTALL_DIR/dnsmasq-webui"
if [ -f "$LOCAL_BIN/dnsmasq-webui" ] 2>/dev/null; then
echo " or: dnsmasq-webui (if ~/.local/bin is in your PATH)"
fi
@@ -345,17 +448,21 @@ do_install() {
fi
echo "Configure via appsettings.json in that directory or Dnsmasq__* environment variables (e.g. Dnsmasq__MainConfigPath=/etc/dnsmasq.conf)."
echo "If the app fails to start with TypeLoadException or a glibc error, try: $0 --build-from-source (requires .NET SDK)."
}
# Install systemd unit. Call only when SERVICE=true and systemctl exists.
# Removes the other service type first (user vs system) so switching works cleanly.
# $1 = INSTALL_DIR (where DnsmasqWebUI binary and appsettings.json live)
# $1 = INSTALL_DIR (contains dnsmasq-webui runnable and appsettings.json)
install_systemd_unit() {
local dir bin
dir="$1"
bin="$dir/DnsmasqWebUI"
if [ ! -f "$bin" ]; then
echo "Warning: Binary $bin not found; skipping systemd unit install." >&2
if [ -f "$dir/dnsmasq-webui" ]; then
bin="$dir/dnsmasq-webui"
elif [ -f "$dir/DnsmasqWebUI" ]; then
bin="$dir/DnsmasqWebUI"
else
echo "Warning: No binary found in $dir; skipping systemd unit install." >&2
return 0
fi
if [ "$SYSTEM_INSTALL" = true ]; then
@@ -470,6 +577,10 @@ while [ $# -gt 0 ]; do
PURGE=true
shift
;;
--build-from-source)
BUILD_FROM_SOURCE=true
shift
;;
-*)
echo "Error: unknown option $1" >&2
usage >&2
@@ -488,16 +599,26 @@ if [ "$PURGE" = true ] && [ "$UNINSTALL" != true ]; then
echo "Error: --purge must be used with --uninstall. E.g. $0 --uninstall --purge" >&2
exit 1
fi
if [ "$UNINSTALL" = true ]; then
if [ "$UPDATE" = true ] || [ -n "$VERSION" ] || [ "$SERVICE" = true ]; then
echo "Error: --uninstall cannot be combined with install/update options (--version, --update, --service). Run uninstall alone, then install if needed." >&2
exit 1
if [ "$UNINSTALL" = true ]; then
if [ "$UPDATE" = true ] || [ -n "$VERSION" ] || [ "$SERVICE" = true ] || [ "$BUILD_FROM_SOURCE" = true ]; then
echo "Error: --uninstall cannot be combined with install/update options (--version, --update, --service, --build-from-source). Run uninstall alone, then install if needed." >&2
exit 1
fi
if [ "$LIST" = true ]; then
echo "Error: --uninstall cannot be combined with --list." >&2
exit 1
fi
fi
if [ "$LIST" = true ]; then
echo "Error: --uninstall cannot be combined with --list." >&2
exit 1
if [ "$BUILD_FROM_SOURCE" = true ]; then
if [ "$LIST" = true ]; then
echo "Error: --build-from-source cannot be combined with --list." >&2
exit 1
fi
if [ -n "$VERSION" ] || [ "$UPDATE" = true ]; then
echo "Error: --build-from-source builds from current source; do not use --version or --update." >&2
exit 1
fi
fi
fi
if [ "$LIST" = true ]; then
if [ "$UNINSTALL" = true ] || [ "$UPDATE" = true ] || [ "$SERVICE" = true ] || [ -n "$VERSION" ]; then
echo "Error: --list lists releases and exits; do not combine with install/uninstall options." >&2
+10 -8
View File
@@ -3,9 +3,7 @@
# for ASP.NET Core). Copy the publish directory to the target host and run the binary.
#
# Platform auto-detection: if you do not pass a RID, the script picks one from /etc/os-release
# and uname -m. On Ubuntu 24.04/22.04 we use ubuntu.<ver>-<arch> so the runtime matches the
# host and avoids TypeLoadException seen with generic linux-x64 on those distros. Else we use
# generic linux-* or linux-musl-* (Alpine). Trimming is off by default because PublishTrimmed
# and uname -m. We use generic linux-* (glibc) or linux-musl-* (Alpine). Trimming is off by default because PublishTrimmed
# can cause Blazor routing/404 issues. The script cleans the target RID before publish
# so previous artefacts (e.g. from a different --trim or RID) cannot pollute the build.
set -e
@@ -14,6 +12,8 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
PROJECT="$REPO_ROOT/src/DnsmasqWebUI/DnsmasqWebUI.csproj"
# Portable RIDs (built in CI). Ubuntu RIDs for local builds only (CI does not build these;
# use when .NET from distro repo so the binary runs on this machine without TypeLoadException).
VALID_RIDS="linux-x64 linux-arm64 linux-arm linux-musl-x64 linux-musl-arm64 ubuntu.24.04-x64 ubuntu.24.04-arm64 ubuntu.22.04-x64 ubuntu.22.04-arm64"
TRIM=false
CLEAN=true
@@ -30,7 +30,9 @@ detect_arch() {
esac
}
# Pick RID from OS and arch so runtime matches host (avoids TypeLoadException on Ubuntu).
# Pick RID from OS and arch. We only add explicit distro RIDs where we've seen
# TypeLoadException with portable linux-x64 (Ubuntu when .NET from distro). Alpine
# uses musl. Other distros (Debian, Fedora, etc.) fall through to linux-x64/linux-arm64.
default_rid() {
local arch
arch="$(detect_arch)"
@@ -64,7 +66,7 @@ while [ $# -gt 0 ]; do
echo ""
echo "Build a self-contained folder publish for Linux. If RID is omitted, the script"
echo "auto-detects from the current OS and architecture (recommended on Ubuntu and Alpine)."
echo "Pass options first, then RID if desired (e.g. $0 --trim ubuntu.24.04-x64)."
echo "Pass options first, then RID if desired (e.g. $0 --trim linux-x64)."
echo ""
echo "Options:"
echo " --trim Enable trimming (smaller output; can cause 404/routing issues with Blazor)"
@@ -79,7 +81,7 @@ while [ $# -gt 0 ]; do
echo " Alpine (musl):"
echo " linux-musl-x64 Alpine amd64"
echo " linux-musl-arm64 Alpine aarch64"
echo " Ubuntu (use on Ubuntu to avoid TypeLoadException with generic RID):"
echo " Ubuntu (local builds only; not in CI releases; use if prebuilt linux-x64 fails):"
echo " ubuntu.24.04-x64 ubuntu.24.04-arm64"
echo " ubuntu.22.04-x64 ubuntu.22.04-arm64"
echo ""
@@ -87,11 +89,11 @@ while [ $# -gt 0 ]; do
echo ""
echo "Examples:"
echo " $0 # Publish for current machine (auto-detect RID)"
echo " $0 ubuntu.24.04-x64 # Publish for Ubuntu 24.04 amd64"
echo " $0 linux-x64 # Publish for glibc amd64 (Debian, Ubuntu, etc.)"
echo " $0 linux-arm64 # Publish for Raspberry Pi 4/5 or other aarch64"
echo " $0 linux-musl-x64 # Publish for Alpine (e.g. Docker)"
echo " $0 --trim linux-x64 # Smaller build (not recommended for Blazor)"
echo " $0 --no-clean ubuntu.24.04-x64 # Skip clean (faster; same RID/options as last run)"
echo " $0 --no-clean linux-x64 # Skip clean (faster; same RID/options as last run)"
exit 0
;;
--trim)
+4 -5
View File
@@ -6,11 +6,10 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
<link rel="stylesheet" href="@Assets["lib/bootstrap/dist/css/bootstrap.min.css"]" />
<link rel="stylesheet" href="@Assets["lib/bootstrap-icons/bootstrap-icons.min.css"]" />
<link rel="stylesheet" href="@Assets["app.css"]" />
<link rel="stylesheet" href="@Assets["DnsmasqWebUI.styles.css"]" />
<ImportMap />
<link rel="stylesheet" href="lib/bootstrap/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="lib/bootstrap-icons/bootstrap-icons.min.css" />
<link rel="stylesheet" href="app.css" />
<link rel="stylesheet" href="DnsmasqWebUI.styles.css" />
<link rel="icon" type="image/png" sizes="16x16" href="favicon-16.png" />
<link rel="icon" type="image/png" sizes="32x32" href="favicon-32.png" />
<link rel="icon" type="image/png" sizes="48x48" href="favicon-48.png" />
@@ -4,14 +4,13 @@
<div class="top-row ps-3 navbar navbar-dark">
<div class="container-fluid d-flex align-items-center">
<a class="navbar-brand d-flex align-items-center gap-2" href="" aria-label="@AppTitle home">
<img src="logo.webp" alt="" class="brand-logo" />
<span class="brand-logo-wrap"><img src="logo.webp" alt="" class="brand-logo" /></span>
<span>@AppTitle</span>
</a>
</div>
<input type="checkbox" title="Navigation menu" class="navbar-toggler" id="nav-toggler" aria-label="Toggle navigation" />
</div>
<input type="checkbox" title="Navigation menu" class="navbar-toggler" id="nav-toggler" aria-label="Toggle navigation" />
<div class="nav-scrollable" onclick="document.getElementById('nav-toggler')?.click()">
<nav class="nav flex-column" aria-label="Main navigation">
<div class="nav-item px-3">
@@ -5,8 +5,9 @@
height: 2.5rem;
color: white;
position: absolute;
top: 0.5rem;
top: 50%;
right: 1rem;
transform: translateY(-50%);
border: 1px solid rgba(255, 255, 255, 0.1);
background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1);
}
@@ -16,6 +17,7 @@
}
.top-row {
position: relative;
min-height: 3.5rem;
background-color: rgba(0,0,0,0.4);
}
@@ -31,13 +33,7 @@
color: white !important;
}
.brand-logo {
display: block;
flex-shrink: 0;
height: 1.75rem;
width: auto;
vertical-align: middle;
}
/* Logo size/alignment: see app.css .sidebar .navbar-brand .brand-logo-wrap / .brand-logo */
/* Bootstrap Icons in nav: white, sized to match link text */
.nav-item .nav-icon {
@@ -48,13 +48,9 @@ else if (_status != null)
</ul>
}
<EffectiveConfigSection Status="_status" />
@if (_status.DnsmasqStatus != "active" && (_status.StatusCommandStdout != null || _status.StatusCommandStderr != null))
{
<pre class="pre-block mt-2 mb-0" style="max-height: 12rem;">@(_status.StatusCommandStdout != null ? _status.StatusCommandStdout : "")@(_status.StatusCommandStdout != null && _status.StatusCommandStderr != null ? "\n" : "")@(_status.StatusCommandStderr ?? "")</pre>
}
@if (_status.ReloadCommandConfigured)
{
<div class="d-flex align-items-center gap-2 flex-wrap">
<div class="d-flex align-items-center gap-2 flex-wrap mt-3">
<button class="btn btn-primary btn-sm" @onclick="ReloadDnsmasq" disabled="@_reloading">
@if (_reloading)
{
+3
View File
@@ -4,6 +4,9 @@
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<!-- Avoid pre-compressed .br/.gz so MapStaticAssets serves uncompressed files (fixes 0-byte/empty response bug). -->
<DisableBuildCompression>true</DisableBuildCompression>
<PublishCompressionFormats></PublishCompressionFormats>
</PropertyGroup>
<ItemGroup>
+7 -2
View File
@@ -3,7 +3,11 @@ using DnsmasqWebUI.Extensions;
using DnsmasqWebUI.Configuration;
using Microsoft.Extensions.Options;
var builder = WebApplication.CreateBuilder(args);
// When not in Development, use the app's directory (not CWD) so static assets work when run via symlink or from any CWD.
var isDevelopment = string.Equals(Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"), "Development", StringComparison.OrdinalIgnoreCase);
var builder = isDevelopment
? WebApplication.CreateBuilder(args)
: WebApplication.CreateBuilder(new WebApplicationOptions { ContentRootPath = AppContext.BaseDirectory.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar), Args = args });
// ---- Application options (title, etc.) ----
builder.Services.AddOptions<ApplicationOptions>()
@@ -55,7 +59,8 @@ app.UseAntiforgery();
// ---- Endpoints ----
app.MapControllers();
app.MapStaticAssets();
// UseStaticFiles: MapStaticAssets returns 0-byte responses for fingerprinted assets (known bug). Serve from wwwroot directly.
app.UseStaticFiles();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
+18
View File
@@ -209,3 +209,21 @@ h1:focus {
.bi-gear-sm {
font-size: 1.125rem;
}
/* Sidebar brand logo – 64px, header stays 3.5rem min-height */
.sidebar .navbar-brand .brand-logo-wrap {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 4rem; /* 64px */
height: 4rem;
}
.sidebar .navbar-brand .brand-logo {
display: block;
height: 4rem;
width: auto;
object-fit: contain;
vertical-align: middle;
}
+4 -2
View File
@@ -1,4 +1,4 @@
# Config for the dnsmasq test container only (docker-compose.test.yml).
# Config for the dnsmasq test container only (docker-compose.test.yml).
# DNS + DHCP: forwards DNS to Cloudflare/Google with caching; hands out DHCP on testnet.
# Uses same testdata layout so app and dnsmasq share hosts, dhcp.conf, leases.
# Richer config so UI shows multi-value lists (server, addn-hosts, address, dhcp-range) and readonly sources.
@@ -31,4 +31,6 @@ addn-hosts=/data/hosts.extra
# Log to file so the harness can show real logs (LogsCommand tails this)
log-facility=/data/dnsmasq.log
log-queries
log-queries
conf-file=zz-dnsmasq-webui.conf
+3 -1
View File
@@ -1,4 +1,6 @@
# Sample main dnsmasq config (not managed by this app).
# Sample main dnsmasq config (not managed by this app).
# In production this would typically be /etc/dnsmasq.conf.
# It includes all .conf files from dnsmasq.d/ (e.g. dhcp.conf managed by this app).
conf-dir=/etc/dnsmasq.d/
conf-file=zz-dnsmasq-webui.conf