Effective config: do-0x20-encode revert fix, validation UI, custom displays, option help

- Fix do-0x20-encode: clearing pending when toggling back to original (Disabled/Default)
  - Do0x20EncodeDisplay: uncheck uses pending.OldValue when present
  - EffectiveConfigFieldDisplay: use pending.OldValue as _valueAtActivation when re-entering with pending
- Effective config: validation errors/warnings in toolbar and save modal, revert from modal
- Custom displays: Do0x20EncodeDisplay (tri-state), KeyOnlyOrValueDisplay
- ExplicitToggleState, config/apply support for do-0x20-encode, option help and tests
This commit is contained in:
2026-03-08 12:52:46 +10:00
parent 6cf4c07695
commit a03e93bcd3
89 changed files with 2229 additions and 67 deletions
+51
View File
@@ -171,6 +171,57 @@ OPTION_KEYS = [
"keep-in-foreground",
"no-daemon",
"conntrack",
"connmark-allowlist-enable",
"no-round-robin",
"dns-forward-max",
"connmark-allowlist",
"do-0x20-encode",
"caa-record",
"dns-rr",
"synth-domain",
"auth-zone",
"auth-soa",
"auth-sec-servers",
"auth-peer",
"leasequery",
"dhcp-generate-names",
"dhcp-broadcast",
"dhcp-sequential-ip",
"dhcp-ignore-clid",
"bootp-dynamic",
"no-ping",
"script-arp",
"script-on-renewal",
"dhcp-no-override",
"dhcp-alternate-port",
"dhcp-duid",
"dhcp-luascript",
"dhcp-scriptuser",
"dhcp-relay",
"dhcp-circuitid",
"dhcp-remoteid",
"dhcp-subscrid",
"dhcp-proxy",
"tag-if",
"bridge-interface",
"shared-network",
"dhcp-pxe-vendor",
"dhcp-option-pxe",
"dnssec-no-timecheck",
"dnssec-debug",
"add-cpe-id",
"dnssec-timestamp",
"dnssec-limits",
"dumpfile",
"dumpmask",
"use-stale-cache",
"add-mac",
"add-subnet",
"umbrella",
"quiet-dhcp",
"quiet-dhcp6",
"quiet-ra",
"quiet-tftp",
]
long_opt_re = re.compile(r"--([a-z][a-z0-9-]*)(?:=[^,\s]*)?", re.IGNORECASE)
+89 -3
View File
@@ -15,8 +15,10 @@ COMPOSE_FILE="docker-compose.test.yml"
SOURCE_DIR=""
MOUNT_DIR=""
DNSMASQ_VERSION=""
PREPARE_ONLY=false
BUILD=false
NO_CACHE_BUILD=false
RECREATE=false
CLEAR=false
STOP=false
@@ -38,6 +40,9 @@ usage() {
echo " --source DIR Source to copy from (default: testdata)"
echo " --mount DIR Target mount directory (default: testdata-mount)"
echo " Compose uses TESTDATA_MOUNT; script exports it if you use --mount."
echo " --dnsmasq-version V Dnsmasq version for the harness image:"
echo " latest (default), distro, or an exact upstream version like 2.91."
echo " Non-default values require --build or --no-cache-build; script fails with --no-build."
echo ""
echo "Mount behaviour:"
echo " (default) Preserve mount dir; sync source over existing contents."
@@ -50,7 +55,10 @@ usage() {
echo " Use to inspect or edit the mount before starting 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-cache-build Rebuild images with --pull --no-cache before starting."
echo " Use to force a fresh image build and refresh 'latest' dnsmasq."
echo " --no-build Do not rebuild (default). Use existing images for a quick restart."
echo " With --no-build, 'latest' is not resolved (no network)."
echo " --recreate Pass --force-recreate to docker compose (recreate containers)."
echo " Use to ensure fresh container state and mounts."
echo ""
@@ -72,6 +80,9 @@ usage() {
echo " $0 --recreate"
echo " Sync, then up -d --force-recreate (fresh containers)."
echo ""
echo " $0 --no-cache-build"
echo " Force a fresh image build (no Docker build cache), then start containers."
echo ""
echo " $0 --clear"
echo " Clear mount, sync testdata, start (clean run, no rebuild)."
echo ""
@@ -91,6 +102,35 @@ usage() {
echo ""
echo " $0 --minimal-conf"
echo " Use dnsmasq-test-minimal.conf (single file, few options) and start."
echo ""
echo " $0 --dnsmasq-version 2.91 --build"
echo " Rebuild the harness image with dnsmasq 2.91."
}
fetch_url() {
if command -v curl >/dev/null 2>&1; then
curl -fsSL "$1"
return $?
fi
if command -v wget >/dev/null 2>&1; then
wget -qO- "$1"
return $?
fi
echo "Error: need curl or wget to resolve the latest upstream dnsmasq version." >&2
return 1
}
resolve_dnsmasq_version() {
if [ "$DNSMASQ_VERSION" != "latest" ]; then
return 0
fi
RESOLVED_VERSION="$(fetch_url "https://thekelleys.org.uk/dnsmasq/" | sed -n 's/.*LATEST_IS_\([0-9][0-9.]*\).*/\1/p' | head -n1)"
if [ -z "$RESOLVED_VERSION" ]; then
echo "Error: could not resolve the latest upstream dnsmasq version." >&2
exit 1
fi
DNSMASQ_VERSION="$RESOLVED_VERSION"
}
while [ $# -gt 0 ]; do
@@ -111,6 +151,12 @@ while [ $# -gt 0 ]; do
MOUNT_DIR="$1"
shift
;;
--dnsmasq-version)
shift
[ $# -gt 0 ] || { echo "Error: --dnsmasq-version requires VERSION" >&2; exit 1; }
DNSMASQ_VERSION="$1"
shift
;;
--clear)
CLEAR=true
shift
@@ -123,8 +169,14 @@ while [ $# -gt 0 ]; do
BUILD=true
shift
;;
--no-cache-build)
NO_CACHE_BUILD=true
BUILD=true
shift
;;
--no-build)
BUILD=false
NO_CACHE_BUILD=false
shift
;;
--recreate)
@@ -170,6 +222,7 @@ cd "$REPO_ROOT"
: "${SOURCE_DIR:=testdata}"
: "${MOUNT_DIR:=testdata-mount}"
: "${DNSMASQ_VERSION:=latest}"
# Stop and/or tidy: no prepare, no start
if [ "$STOP" = true ] || [ "$TIDY" = true ]; then
@@ -192,6 +245,17 @@ if [ ! -d "$SOURCE_DIR" ]; then
exit 1
fi
# Resolve "latest" only when we will build; avoid network when doing a no-build run.
WILL_BUILD=false
[ "$BUILD" = true ] || [ "$NO_CACHE_BUILD" = true ] && WILL_BUILD=true
if [ "$WILL_BUILD" = false ] && [ "$DNSMASQ_VERSION" != "latest" ] && [ "$DNSMASQ_VERSION" != "distro" ]; then
echo "Error: --dnsmasq-version $DNSMASQ_VERSION has no effect without --build or --no-cache-build. Add --build or omit --dnsmasq-version for a no-build run." >&2
exit 1
fi
if [ "$WILL_BUILD" = true ]; then
resolve_dnsmasq_version
fi
# Take the harness down so all containers (including one-shot DHCP clients) are recreated on up.
echo "Stopping test harness: docker compose -f $COMPOSE_FILE down"
docker compose -f "$COMPOSE_FILE" down
@@ -220,9 +284,14 @@ if [ "$PREPARE_ONLY" = true ]; then
/*) MOUNT_EXPORT="$MOUNT_DIR" ;;
*) MOUNT_EXPORT="./$MOUNT_DIR" ;;
esac
START_CMD="TESTDATA_MOUNT=$MOUNT_EXPORT docker compose -f $COMPOSE_FILE up -d [--build]"
START_PREFIX="TESTDATA_MOUNT=$MOUNT_EXPORT DNSMASQ_VERSION=$DNSMASQ_VERSION"
if [ "$MINIMAL_CONF" = true ]; then
START_CMD="TESTDATA_MOUNT=$MOUNT_EXPORT TEST_DNSMASQ_CONF=/data/dnsmasq-test-minimal.conf docker compose -f $COMPOSE_FILE up -d [--build]"
START_PREFIX="$START_PREFIX TEST_DNSMASQ_CONF=/data/dnsmasq-test-minimal.conf"
fi
if [ "$NO_CACHE_BUILD" = true ]; then
START_CMD="$START_PREFIX docker compose -f $COMPOSE_FILE build --pull --no-cache && $START_PREFIX docker compose -f $COMPOSE_FILE up -d"
else
START_CMD="$START_PREFIX docker compose -f $COMPOSE_FILE up -d [--build]"
fi
echo "To start the harness: $START_CMD"
exit 0
@@ -234,13 +303,30 @@ case "$MOUNT_DIR" in
*) export TESTDATA_MOUNT="./$MOUNT_DIR" ;;
esac
export DNSMASQ_VERSION
case "$DNSMASQ_VERSION" in
distro)
echo "Using distro-packaged dnsmasq for the harness image."
;;
*)
echo "Using dnsmasq $DNSMASQ_VERSION for the harness image."
;;
esac
if [ "$MINIMAL_CONF" = true ]; then
export TEST_DNSMASQ_CONF="/data/dnsmasq-test-minimal.conf"
echo "Using minimal config: $TEST_DNSMASQ_CONF"
fi
if [ "$NO_CACHE_BUILD" = true ]; then
BUILD_CMD="docker compose -f $COMPOSE_FILE build --pull --no-cache"
echo "Running: $BUILD_CMD"
$BUILD_CMD
fi
COMPOSE_CMD="docker compose -f $COMPOSE_FILE up -d"
if [ "$BUILD" = true ]; then
if [ "$BUILD" = true ] && [ "$NO_CACHE_BUILD" != true ]; then
COMPOSE_CMD="$COMPOSE_CMD --build"
fi
if [ "$RECREATE" = true ]; then