From aa4f1f1d9c8ea688c8ee07ebbe6d744ccde83c78 Mon Sep 17 00:00:00 2001 From: librelad Date: Thu, 20 Aug 2026 01:32:17 +0100 Subject: [PATCH] fix(config): reconcile restores port columns a truncated row dropped MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The port editor used to serialise ten of the twelve columns, so saving any port on an app silently discarded that app's Traefik subdomain and the router fell back to the app-name default. Navidrome lost "music" and Speedtest lost "speedtest" exactly that way, and nothing reported it — the app kept working, on the wrong hostname. The writer is fixed, but an install already carrying the damage would keep it forever: reconcile preserves the user's value, and a truncated row IS the user's value as far as it can tell. It now tops such a row up from the template, appending ONLY the columns the live row does not reach. Everything the live row states wins — including a deliberately blanked column — so clearing a subdomain is not undone, and a live row longer than its template is left alone. Two faults of my own, caught while testing it end to end: The notice was printed on stdout. This function returns its result through a command substitution, so the notice text was captured INTO the config value and written to navidrome's descriptor. It goes to stderr. The width in the message was measured after the merge, so it reported the post-merge count as the "before". Captured up front instead. Verified against a live install: truncating navidrome's row to 9 columns and running `config check` restores it to 11 with "music" intact, a second run changes nothing, and no port on the box is left Traefik- managed without a subdomain. Unit-checked that it declines to act on a complete row, a non-port key, a row longer than its template, and never overwrites a live value; quoting style is preserved either way. Co-Authored-By: Claude Opus 5 --- .../core/variables/config_scan_variables.sh | 55 ++++++++++++++++++- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/scripts/config/core/variables/config_scan_variables.sh b/scripts/config/core/variables/config_scan_variables.sh index a45ac65..944dfe3 100755 --- a/scripts/config/core/variables/config_scan_variables.sh +++ b/scripts/config/core/variables/config_scan_variables.sh @@ -34,6 +34,53 @@ _reconcileSplitValueComment() fi } +# Restore trailing columns a port descriptor has LOST, without touching any +# value the live config already states. +# +# CFG__PORT_ is a positional, pipe-separated row whose last three +# columns (url_path, subdomain, recommended) are optional. The port editor used +# to serialise only ten of them, so saving any port on an app silently dropped +# that app's Traefik subdomain and the router quietly fell back to the app-name +# default. Navidrome lost "music" and Speedtest lost "speedtest" exactly that +# way, and nothing said so — the app kept working on the wrong hostname. +# +# The writer is fixed, but installs already carrying the damage would keep it +# forever: reconcile preserves the user's value, and a truncated row IS the +# user's value as far as it can tell. So top it up from the template, appending +# only the columns the live row does not reach. Everything the live row states +# wins, including a deliberately blanked one, so a user who cleared a subdomain +# does not get it handed back. +# +# Echoes the value to use. +_reconcilePortColumns() +{ + local key="$1" live_v="$2" tmpl_v="$3" + [[ "$key" =~ _PORT_[0-9]+$ ]] || { printf '%s' "$live_v"; return 0; } + + # Values are stored quoted; compare and rebuild on the bare text. + local lq="" l="$live_v" t="$tmpl_v" + if [[ "$l" == \"*\" ]]; then lq='"'; l="${l%\"}"; l="${l#\"}"; fi + if [[ "$t" == \"*\" ]]; then t="${t%\"}"; t="${t#\"}"; fi + [[ -n "$l" && -n "$t" ]] || { printf '%s' "$live_v"; return 0; } + + local -a L T + IFS='|' read -ra L <<< "$l" + IFS='|' read -ra T <<< "$t" + (( ${#L[@]} < ${#T[@]} )) || { printf '%s' "$live_v"; return 0; } + + # Count BEFORE extending, or the message reports the post-merge width. + local was=${#L[@]} + local k + for (( k=${#L[@]}; k<${#T[@]}; k++ )); do L+=("${T[k]}"); done + local rebuilt; rebuilt="$(IFS='|'; printf '%s' "${L[*]}")" + # To STDERR. This function returns its result on stdout through a command + # substitution, so anything isNotice prints there lands INSIDE the config + # value — which is exactly what happened the first time, writing the notice + # text into navidrome's descriptor. + isNotice "Restored port descriptor $key from $was to ${#L[@]} columns (a truncated row had dropped its trailing column(s))." >&2 + printf '%s%s%s' "$lq" "$rebuilt" "$lq" +} + reconcileConfigFile() { local live="$1" template="$2" @@ -66,10 +113,14 @@ reconcileConfigFile() if [[ -n "${live_value[$key]+x}" ]]; then # Keep the user's value, take the template's comment (so # title/description/options/markers stay in sync with the repo). + # One exception, and it only ever ADDS: a port descriptor that + # has lost its trailing columns is topped up from the template. + local kept="${live_value[$key]}" + kept="$(_reconcilePortColumns "$key" "$kept" "$tmpl_value")" if [[ -n "$tmpl_comment" ]]; then - merged="${key}=${live_value[$key]} ${tmpl_comment}" + merged="${key}=${kept} ${tmpl_comment}" else - merged="${key}=${live_value[$key]}" + merged="${key}=${kept}" fi printf '%s\n' "$merged" >> "$tmp" emitted["$key"]=1