mirror of
https://github.com/andrey271192/amnezia_web-PRO.git
synced 2026-09-21 14:51:59 +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 ghost" data-act="stop" data-id="${i.id}">■ Стоп</button>`
|
||||||
: `<button class="btn small primary" data-act="start" 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 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">
|
return `<div class="inst-card">
|
||||||
<div class="inst-card-head">
|
<div class="inst-card-head">
|
||||||
<div class="inst-ico">${m.icon}</div>
|
<div class="inst-ico">${m.icon}</div>
|
||||||
${toggle}
|
${toggle}
|
||||||
</div>
|
</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-desc muted">${escapeHtmlSafe(i.variantMeta?.desc || "")}</div>
|
||||||
<div class="inst-status-row">${status}</div>
|
<div class="inst-status-row">${status}</div>
|
||||||
<div class="inst-stats">
|
<div class="inst-stats">
|
||||||
@@ -1468,7 +1474,7 @@ async function loadInstances() {
|
|||||||
</div>
|
</div>
|
||||||
<div class="inst-actions">
|
<div class="inst-actions">
|
||||||
<button class="btn small ghost" data-act="use" data-id="${i.id}">Подключения</button>
|
<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>
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join("");
|
}).join("");
|
||||||
|
|||||||
@@ -1201,3 +1201,5 @@ tr:last-child td {
|
|||||||
.instance-form-row label { font-size: 0.75rem; color: var(--muted); }
|
.instance-form-row label { font-size: 0.75rem; color: var(--muted); }
|
||||||
.instance-hint { font-size: 0.75rem; margin-top: 8px; }
|
.instance-hint { font-size: 0.75rem; margin-top: 8px; }
|
||||||
.instance-empty { padding: 12px; }
|
.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) => {
|
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 running = await listRunningContainerNames();
|
||||||
const items = [];
|
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;
|
let peers = null;
|
||||||
if (running.includes(m.container)) {
|
let port = prof.port || null;
|
||||||
try {
|
let variant = prof.variant || "awg2";
|
||||||
const out = (await execDocker(["exec", m.container, m.wgBinary, "show", m.iface, "peers"])).stdout || "";
|
try {
|
||||||
peers = out.split("\n").map((x) => x.trim()).filter(Boolean).length;
|
const rt = createRuntime(prof);
|
||||||
} catch { peers = null; }
|
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({
|
items.push({
|
||||||
id: m.id, label: m.label, variant: m.variant, port: m.port,
|
id: prof.id,
|
||||||
container: m.container,
|
label: prof.label,
|
||||||
running: running.includes(m.container),
|
variant,
|
||||||
|
port,
|
||||||
|
container,
|
||||||
|
running: runningNow,
|
||||||
peers,
|
peers,
|
||||||
variantMeta: INSTANCE_VARIANTS[m.variant] || null,
|
managed: isManaged,
|
||||||
|
variantMeta: INSTANCE_VARIANTS[variant] || null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
res.json({ instances: items, variants: INSTANCE_VARIANTS });
|
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 });
|
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) => {
|
app.post("/api/instances/stop", requireAuth, requireProTier, async (req, res) => {
|
||||||
const id = String(req.body?.id || "").trim();
|
const id = String(req.body?.id || "").trim();
|
||||||
if (!loadManagedProfiles().some((p) => p.id === id)) return res.status(404).json({ error: "Инстанс не найден." });
|
const container = await resolveProfileContainer(id);
|
||||||
try { await execDocker(["stop", id]); res.json({ ok: true }); }
|
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) }); }
|
catch (e) { res.status(500).json({ error: String(e.message || e) }); }
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post("/api/instances/start", requireAuth, requireProTier, async (req, res) => {
|
app.post("/api/instances/start", requireAuth, requireProTier, async (req, res) => {
|
||||||
const id = String(req.body?.id || "").trim();
|
const id = String(req.body?.id || "").trim();
|
||||||
if (!loadManagedProfiles().some((p) => p.id === id)) return res.status(404).json({ error: "Инстанс не найден." });
|
const container = await resolveProfileContainer(id);
|
||||||
try { await execDocker(["start", id]); res.json({ ok: true }); }
|
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) }); }
|
catch (e) { res.status(500).json({ error: String(e.message || e) }); }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user