diff --git a/docs/panel-guide.md b/docs/panel-guide.md index 854bb25..26a29d4 100644 --- a/docs/panel-guide.md +++ b/docs/panel-guide.md @@ -28,6 +28,10 @@ 3. Ручной JSON нужен только при нестандартных путях внутри AWG-контейнеров. 4. После правок обновите страницу с **жёстким сбросом кэша** (Ctrl+Shift+R). +## Инстансы задублировались + +После переустановки старый `AWG_PROFILES` и сохранённые managed-инстансы могли указывать на один и тот же контейнер/конфиг с разными `id`. Начиная с **v1.2.32** панель дедуплицирует профили по `container + confPath + iface + clientsPath`, а также чистит дубли при чтении/сохранении `instances.json`. Обновите панель из GitHub и сделайте жёсткое обновление страницы. + ## Лендинг не поднимается (порт 80 занят) При ошибке bind `:80` используйте при установке **`SKIP_LANDING=1`** или **`LANDING_PORT=8081`** — админка на `HOST_PORT` (например 8080) от этого не зависит. diff --git a/package-lock.json b/package-lock.json index 05f726c..4185e97 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "amnezia-admin", - "version": "1.2.31", + "version": "1.2.32", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "amnezia-admin", - "version": "1.2.31", + "version": "1.2.32", "license": "MIT", "dependencies": { "express": "^4.21.2" diff --git a/package.json b/package.json index b6f7893..892c91c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "amnezia-admin", - "version": "1.2.31", + "version": "1.2.32", "private": false, "description": "amnezia_web-PRO — веб-панель AmneziaWG: несколько инстансов, WARP, каскад/Endpoint, экспорт .conf (Docker)", "license": "MIT", diff --git a/server.js b/server.js index 472d112..c2f9319 100644 --- a/server.js +++ b/server.js @@ -164,7 +164,7 @@ function loadManagedProfiles() { const raw = fs.readFileSync(INSTANCES_FILE, "utf-8"); const arr = JSON.parse(raw); if (!Array.isArray(arr)) return []; - return arr.map((m) => ({ + const mapped = arr.map((m) => ({ id: String(m.id), label: String(m.label || m.id), container: String(m.container || m.id), @@ -181,6 +181,13 @@ function loadManagedProfiles() { variant: String(m.variant || "awg2"), port: Number(m.port) || null, })); + const seen = new Set(); + return mapped.filter((p) => { + const key = profileIdentityKey(p); + if (seen.has(key)) return false; + seen.add(key); + return true; + }); } catch { return []; } @@ -188,15 +195,40 @@ function loadManagedProfiles() { function saveManagedProfiles(list) { fs.mkdirSync(path.dirname(INSTANCES_FILE), { recursive: true }); - fs.writeFileSync(INSTANCES_FILE, JSON.stringify(list, null, 2)); + const seen = new Set(); + const deduped = []; + for (const p of list) { + const key = profileIdentityKey(p); + if (seen.has(key)) continue; + seen.add(key); + deduped.push(p); + } + fs.writeFileSync(INSTANCES_FILE, JSON.stringify(deduped, null, 2)); } -// Effective profile set = env profiles + managed instances (deduped by id). +function profileIdentityKey(p) { + return [ + String(p.container || "").trim(), + String(p.confPath || "").trim(), + String(p.iface || "").trim(), + String(p.clientsPath || "").trim(), + ].join("|"); +} + +// Effective profile set = env profiles + managed instances (deduped by id and target files). function getProfiles() { const managed = loadManagedProfiles(); - const seen = new Set(ENV_PROFILES.map((p) => p.id)); - const out = [...ENV_PROFILES]; - for (const m of managed) if (!seen.has(m.id)) { out.push(m); seen.add(m.id); } + const seenIds = new Set(); + const seenTargets = new Set(); + const out = []; + for (const p of [...ENV_PROFILES, ...managed]) { + const id = String(p.id || "").trim(); + const target = profileIdentityKey(p); + if ((id && seenIds.has(id)) || (target !== "|||" && seenTargets.has(target))) continue; + out.push(p); + if (id) seenIds.add(id); + if (target !== "|||") seenTargets.add(target); + } return out; } @@ -2873,6 +2905,7 @@ app.get("/api/instances", requireAuth, async (_req, res) => { const profiles = getProfiles(); const running = await listRunningContainerNames(); const items = []; + const seenRuntimeTargets = new Set(); for (const prof of profiles) { const isManaged = managedIds.has(prof.id) || prof.managed === true; let container = prof.container; @@ -2894,6 +2927,9 @@ app.get("/api/instances", requireAuth, async (_req, res) => { } catch { runningNow = container ? running.includes(container) : false; } + const runtimeTarget = [container, prof.confPath, prof.iface, prof.clientsPath].join("|"); + if (seenRuntimeTargets.has(runtimeTarget)) continue; + seenRuntimeTargets.add(runtimeTarget); items.push({ id: prof.id, label: prof.label,