Effective config refactor, remove dead parser path, add source line numbers

Effective config:
- Add EffectiveConfigFieldDescriptor (metadata + value/source/items delegates)
- Add EffectiveConfigFieldBuilder with Config/Sources/Items helpers; build all
  fields when EffectiveConfig present (empty = not set); empty list when null
- Add EffectiveConfigSectionPanel + EffectiveConfigFieldDisplay; section CSS
  uses ::deep for child markup, open-state styling for headers
- Remove Kind from descriptor (use DnsmasqConfOptionRegistry.GetKind(OptionName)
  when needed)

Dead code removal:
- Remove DnsmasqOptionModels, ParsedDirective (DnsmasqConfDirective),
  DnsmasqOptionKind, DnsmasqConfOptionRegistry
- Remove ParseLine from DnsmasqConfDirectiveParser; keep StripComment and
  TryParseKeyValue (used by DnsmasqConfIncludeParser)
- Simplify DnsmasqConfDirectiveParserTests; remove DnsmasqConfOptionRegistryTests

ConfigValueSource:
- Add LineNumber (int?); IncludeParser passes 1-based line to MakeSource
- GetReadOnlyTooltip includes 'line N' when LineNumber set
- Add ConfigValueSourceTests; assert LineNumber in IncludeParser WithSource tests

