perf(mtproto): fast tg link; logs via separate endpoint

/api/mtproto/status skips docker logs. /api/mtproto/logs loads tail async. Install response sets link card immediately. v1.2.23.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Андрей Бобырев
2026-05-15 04:14:35 +03:00
parent c72a7b9c02
commit 3c2866818a
5 changed files with 53 additions and 30 deletions

View File

@@ -91,6 +91,8 @@ cd /opt/amnezia-admin && chmod +x scripts/install.sh && sudo SKIP_DOWNLOAD=1 bas
| `UI_HIDE_MTPROTO` | _(не задано)_ | `1` или **`UI_HIDE_SECTIONS=...,mtproto`** — скрыть раздел установки MTProto в веб‑интерфейсе (по умолчанию виден даже FREE). |
| `FREE_PANEL_CONTAINER_FOR_PRO_INSTALL` и др. | см. `server.js` | Имена контейнеров для `docker rm` перед install (по умолчанию `amnezia-admin`, `amnezia-web-landing`, `amnezia-admin-pro`). |
Примечание для MTProto на панели: **`GET /api/mtproto/status`** отдаёт состояние и **`tg://` без синхронного `docker logs`**; хвост логов подгружается вторым запросом **`GET /api/mtproto/logs`**, чтобы блок со ссылкой открывался быстрее.
Чтобы включить форму установки из панели после «обычной» установки без **`INSTALL_FREE…`**, задайте в контейнере **`AMNEZIA_EDITION=community`** и **`ALLOW_COMMUNITY_GITHUB_ACTIVATION=1`** (или проще переустановите с **`INSTALL_FREE_COMMUNITY_ACTIVATION=1`**). URL приватного `install.sh` при необходимости переопределите через **`COMMUNITY_PRIVATE_INSTALL_SCRIPT_URL`**.
**Важно:** при одноразовом запуске с переменными в одной строке с `curl … | sudo bash` они **пропадают** — нужен **`export`** перед `curl` и **`sudo -E bash`**, см. блок **«Установка для клиента»** выше.

View File

