From b8a4b3b511427867225a159fbfbfe68b55bae3c6 Mon Sep 17 00:00:00 2001 From: valer23 Date: Fri, 17 Apr 2026 10:28:38 +0200 Subject: [PATCH 1/4] feat: sort server list by country name instead of city name Sort the server dropdown by country first, then by city within the same country. This makes it easier to find servers in a specific country when the list is long. Applies to both modern and classic UIs. --- frontend/javascript/index.js | 11 +++++++++++ index-classic.html | 10 ++++++++++ 2 files changed, 21 insertions(+) diff --git a/frontend/javascript/index.js b/frontend/javascript/index.js index ba4eb0ead..146ae3d35 100644 --- a/frontend/javascript/index.js +++ b/frontend/javascript/index.js @@ -230,6 +230,17 @@ function populateDropdown(servers) { }); } + // Sort servers by country, then by city within the same country + servers.sort((a, b) => { + const commaA = a.name.lastIndexOf(","); + const commaB = b.name.lastIndexOf(","); + const countryA = commaA >= 0 ? a.name.substring(commaA + 1).trim() : a.name; + const countryB = commaB >= 0 ? b.name.substring(commaB + 1).trim() : b.name; + const cityA = commaA >= 0 ? a.name.substring(0, commaA).trim() : ""; + const cityB = commaB >= 0 ? b.name.substring(0, commaB).trim() : ""; + return countryA.localeCompare(countryB) || cityA.localeCompare(cityB); + }); + // Populate the list to choose from servers.forEach((server) => { const item = document.createElement("li"); diff --git a/index-classic.html b/index-classic.html index 19bf2e20b..7b5592684 100644 --- a/index-classic.html +++ b/index-classic.html @@ -50,6 +50,16 @@ s.selectServer(function (server) { if (server != null) { //at least 1 server is available I("loading").className = "hidden"; //hide loading message + //sort servers by country, then by city + SPEEDTEST_SERVERS.sort(function (a, b) { + var commaA = a.name.lastIndexOf(","); + var commaB = b.name.lastIndexOf(","); + var countryA = commaA >= 0 ? a.name.substring(commaA + 1).trim() : a.name; + var countryB = commaB >= 0 ? b.name.substring(commaB + 1).trim() : b.name; + var cityA = commaA >= 0 ? a.name.substring(0, commaA).trim() : ""; + var cityB = commaB >= 0 ? b.name.substring(0, commaB).trim() : ""; + return countryA.localeCompare(countryB) || cityA.localeCompare(cityB); + }); //populate server list for manual selection for (var i = 0; i < SPEEDTEST_SERVERS.length; i++) { if (SPEEDTEST_SERVERS[i].pingT == -1) continue; From 16abce99535e151f2ba829d2e294f8963c6d4a7e Mon Sep 17 00:00:00 2001 From: valer23 Date: Fri, 17 Apr 2026 10:28:38 +0200 Subject: [PATCH 2/4] fix: avoid mutating original server array and add null guard Address code review findings: - Sort a shallow copy instead of mutating the caller's array - Add null guard on server.name to handle malformed entries - Use original SPEEDTEST_SERVERS index for classic UI option values --- frontend/javascript/index.js | 18 ++++++++++-------- index-classic.html | 26 ++++++++++++++------------ 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/frontend/javascript/index.js b/frontend/javascript/index.js index 146ae3d35..e7f4aa450 100644 --- a/frontend/javascript/index.js +++ b/frontend/javascript/index.js @@ -231,18 +231,20 @@ function populateDropdown(servers) { } // Sort servers by country, then by city within the same country - servers.sort((a, b) => { - const commaA = a.name.lastIndexOf(","); - const commaB = b.name.lastIndexOf(","); - const countryA = commaA >= 0 ? a.name.substring(commaA + 1).trim() : a.name; - const countryB = commaB >= 0 ? b.name.substring(commaB + 1).trim() : b.name; - const cityA = commaA >= 0 ? a.name.substring(0, commaA).trim() : ""; - const cityB = commaB >= 0 ? b.name.substring(0, commaB).trim() : ""; + const sorted = [...servers].sort((a, b) => { + const nameA = a.name || ""; + const nameB = b.name || ""; + const commaA = nameA.lastIndexOf(","); + const commaB = nameB.lastIndexOf(","); + const countryA = commaA >= 0 ? nameA.substring(commaA + 1).trim() : nameA; + const countryB = commaB >= 0 ? nameB.substring(commaB + 1).trim() : nameB; + const cityA = commaA >= 0 ? nameA.substring(0, commaA).trim() : ""; + const cityB = commaB >= 0 ? nameB.substring(0, commaB).trim() : ""; return countryA.localeCompare(countryB) || cityA.localeCompare(cityB); }); // Populate the list to choose from - servers.forEach((server) => { + sorted.forEach((server) => { const item = document.createElement("li"); const link = document.createElement("a"); link.href = "#"; diff --git a/index-classic.html b/index-classic.html index 7b5592684..9a8c0fdd5 100644 --- a/index-classic.html +++ b/index-classic.html @@ -51,22 +51,24 @@ if (server != null) { //at least 1 server is available I("loading").className = "hidden"; //hide loading message //sort servers by country, then by city - SPEEDTEST_SERVERS.sort(function (a, b) { - var commaA = a.name.lastIndexOf(","); - var commaB = b.name.lastIndexOf(","); - var countryA = commaA >= 0 ? a.name.substring(commaA + 1).trim() : a.name; - var countryB = commaB >= 0 ? b.name.substring(commaB + 1).trim() : b.name; - var cityA = commaA >= 0 ? a.name.substring(0, commaA).trim() : ""; - var cityB = commaB >= 0 ? b.name.substring(0, commaB).trim() : ""; + var sortedServers = SPEEDTEST_SERVERS.slice().sort(function (a, b) { + var nameA = a.name || ""; + var nameB = b.name || ""; + var commaA = nameA.lastIndexOf(","); + var commaB = nameB.lastIndexOf(","); + var countryA = commaA >= 0 ? nameA.substring(commaA + 1).trim() : nameA; + var countryB = commaB >= 0 ? nameB.substring(commaB + 1).trim() : nameB; + var cityA = commaA >= 0 ? nameA.substring(0, commaA).trim() : ""; + var cityB = commaB >= 0 ? nameB.substring(0, commaB).trim() : ""; return countryA.localeCompare(countryB) || cityA.localeCompare(cityB); }); //populate server list for manual selection - for (var i = 0; i < SPEEDTEST_SERVERS.length; i++) { - if (SPEEDTEST_SERVERS[i].pingT == -1) continue; + for (var i = 0; i < sortedServers.length; i++) { + if (sortedServers[i].pingT == -1) continue; var option = document.createElement("option"); - option.value = i; - option.textContent = SPEEDTEST_SERVERS[i].name; - if (SPEEDTEST_SERVERS[i] === server) option.selected = true; + option.value = SPEEDTEST_SERVERS.indexOf(sortedServers[i]); + option.textContent = sortedServers[i].name; + if (sortedServers[i] === server) option.selected = true; I("server").appendChild(option); } //show test UI From d8a976395299fa52cc75c64645cfb50968dd6fa5 Mon Sep 17 00:00:00 2001 From: valer23 Date: Fri, 17 Apr 2026 10:39:33 +0200 Subject: [PATCH 3/4] fix: handle parenthetical qualifiers and multi-comma server names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Parse server names more robustly for sorting: - "City, Country, Provider" → use second part as country - "City, Country (1) (Hetzner)" → strip parentheticals from country - "Frankfurt, Germany (FRA01)" → country is "Germany" not "Germany (FRA01)" --- frontend/javascript/index.js | 33 +++++++++++++++++++++++---------- index-classic.html | 30 +++++++++++++++++++++--------- 2 files changed, 44 insertions(+), 19 deletions(-) diff --git a/frontend/javascript/index.js b/frontend/javascript/index.js index e7f4aa450..9935bd4c3 100644 --- a/frontend/javascript/index.js +++ b/frontend/javascript/index.js @@ -230,17 +230,30 @@ function populateDropdown(servers) { }); } - // Sort servers by country, then by city within the same country + // Sort servers by country, then by city within the same country. + // Name formats: "City, Country", "City, Country (qualifier)", "City, Country, Provider", "Country" + const parseServerName = (name) => { + const parts = (name || "").split(",").map((s) => s.trim()); + let country, city; + if (parts.length >= 3) { + // "City, Country, Provider" — use second part as country + country = parts[1]; + city = parts[0]; + } else if (parts.length === 2) { + country = parts[1]; + city = parts[0]; + } else { + country = parts[0]; + city = ""; + } + // Strip parenthetical qualifiers for sorting: "Germany (1) (Hetzner)" → "Germany" + country = country.replace(/\s*\([^)]*\)\s*/g, "").trim(); + return { country, city }; + }; const sorted = [...servers].sort((a, b) => { - const nameA = a.name || ""; - const nameB = b.name || ""; - const commaA = nameA.lastIndexOf(","); - const commaB = nameB.lastIndexOf(","); - const countryA = commaA >= 0 ? nameA.substring(commaA + 1).trim() : nameA; - const countryB = commaB >= 0 ? nameB.substring(commaB + 1).trim() : nameB; - const cityA = commaA >= 0 ? nameA.substring(0, commaA).trim() : ""; - const cityB = commaB >= 0 ? nameB.substring(0, commaB).trim() : ""; - return countryA.localeCompare(countryB) || cityA.localeCompare(cityB); + const pa = parseServerName(a.name); + const pb = parseServerName(b.name); + return pa.country.localeCompare(pb.country) || pa.city.localeCompare(pb.city); }); // Populate the list to choose from diff --git a/index-classic.html b/index-classic.html index 9a8c0fdd5..036191cfe 100644 --- a/index-classic.html +++ b/index-classic.html @@ -51,16 +51,28 @@ if (server != null) { //at least 1 server is available I("loading").className = "hidden"; //hide loading message //sort servers by country, then by city + //name formats: "City, Country", "City, Country (qualifier)", "City, Country, Provider", "Country" + function parseServerName(name) { + var parts = (name || "").split(","); + for (var p = 0; p < parts.length; p++) parts[p] = parts[p].trim(); + var country, city; + if (parts.length >= 3) { + country = parts[1]; + city = parts[0]; + } else if (parts.length === 2) { + country = parts[1]; + city = parts[0]; + } else { + country = parts[0]; + city = ""; + } + country = country.replace(/\s*\([^)]*\)\s*/g, "").trim(); + return { country: country, city: city }; + } var sortedServers = SPEEDTEST_SERVERS.slice().sort(function (a, b) { - var nameA = a.name || ""; - var nameB = b.name || ""; - var commaA = nameA.lastIndexOf(","); - var commaB = nameB.lastIndexOf(","); - var countryA = commaA >= 0 ? nameA.substring(commaA + 1).trim() : nameA; - var countryB = commaB >= 0 ? nameB.substring(commaB + 1).trim() : nameB; - var cityA = commaA >= 0 ? nameA.substring(0, commaA).trim() : ""; - var cityB = commaB >= 0 ? nameB.substring(0, commaB).trim() : ""; - return countryA.localeCompare(countryB) || cityA.localeCompare(cityB); + var pa = parseServerName(a.name); + var pb = parseServerName(b.name); + return pa.country.localeCompare(pb.country) || pa.city.localeCompare(pb.city); }); //populate server list for manual selection for (var i = 0; i < sortedServers.length; i++) { From 070898e7bdb07f17b686d336b415e30d46057681 Mon Sep 17 00:00:00 2001 From: valer23 Date: Fri, 17 Apr 2026 10:42:37 +0200 Subject: [PATCH 4/4] =?UTF-8?q?perf:=20avoid=20O(n=C2=B2)=20indexOf=20look?= =?UTF-8?q?up=20in=20classic=20UI=20server=20loop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Build an array of {idx, server} pairs before sorting so the original SPEEDTEST_SERVERS index is carried through, eliminating the per-option indexOf call. --- index-classic.html | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/index-classic.html b/index-classic.html index 036191cfe..b8f2d5cef 100644 --- a/index-classic.html +++ b/index-classic.html @@ -69,18 +69,22 @@ country = country.replace(/\s*\([^)]*\)\s*/g, "").trim(); return { country: country, city: city }; } - var sortedServers = SPEEDTEST_SERVERS.slice().sort(function (a, b) { - var pa = parseServerName(a.name); - var pb = parseServerName(b.name); + var indexed = []; + for (var j = 0; j < SPEEDTEST_SERVERS.length; j++) { + indexed.push({ idx: j, server: SPEEDTEST_SERVERS[j] }); + } + indexed.sort(function (a, b) { + var pa = parseServerName(a.server.name); + var pb = parseServerName(b.server.name); return pa.country.localeCompare(pb.country) || pa.city.localeCompare(pb.city); }); //populate server list for manual selection - for (var i = 0; i < sortedServers.length; i++) { - if (sortedServers[i].pingT == -1) continue; + for (var i = 0; i < indexed.length; i++) { + if (indexed[i].server.pingT == -1) continue; var option = document.createElement("option"); - option.value = SPEEDTEST_SERVERS.indexOf(sortedServers[i]); - option.textContent = sortedServers[i].name; - if (sortedServers[i] === server) option.selected = true; + option.value = indexed[i].idx; + option.textContent = indexed[i].server.name; + if (indexed[i].server === server) option.selected = true; I("server").appendChild(option); } //show test UI