Also: .github/workflows/release.yml, scripts/install.sh, script tweaks
This commit is contained in:
2026-02-04 20:23:59 +10:00
parent 368489e5f2
commit 633c1ef827
23 changed files with 783 additions and 776 deletions
+230
View File
@@ -0,0 +1,230 @@
#!/usr/bin/env sh
# Download and install dnsmasq-webui from a GitHub release. Picks the binary
# matching the current OS/arch (RID). Run from a clone or via:
# curl -sSL https://raw.githubusercontent.com/OWNER/REPO/main/scripts/install.sh | sh -s -- [OPTIONS]
# When run from a clone, repo is detected from git remote origin. Otherwise set
# GITHUB_REPO=owner/repo or pass --repo owner/repo.
set -e
GITHUB_REPO="${GITHUB_REPO:-}"
VERSION=""
LIST=false
INSTALL_DIR=""
usage() {
echo "Usage: $0 [OPTIONS]"
echo ""
echo "Download and install dnsmasq-webui from a GitHub release for this machine's OS/arch."
echo ""
echo "Repo (when not in a clone):"
echo " --repo OWNER/REPO GitHub owner/repo (e.g. myuser/dnsmasq-webui)."
echo " Or set GITHUB_REPO=owner/repo."
echo " When run inside a git clone with origin, repo is detected automatically."
echo ""
echo "Release:"
echo " --list List available releases (tag, name, published_at) and exit."
echo " --version TAG Install from release with tag TAG (e.g. v1.0.0). Default: latest."
echo " -h, -?, --help Show this help."
echo ""
echo "Install:"
echo " --dir DIR Install into DIR (default: ./dnsmasq-webui in current directory)."
echo ""
echo "Examples:"
echo " $0 # In clone: install latest into ./dnsmasq-webui"
echo " $0 --repo owner/dnsmasq-webui # Install latest from owner/dnsmasq-webui"
echo " $0 --repo owner/dnsmasq-webui --version v1.0.0"
echo " $0 --repo owner/dnsmasq-webui --list # List releases"
echo " GITHUB_REPO=owner/dnsmasq-webui $0 --dir /opt/dnsmasq-webui"
echo ""
echo "After install, configure via appsettings.json or Dnsmasq__* environment variables, then run the binary."
exit 0
}
# Detect owner/repo from git remote if we're in a clone.
detect_repo() {
if [ -n "$GITHUB_REPO" ]; then
return
fi
if command -v git >/dev/null 2>&1 && git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
origin="$(git remote get-url origin 2>/dev/null)" || true
if [ -n "$origin" ]; then
# https://github.com/owner/repo or git@github.com:owner/repo.git
GITHUB_REPO="$(echo "$origin" | sed -E 's|^https://github\.com/||; s|^git@github\.com:||; s|\.git$||; s|/$||')"
if [ -n "$GITHUB_REPO" ]; then
return
fi
fi
fi
echo "Error: GitHub repo not set. Use --repo owner/repo or set GITHUB_REPO=owner/repo (or run from a clone)." >&2
exit 1
}
# Require jq for parsing GitHub API JSON.
check_jq() {
if command -v jq >/dev/null 2>&1; then
return
fi
echo "Error: jq is required to parse GitHub API responses. Install jq (e.g. apt install jq)." >&2
exit 1
}
# Detect RID (same logic as publish-self-contained.sh).
detect_arch() {
case "$(uname -m)" in
x86_64|amd64) echo "x64" ;;
aarch64|arm64) echo "arm64" ;;
armv7l|armhf) echo "arm" ;;
*) echo "x64" ;;
esac
}
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 ;;
arm64) echo "linux-musl-arm64"; return ;;
*) echo "linux-musl-x64"; return ;;
esac ;;
esac
fi
case "$arch" in
arm64) echo "linux-arm64" ;;
arm) echo "linux-arm" ;;
*) echo "linux-x64" ;;
esac
}
# GET release (latest or by tag). Output raw JSON to stdout.
fetch_release() {
local api_url
if [ -z "$VERSION" ] || [ "$VERSION" = "latest" ]; then
api_url="https://api.github.com/repos/$GITHUB_REPO/releases/latest"
else
api_url="https://api.github.com/repos/$GITHUB_REPO/releases/tags/$VERSION"
fi
resp="$(curl -sSL -A "dnsmasq-webui-install/1.0" -w "\n%{http_code}" "$api_url")"
code="$(echo "$resp" | tail -n1)"
body="$(echo "$resp" | sed '$d')"
if [ "$code" != "200" ]; then
echo "Error: GitHub API returned $code for $api_url" >&2
echo "$body" | jq -r '.message // .' 2>/dev/null || echo "$body" >&2
exit 1
fi
echo "$body"
}
# List releases and exit.
list_releases() {
check_jq
detect_repo
api_url="https://api.github.com/repos/$GITHUB_REPO/releases?per_page=20"
resp="$(curl -sSL -A "dnsmasq-webui-install/1.0" -w "\n%{http_code}" "$api_url")"
code="$(echo "$resp" | tail -n1)"
body="$(echo "$resp" | sed '$d')"
if [ "$code" != "200" ]; then
echo "Error: GitHub API returned $code" >&2
exit 1
fi
echo "$body" | jq -r '.[] | "\(.tag_name) \(.name) \(.published_at // .created_at)"'
exit 0
}
# Find asset download URL whose name contains the given RID.
find_asset_url() {
local release_json rid
release_json="$1"
rid="$2"
echo "$release_json" | jq -r --arg rid "$rid" '
.assets[] | select(.name | test($rid)) | .browser_download_url
' | head -n1
}
# Main install: fetch release, find asset for RID, download, extract.
do_install() {
check_jq
detect_repo
rid="$(detect_rid)"
echo "Detected RID: $rid"
echo "Fetching release..."
release_json="$(fetch_release)"
tag="$(echo "$release_json" | jq -r '.tag_name')"
echo "Release: $tag"
url="$(find_asset_url "$release_json" "$rid")"
if [ -z "$url" ] || [ "$url" = "null" ]; then
echo "Error: No asset found for RID $rid in release $tag." >&2
echo "Available assets:" >&2
echo "$release_json" | jq -r '.assets[].name' | sed 's/^/ /' >&2
exit 1
fi
if [ -z "$INSTALL_DIR" ]; then
INSTALL_DIR="./dnsmasq-webui"
fi
mkdir -p "$INSTALL_DIR"
tmpzip="${TMPDIR:-/tmp}/dnsmasq-webui-$rid.zip"
echo "Downloading $url ..."
curl -sSL -A "dnsmasq-webui-install/1.0" -o "$tmpzip" "$url"
echo "Extracting to $INSTALL_DIR ..."
unzip -o -q "$tmpzip" -d "$INSTALL_DIR"
rm -f "$tmpzip"
echo ""
echo "Installed to $INSTALL_DIR"
echo "Run: $INSTALL_DIR/DnsmasqWebUI"
echo "Configure via appsettings.json in that directory or Dnsmasq__* environment variables (e.g. Dnsmasq__MainConfigPath=/etc/dnsmasq.conf)."
}
# Parse args
while [ $# -gt 0 ]; do
case "$1" in
-h|-?|--help)
usage
;;
--repo)
shift
[ $# -gt 0 ] || { echo "Error: --repo requires OWNER/REPO" >&2; exit 1; }
GITHUB_REPO="$1"
shift
;;
--list)
LIST=true
shift
;;
--version)
shift
[ $# -gt 0 ] || { echo "Error: --version requires TAG" >&2; exit 1; }
VERSION="$1"
shift
;;
--dir)
shift
[ $# -gt 0 ] || { echo "Error: --dir requires DIR" >&2; exit 1; }
INSTALL_DIR="$1"
shift
;;
-*)
echo "Error: unknown option $1" >&2
usage >&2
exit 1
;;
*)
echo "Error: unexpected argument $1" >&2
usage >&2
exit 1
;;
esac
done
if [ "$LIST" = true ]; then
list_releases
fi
do_install
+47 -27
View File
@@ -3,7 +3,7 @@
# (app + dnsmasq + DHCP client). See testdata/README.md and docker-compose.test.yml.
#
# What this script does:
# Start (default): clear mount (unless --no-clear), sync source -> mount, remove *dnsmasq-webui*.conf,
# Start (default): clear mount (unless --no-clear), sync source -> mount, clean up previous test data,
# then docker compose up -d [--build] [--force-recreate].
# --stop: docker compose down (stop and remove containers/networks).
# --tidy: docker compose down, then clear the mount directory for a clean next run.
@@ -16,7 +16,7 @@ COMPOSE_FILE="docker-compose.test.yml"
SOURCE_DIR=""
MOUNT_DIR=""
PREPARE_ONLY=false
NO_BUILD=false
BUILD=false
RECREATE=false
NO_CLEAR=false
STOP=false
@@ -25,13 +25,12 @@ TIDY=false
usage() {
echo "Usage: $0 [OPTIONS] [--]"
echo ""
echo "Prepare the testdata mount directory and optionally start the Docker test harness"
echo "(app with dnsmasq in one container, plus a DHCP client). The mount is synced from"
echo "testdata/ by default (includes sample leases file so the harness shows leases)."
echo "Prepare the testdata mount directory and optionally start or stop the Docker test harness"
echo "(app + dnsmasq + DHCP client). By default: clear mount, sync testdata -> mount, then up -d (no rebuild)."
echo ""
echo "Steps:"
echo "Steps (when starting):"
echo " 1. Clear mount dir (unless --no-clear), then sync source -> mount."
echo " 2. Remove any leftover *dnsmasq-webui*.conf so dnsmasq starts clean."
echo " 2. Clean up previous test data (e.g. managed config) so the harness starts clean."
echo " 3. If not --prepare-only: docker compose -f $COMPOSE_FILE up -d [options]."
echo ""
echo "Path options:"
@@ -47,31 +46,32 @@ usage() {
echo "Compose behaviour:"
echo " --prepare-only Only prepare the mount; do not run docker compose."
echo " Use to inspect or edit the mount before starting containers."
echo " --no-build Do not pass --build to docker compose (use existing images)."
echo " Use for a quick restart when only the mount changed."
echo " --recreate Pass --force-recreate to docker compose (recreate containers)."
echo " --build Pass --build to docker compose (rebuild images before starting)."
echo " Use after changing the app or Dockerfile. Default: use existing images."
echo " --no-build Do not rebuild (default). Use existing images for a quick restart."
echo " --recreate Pass --force-recreate to docker compose (recreate containers)."
echo " Use to ensure fresh container state and mounts."
echo ""
echo "Stop / tidy:"
echo "Stop / tidy (take precedence: other options are ignored when used):"
echo " --stop Stop the test harness: docker compose down (no prepare, no start)."
echo " --tidy Stop the harness and clear the mount directory for a clean next run."
echo " Uses default mount dir unless --mount DIR is given."
echo ""
echo "Other:"
echo " -h, --help Show this help and exit."
echo " -h, -?, --help Show this help and exit."
echo ""
echo "Examples:"
echo " $0"
echo " Full run: clear mount, sync testdata, build and start containers."
echo " Full run: clear mount, sync testdata, start containers (no rebuild)."
echo ""
echo " $0 --no-build"
echo " Clear and sync, then start containers without rebuilding images."
echo " $0 --build"
echo " Clear and sync, then rebuild images and start containers."
echo ""
echo " $0 --recreate"
echo " Clear and sync, then up --build --force-recreate (clean containers)."
echo " Clear and sync, then up -d --force-recreate (fresh containers)."
echo ""
echo " $0 --no-clear --no-build"
echo " Preserve mount contents, sync over it, start without rebuild."
echo " $0 --no-clear"
echo " Preserve mount contents, sync over it, start (no rebuild)."
echo ""
echo " $0 --prepare-only"
echo " Only clear and sync testdata -> testdata-mount; no containers."
@@ -90,7 +90,7 @@ usage() {
while [ $# -gt 0 ]; do
case "$1" in
-h|--help)
-h|-?|--help)
usage
exit 0
;;
@@ -114,8 +114,12 @@ while [ $# -gt 0 ]; do
PREPARE_ONLY=true
shift
;;
--build)
BUILD=true
shift
;;
--no-build)
NO_BUILD=true
BUILD=false
shift
;;
--recreate)
@@ -134,14 +138,25 @@ while [ $# -gt 0 ]; do
shift
break
;;
*)
-?*)
echo "Error: unknown option $1" >&2
usage >&2
exit 1
;;
*)
echo "Error: unexpected argument: $1" >&2
usage >&2
exit 1
;;
esac
done
if [ $# -gt 0 ]; then
echo "Error: unexpected argument: $1" >&2
usage >&2
exit 1
fi
cd "$REPO_ROOT"
: "${SOURCE_DIR:=testdata}"
@@ -186,23 +201,28 @@ else
rm -f "$MOUNT_DIR/leases"
fi
# Remove any leftover managed config from previous runs so dnsmasq starts clean (app will create zz-dnsmasq-webui.conf on startup).
# Clean up previous test data (e.g. managed config) so the harness starts clean (app will create zz-dnsmasq-webui.conf on startup).
find "$MOUNT_DIR" -name '*dnsmasq-webui*.conf' -type f -delete 2>/dev/null || true
echo "Mount directory ready: $MOUNT_DIR (source: $SOURCE_DIR)."
if [ "$PREPARE_ONLY" = true ]; then
echo "To start the harness: TESTDATA_MOUNT=./$MOUNT_DIR docker compose -f $COMPOSE_FILE up -d --build"
case "$MOUNT_DIR" in
/*) MOUNT_EXPORT="$MOUNT_DIR" ;;
*) MOUNT_EXPORT="./$MOUNT_DIR" ;;
esac
echo "To start the harness: TESTDATA_MOUNT=$MOUNT_EXPORT docker compose -f $COMPOSE_FILE up -d [--build]"
exit 0
fi
# Compose uses TESTDATA_MOUNT for the data volume (default: ./testdata-mount)
export TESTDATA_MOUNT="./$MOUNT_DIR"
case "$MOUNT_DIR" in
/*) export TESTDATA_MOUNT="$MOUNT_DIR" ;;
*) export TESTDATA_MOUNT="./$MOUNT_DIR" ;;
esac
COMPOSE_CMD="docker compose -f $COMPOSE_FILE up -d"
if [ "$NO_BUILD" = true ]; then
COMPOSE_CMD="$COMPOSE_CMD"
else
if [ "$BUILD" = true ]; then
COMPOSE_CMD="$COMPOSE_CMD --build"
fi
if [ "$RECREATE" = true ]; then
+14 -2
View File
@@ -59,16 +59,17 @@ default_rid() {
while [ $# -gt 0 ]; do
case "$1" in
-h|--help)
-h|-?|--help)
echo "Usage: $0 [OPTIONS] [RID]"
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 ""
echo "Options:"
echo " --trim Enable trimming (smaller output; can cause 404/routing issues with Blazor)"
echo " --no-clean Skip clean before publish (faster; use only if same RID and options as last run)"
echo " -h, --help Show this help"
echo " -h, -?, --help Show this help"
echo ""
echo "Supported RIDs:"
echo " Generic (glibc):"
@@ -101,6 +102,11 @@ while [ $# -gt 0 ]; do
CLEAN=false
shift
;;
-*)
echo "Error: unknown option $1" >&2
echo "Use -h, -?, or --help for usage." >&2
exit 1
;;
*)
RID="$1"
shift
@@ -109,6 +115,12 @@ while [ $# -gt 0 ]; do
esac
done
if [ $# -gt 0 ]; then
echo "Error: unexpected argument: $1" >&2
echo "Use -h, -?, or --help for usage." >&2
exit 1
fi
if [ -z "$RID" ]; then
RID="$(default_rid)"
AUTO_RID=true