From eec57cbc10a683fa99ecb8fd44eb59e483c7e5d1 Mon Sep 17 00:00:00 2001 From: Tobias Gesellchen Date: Sun, 16 Aug 2026 20:41:40 +0200 Subject: [PATCH] fix(admin-ui): Migrate tab blocked on-device localhost by default validateURL() unconditionally rejected the hostnames "localhost" and "127.0.0.1" for the four Migrate-tab plan URL fields, with no awareness of deployment mode. Since the Suggested Plan's URLs are derived from the page's own configured Target URL, a fresh on-device install (whose server_url is now correctly http://localhost:8000, since #546) loaded the Migrate tab with "Apply Suggested Plan" and "Pre-flight" disabled by default, before the user touched anything -- directly contradicting the on-device docs' "Migrate -> accept the suggested plan -> apply" instructions. Found while investigating why a #614 reporter used the non-standard "localhost.localdomain" as a workaround, and why a #621 reporter got stuck with "Migration Status: Migrated (URL mismatch)" trying to follow the (correct) on-device localhost guidance. Fix: a loopback URL is only flagged when it doesn't match the plan's own Target URL origin. A field that's exactly what the service itself is already configured to answer as (the on-device case) is accepted; a stray "localhost" typed into one field while Target URL is a real LAN address (the external-host mistake the check exists to catch) is still flagged, since the origins differ. Co-Authored-By: Claude Sonnet 5 --- pkg/service/handlers/web/js/script.js | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/pkg/service/handlers/web/js/script.js b/pkg/service/handlers/web/js/script.js index db85079..0baca47 100644 --- a/pkg/service/handlers/web/js/script.js +++ b/pkg/service/handlers/web/js/script.js @@ -2882,7 +2882,16 @@ function readPlanURLOptions() { // on the speaker itself). For the typical "AfterTouch on a separate // host" deployment, the speaker can't reach loopback on a different // machine, so the URL must be a LAN-reachable IP or hostname. -function validateURL(value) { +// +// referenceOrigin (optional) is the plan's own Target URL origin. A +// loopback value that matches it is exempted from the warning: it means +// this is exactly what the service itself is already configured to +// answer as (e.g. an on-device install's `http://localhost:8000`, +// auto-set since #546), not a mistaken paste. Without this exemption, +// every on-device install's Suggested Plan fails validation by +// default and silently disables Apply/Pre-flight before the user does +// anything (#546 follow-up, reported via #621). +function validateURL(value, referenceOrigin) { const v = (value || "").trim(); if (!v) return {ok: true, error: ""}; @@ -2899,7 +2908,8 @@ function validateURL(value) { if (!u.hostname) return {ok: false, error: "hostname is empty"}; - if (u.hostname === "localhost" || u.hostname === "127.0.0.1") { + const isLoopback = u.hostname === "localhost" || u.hostname === "127.0.0.1"; + if (isLoopback && u.origin !== referenceOrigin) { return {ok: false, error: "loopback URL — speakers can only reach this if AfterTouch is installed on the speaker itself (on-device install). For the typical multi-device setup, use a LAN-reachable IP or hostname."}; } @@ -2918,12 +2928,23 @@ function validatePlanURLs() { ["bmxRegistryUrl", "plan-bmx-url"], ]; + const targetUrl = (document.getElementById("plan-target-url") || {}).value || ""; + let referenceOrigin = ""; + try { + referenceOrigin = new URL(targetUrl).origin; + } catch (e) { + // Target URL isn't a valid absolute URL yet (e.g. empty) — leave + // referenceOrigin empty, so a loopback field simply won't match + // it and falls back to today's warning, same as before this + // exemption existed. + } + const errors = []; for (const [name, elemId] of fields) { const el = document.getElementById(elemId); if (!el) continue; - const v = validateURL(el.value); + const v = validateURL(el.value, referenceOrigin); el.style.borderColor = v.ok ? "" : "#c62828"; if (!v.ok) errors.push(`${name}: ${v.error}`); }