mirror of
https://github.com/andrey271192/amnezia_web-PRO.git
synced 2026-09-20 14:42:00 +00:00
feat(instances): show app-installed AmneziaWG, not only panel-created
The instances section listed only panel-managed instances, so an AmneziaWG container set up by the Amnezia desktop app (e.g. amnezia-awg2) did not appear as a card. /api/instances now iterates ALL effective profiles (env-discovered + managed): resolves each container, reads ListenPort and peer count, and detects the variant from the interface config (S3/S4 -> awg2, S1/S2 -> awg, wg0 -> legacy). Native (non-managed) instances are flagged managed:false. UI: native cards get an "приложение Amnezia" tag and hide the Удалить button (the panel must not tear down app-managed containers); they keep Подключения and Стоп/Старт. Stop/start now resolve the real container name so they work for native instances too. Verified live: native amnezia-awg2 shows as "AmneziaWG 2.0" port 37395 (managed:false) alongside a panel-created Legacy :5534.
This commit is contained in:
@@ -1454,12 +1454,18 @@ async function loadInstances() {
|
||||
? `<button class="btn small ghost" data-act="stop" data-id="${i.id}">■ Стоп</button>`
|
||||
: `<button class="btn small primary" data-act="start" data-id="${i.id}">▶ Старт</button>`;
|
||||
const badge = m.badge ? `<span class="inst-badge">${m.badge}</span>` : "";
|
||||
const srcTag = i.managed
|
||||
? ""
|
||||
: `<span class="inst-src" title="Контейнер развёрнут приложением Amnezia">приложение Amnezia</span>`;
|
||||
const delBtn = i.managed
|
||||
? `<button class="btn small warn" data-act="delete" data-id="${i.id}" data-label="${escapeHtmlSafe(m.title)}">Удалить</button>`
|
||||
: "";
|
||||
return `<div class="inst-card">
|
||||
<div class="inst-card-head">
|
||||
<div class="inst-ico">${m.icon}</div>
|
||||
${toggle}
|
||||
</div>
|
||||
<div class="inst-title">${escapeHtmlSafe(m.title)} ${badge}</div>
|
||||
<div class="inst-title">${escapeHtmlSafe(m.title)} ${badge} ${srcTag}</div>
|
||||
<div class="inst-desc muted">${escapeHtmlSafe(i.variantMeta?.desc || "")}</div>
|
||||
<div class="inst-status-row">${status}</div>
|
||||
<div class="inst-stats">
|
||||
@@ -1468,7 +1474,7 @@ async function loadInstances() {
|
||||
</div>
|
||||
<div class="inst-actions">
|
||||
<button class="btn small ghost" data-act="use" data-id="${i.id}">Подключения</button>
|
||||
<button class="btn small warn" data-act="delete" data-id="${i.id}" data-label="${escapeHtmlSafe(m.title)}">Удалить</button>
|
||||
${delBtn}
|
||||
</div>
|
||||
</div>`;
|
||||
}).join("");
|
||||
|
||||
@@ -1201,3 +1201,5 @@ tr:last-child td {
|
||||
.instance-form-row label { font-size: 0.75rem; color: var(--muted); }
|
||||
.instance-hint { font-size: 0.75rem; margin-top: 8px; }
|
||||
.instance-empty { padding: 12px; }
|
||||
|
||||
.inst-src { font-size: 0.6rem; color: var(--muted); border: 1px solid var(--line); border-radius: 999px; padding: 2px 7px; vertical-align: middle; }
|
||||
|
||||
63
server.js
63
server.js
@@ -2653,24 +2653,48 @@ function runInstanceScript(args) {
|
||||
});
|
||||
}
|
||||
|
||||
function detectVariantFromHead(head, iface) {
|
||||
if (iface === "wg0" || !/^S1\s*=/m.test(head)) return "legacy";
|
||||
if (/^S3\s*=/m.test(head) || /^S4\s*=/m.test(head)) return "awg2";
|
||||
return "awg";
|
||||
}
|
||||
|
||||
app.get("/api/instances", requireAuth, async (_req, res) => {
|
||||
const managed = loadManagedProfiles();
|
||||
const managedIds = new Set(loadManagedProfiles().map((m) => m.id));
|
||||
const profiles = getProfiles();
|
||||
const running = await listRunningContainerNames();
|
||||
const items = [];
|
||||
for (const m of managed) {
|
||||
for (const prof of profiles) {
|
||||
const isManaged = managedIds.has(prof.id) || prof.managed === true;
|
||||
let container = prof.container;
|
||||
let runningNow = false;
|
||||
let peers = null;
|
||||
if (running.includes(m.container)) {
|
||||
try {
|
||||
const out = (await execDocker(["exec", m.container, m.wgBinary, "show", m.iface, "peers"])).stdout || "";
|
||||
peers = out.split("\n").map((x) => x.trim()).filter(Boolean).length;
|
||||
} catch { peers = null; }
|
||||
let port = prof.port || null;
|
||||
let variant = prof.variant || "awg2";
|
||||
try {
|
||||
const rt = createRuntime(prof);
|
||||
container = await rt.resolveContainer();
|
||||
runningNow = running.includes(container);
|
||||
if (runningNow) {
|
||||
const { conf } = await rt.loadState();
|
||||
peers = conf.peers.length;
|
||||
const m = /^ListenPort\s*=\s*(\d+)/m.exec(conf.head);
|
||||
if (m) port = Number(m[1]);
|
||||
if (!prof.variant) variant = detectVariantFromHead(conf.head, prof.iface || "awg0");
|
||||
}
|
||||
} catch {
|
||||
runningNow = container ? running.includes(container) : false;
|
||||
}
|
||||
items.push({
|
||||
id: m.id, label: m.label, variant: m.variant, port: m.port,
|
||||
container: m.container,
|
||||
running: running.includes(m.container),
|
||||
id: prof.id,
|
||||
label: prof.label,
|
||||
variant,
|
||||
port,
|
||||
container,
|
||||
running: runningNow,
|
||||
peers,
|
||||
variantMeta: INSTANCE_VARIANTS[m.variant] || null,
|
||||
managed: isManaged,
|
||||
variantMeta: INSTANCE_VARIANTS[variant] || null,
|
||||
});
|
||||
}
|
||||
res.json({ instances: items, variants: INSTANCE_VARIANTS });
|
||||
@@ -2720,17 +2744,26 @@ app.post("/api/instances/delete", requireAuth, requireProTier, async (req, res)
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
async function resolveProfileContainer(id) {
|
||||
const prof = getProfiles().find((p) => p.id === id);
|
||||
if (!prof) return null;
|
||||
try { return await createRuntime(prof).resolveContainer(); }
|
||||
catch { return prof.container || null; }
|
||||
}
|
||||
|
||||
app.post("/api/instances/stop", requireAuth, requireProTier, async (req, res) => {
|
||||
const id = String(req.body?.id || "").trim();
|
||||
if (!loadManagedProfiles().some((p) => p.id === id)) return res.status(404).json({ error: "Инстанс не найден." });
|
||||
try { await execDocker(["stop", id]); res.json({ ok: true }); }
|
||||
const container = await resolveProfileContainer(id);
|
||||
if (!container) return res.status(404).json({ error: "Инстанс не найден." });
|
||||
try { await execDocker(["stop", container]); res.json({ ok: true }); }
|
||||
catch (e) { res.status(500).json({ error: String(e.message || e) }); }
|
||||
});
|
||||
|
||||
app.post("/api/instances/start", requireAuth, requireProTier, async (req, res) => {
|
||||
const id = String(req.body?.id || "").trim();
|
||||
if (!loadManagedProfiles().some((p) => p.id === id)) return res.status(404).json({ error: "Инстанс не найден." });
|
||||
try { await execDocker(["start", id]); res.json({ ok: true }); }
|
||||
const container = await resolveProfileContainer(id);
|
||||
if (!container) return res.status(404).json({ error: "Инстанс не найден." });
|
||||
try { await execDocker(["start", container]); res.json({ ok: true }); }
|
||||
catch (e) { res.status(500).json({ error: String(e.message || e) }); }
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user