Ver Fonte

feat(xray/nord): searchable server list + colored load tag, surface API errors

Frontend (NordModal.vue):
- Server selector gets show-search with the option label set to
  `${cityName} ${name} ${hostname}` so admins can find a specific
  server inside a 100+ entry country list by typing.
- Each option renders the load as a colored a-tag (green <30%,
  orange 30-70%, red >70%) instead of plain text — quicker visual
  scan when sorting through servers in the dropdown.

Backend (nord.go):
- GetCountries / GetServers now check resp.StatusCode and return
  "NordVPN API error: <status>" on non-200, matching the pattern
  GetCredentials already used. Previously a 4xx/5xx body was
  returned as a "success" string and the frontend silently failed
  to parse it, surfacing only as an empty "No servers found".
- GetCredentials drops its own ad-hoc 10s http.Client and reuses
  the shared nordHTTPClient (15s) — one client, one timeout.
MHSanaei há 19 horas atrás
pai
commit
5f3e9ed0ea
2 ficheiros alterados com 38 adições e 5 exclusões
  1. 31 3
      frontend/src/pages/xray/NordModal.vue
  2. 7 2
      web/service/nord.go

+ 31 - 3
frontend/src/pages/xray/NordModal.vue

@@ -222,6 +222,12 @@ function resetOutbound() {
 }
 
 function close() { emit('update:open', false); }
+
+function loadColor(load) {
+  if (load < 30) return 'green';
+  if (load < 70) return 'orange';
+  return 'red';
+}
 </script>
 
 <template>
@@ -299,9 +305,13 @@ function close() { emit('update:open', false); }
         </a-form-item>
 
         <a-form-item v-if="filteredServers.length > 0" label="Server">
-          <a-select v-model:value="serverId">
-            <a-select-option v-for="s in filteredServers" :key="s.id" :value="s.id">
-              {{ s.cityName }} - {{ s.name }} (load: {{ s.load }}%)
+          <a-select v-model:value="serverId" show-search option-filter-prop="label">
+            <a-select-option v-for="s in filteredServers" :key="s.id" :value="s.id"
+              :label="`${s.cityName} ${s.name} ${s.hostname}`">
+              <span class="server-row">
+                <span class="server-name">{{ s.cityName }} - {{ s.name }}</span>
+                <a-tag :color="loadColor(s.load)" class="server-load-tag">{{ s.load }}%</a-tag>
+              </span>
             </a-select-option>
           </a-select>
         </a-form-item>
@@ -376,4 +386,22 @@ function close() { emit('update:open', false); }
 .ml-8 {
   margin-left: 8px;
 }
+
+.server-row {
+  display: inline-flex;
+  align-items: center;
+  gap: 8px;
+  width: 100%;
+}
+
+.server-name {
+  flex: 1;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.server-load-tag {
+  margin-right: 0;
+  flex-shrink: 0;
+}
 </style>

+ 7 - 2
web/service/nord.go

@@ -25,6 +25,9 @@ func (s *NordService) GetCountries() (string, error) {
 		return "", err
 	}
 	defer resp.Body.Close()
+	if resp.StatusCode != http.StatusOK {
+		return "", common.NewErrorf("NordVPN API error: %s", resp.Status)
+	}
 	body, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseSize))
 	if err != nil {
 		return "", err
@@ -45,6 +48,9 @@ func (s *NordService) GetServers(countryId string) (string, error) {
 		return "", err
 	}
 	defer resp.Body.Close()
+	if resp.StatusCode != http.StatusOK {
+		return "", common.NewErrorf("NordVPN API error: %s", resp.Status)
+	}
 	body, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseSize))
 	if err != nil {
 		return "", err
@@ -97,8 +103,7 @@ func (s *NordService) GetCredentials(token string) (string, error) {
 	}
 	req.SetBasicAuth("token", token)
 
-	client := &http.Client{Timeout: 10 * time.Second}
-	resp, err := client.Do(req)
+	resp, err := nordHTTPClient.Do(req)
 	if err != nil {
 		return "", err
 	}