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 <noreply@anthropic.com>
This commit is contained in:
Tobias Gesellchen
2026-08-16 20:41:40 +02:00
co-authored by Claude Sonnet 5
parent 3e730c983f
commit eec57cbc10
+24 -3
View File
@@ -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}`);
}