@@ -1,6 +1,6 @@
{
"name": "amnezia-admin",
"version": "1.2.22",
"version": "1.2.23",
"private": true,
"description": "amnezia_web — базовая панель AmneziaWG (FREE: просмотр и удаление клиентов; полная версия — PRO).",
"license": "MIT",

View File

@@ -426,6 +426,28 @@ function applyEditionPayload(data) {
if (wgRawDetails) wgRawDetails.hidden = uiHidden.users || !editionState.showDebugWg;
}
/** @param {string} [tgLink] */
function setMtprotoLinkCard(tgLink) {
const t = typeof tgLink === "string" ? tgLink.trim() : "";
if (!mtprotoLinkCardEl) return;
if (!t) {
mtprotoLinkCardEl.classList.add("hidden");
delete mtprotoLinkCardEl.dataset.tgLink;
if (mtprotoLinkAnchorEl) {
mtprotoLinkAnchorEl.removeAttribute("href");
mtprotoLinkAnchorEl.textContent = "";
}
return;
}
mtprotoLinkCardEl.classList.remove("hidden");
mtprotoLinkCardEl.dataset.tgLink = t;
if (mtprotoLinkAnchorEl) {
mtprotoLinkAnchorEl.href = t;
mtprotoLinkAnchorEl.textContent = t;
}
if (mtprotoLinkCopyState) mtprotoLinkCopyState.textContent = "";
}
async function refreshMtprotoPanel() {
if (!mtprotoPanel || !mtprotoStatusLine || !mtprotoActionsEl) return;
if (uiHidden.mtproto) {
@@ -434,6 +456,7 @@ async function refreshMtprotoPanel() {
mtprotoCalloutEl.textContent = "";
mtprotoCalloutEl.classList.add("hidden");
}
setMtprotoLinkCard("");
return;
}
mtprotoPanel.hidden = false;
@@ -443,11 +466,17 @@ async function refreshMtprotoPanel() {
mtprotoBanner.textContent = "";
}
try {
if (mtprotoLogsTailEl) mtprotoLogsTailEl.textContent = "Загрузка логов…";
const snap = await api("/api/mtproto/status");
if (mtprotoLogsTailEl) {
mtprotoLogsTailEl.textContent =
typeof snap.logsTail === "string" ? snap.logsTail : "";
void (async () => {
try {
const lg = await api("/api/mtproto/logs");
const tail = typeof lg?.logsTail === "string" ? lg.logsTail : "";
if (mtprotoLogsTailEl) mtprotoLogsTailEl.textContent = tail;
} catch {
if (mtprotoLogsTailEl) mtprotoLogsTailEl.textContent = "(лог не загрузился — обновите раздел позже)";
}
})();
if (mtprotoCalloutEl) {
if (snap.exists) {
@@ -485,20 +514,7 @@ async function refreshMtprotoPanel() {
mtprotoActionsEl.appendChild(imgHint);
}
if (mtprotoLinkCardEl) {
if (snap.tgLink) {
mtprotoLinkCardEl.classList.remove("hidden");
mtprotoLinkCardEl.dataset.tgLink = snap.tgLink;
if (mtprotoLinkAnchorEl) {
mtprotoLinkAnchorEl.href = snap.tgLink;
mtprotoLinkAnchorEl.textContent = snap.tgLink;
}
if (mtprotoLinkCopyState) mtprotoLinkCopyState.textContent = "";
} else {
mtprotoLinkCardEl.classList.add("hidden");
delete mtprotoLinkCardEl.dataset.tgLink;
}
}
setMtprotoLinkCard(snap.tgLink);
const row = document.createElement("div");
row.className = "warp-actions";
@@ -516,6 +532,7 @@ async function refreshMtprotoPanel() {
}
if (/^[a-f0-9]{32}$/.test(sec)) body.secret = sec;
const j = await api("/api/mtproto/install", { method: "POST", body: JSON.stringify(body) });
if (typeof j.tgLink === "string" && j.tgLink.trim()) setMtprotoLinkCard(j.tgLink.trim());
const lines = [];
if (typeof j.secretHex === "string") lines.push(`Секрет (сохраните): ${j.secretHex}`);
if (typeof j.tgLink === "string" && j.tgLink) lines.push(`Ссылка: ${j.tgLink}`);
@@ -580,10 +597,7 @@ async function refreshMtprotoPanel() {
mtprotoCalloutEl.textContent = "";
mtprotoCalloutEl.classList.add("hidden");
}
if (mtprotoLinkCardEl) {
mtprotoLinkCardEl.classList.add("hidden");
delete mtprotoLinkCardEl.dataset.tgLink;
}
setMtprotoLinkCard("");
if (mtprotoLogsTailEl) mtprotoLogsTailEl.textContent = "";
const err = document.createElement("p");
err.className = "muted warp-muted err";

View File

@@ -7,7 +7,7 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;600;700&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/styles.css?v=1.2.22">
<link rel="stylesheet" href="/styles.css?v=1.2.23">
<script>
(function () {
try {
@@ -312,6 +312,6 @@
</div>
</dialog>
<script src="/app.js?v=1.2.22" type="module"></script>
<script src="/app.js?v=1.2.23" type="module"></script>
</body>
</html>

View File

@@ -2246,12 +2246,19 @@ app.get("/api/mtproto/status", requireAuth, (req, res) => {
return res.status(403).json({ error: MSG_UI_MTProto_OFF });
}
const snap = mtprotoSnapshot(hostFromRequest(req));
let logsTail = "";
if (snap.exists && snap.running) {
const l = dockerSpawnSync(["logs", "--tail", "100", MTPRO_CONTAINER], 12_000);
if (l.code === 0) logsTail = l.stdout.trim().slice(-4500);
res.json({ ...snap });
});
app.get("/api/mtproto/logs", requireAuth, (req, res) => {
if (effectiveUiHidden().mtproto) {
return res.status(403).json({ error: MSG_UI_MTProto_OFF });
}
res.json({ ...snap, logsTail });
const snap = mtprotoSnapshot(hostFromRequest(req));
if (!snap.exists || !snap.running) return res.json({ logsTail: "" });
const l = dockerSpawnSync(["logs", "--tail", "100", MTPRO_CONTAINER], 12_000);
let logsTail = "";
if (l.code === 0) logsTail = l.stdout.trim().slice(-4500);
res.json({ logsTail });
});
app.post("/api/mtproto/install", requireAuth, (req, res) => {