From 10badd4d5200398ab215b6578730c3a6c065cef6 Mon Sep 17 00:00:00 2001 From: Arseniy Romenskiy Date: Sat, 27 Dec 2025 05:11:54 +0300 Subject: [PATCH] fix --- frontend/js/app.js | 131 +++++++++++++++------------------------------ 1 file changed, 43 insertions(+), 88 deletions(-) diff --git a/frontend/js/app.js b/frontend/js/app.js index b5040a9..274c0d6 100755 --- a/frontend/js/app.js +++ b/frontend/js/app.js @@ -1,7 +1,7 @@ import { requireAuthOrRedirect, logout } from "./auth.js"; import { apiFetch } from "./api.js"; +import { loadSession } from "./storage.js"; import { CONFIG } from "./config.js"; -import { loadSession, saveSession, clearSession } from "./storage.js"; if (!requireAuthOrRedirect()) { // редирект уже сделан @@ -60,38 +60,35 @@ function renderTable(filter = "") { const filtered = !q ? rows : rows.filter(r => r.searchBlob.includes(q)); tbody.innerHTML = ""; - for (const r of filtered) { const tr = document.createElement("tr"); + const commentPreview = r.comments.length > 180 ? (r.comments.slice(0, 180) + "…") : r.comments; + tr.innerHTML = ` - ${escapeHtml(r.id)} - ${escapeHtml(r.name)} + ${r.id} + ${escapeHtml(r.name)} ${escapeHtml(r.status)} - ${escapeHtml(r.ipLocal)} - ${escapeHtml(r.ipServer)} + ${escapeHtml(r.ownerName)} (#${r.ownerId}) + ${escapeHtml(r.targetName)} (#${r.targetId}) +
${escapeHtml(commentPreview)}
+ ${escapeHtml(r.inventoryId)} `; - const tdBtn = tr.lastElementChild; - const btn = document.createElement("button"); - btn.className = "btn btn-ghost btn-sm"; - btn.textContent = "Скачать"; - btn.addEventListener("click", async () => { - clearError(); - try { - await triggerDownloadById(r.id); - } catch (err) { - if (err?.status === CONFIG.AUTH_ERROR_STATUS) { - await logout(); - location.replace("./login.html"); - return; - } - showError("Не удалось скачать конфиг."); - setStatus(""); - } - }); - tdBtn.appendChild(btn); + const actionTd = tr.lastElementChild; + + if (isWireguardLike(r.comments)) { + const btn = document.createElement("button"); + btn.className = "btn btn-ghost btn-sm"; + btn.textContent = "Копировать WG"; + btn.addEventListener("click", async () => { + await copyToClipboard(r.comments); + setStatus("Скопировано."); + setTimeout(() => setStatus(""), 1200); + }); + actionTd.appendChild(btn); + } tbody.appendChild(tr); } @@ -111,21 +108,34 @@ async function loadData() { setStatus("Загрузка…"); try { + // /ls/ возвращает {"ls": bd_module.ls_item(uid)} :contentReference[oaicite:7]{index=7} + // Формат строк из БД: [item_id,item_name,status,user_id_1,user_name1,user_id_2,user_name2,icon,comments,inventory_id] :contentReference[oaicite:8]{index=8} const data = await apiFetch(CONFIG.ENDPOINTS.list, { method: "GET", auth: true }); - const list = Array.isArray(data?.ls) ? data.ls : Array.isArray(data) ? data : []; - rows = list - .map(mapLsItem) - .filter(Boolean) - .map(r => ({ - ...r, - searchBlob: [r.id, r.name, r.status, r.ipLocal, r.ipServer].join(" ").toLowerCase() - })); + const list = Array.isArray(data?.ls) ? data.ls : []; + rows = list.map((a) => { + const id = normalizeCell(a?.[0]); + const name = normalizeCell(a?.[1]); + const status = normalizeCell(a?.[2]); + const ownerId = normalizeCell(a?.[3]); + const ownerName = normalizeCell(a?.[4]); + const targetId = normalizeCell(a?.[5]); + const targetName = normalizeCell(a?.[6]); + const comments = normalizeCell(a?.[8]); + const inventoryId = normalizeCell(a?.[9]); + + const searchBlob = [ + id, name, status, ownerId, ownerName, targetId, targetName, comments, inventoryId + ].join(" ").toLowerCase(); + + return { id, name, status, ownerId, ownerName, targetId, targetName, comments, inventoryId, searchBlob }; + }); renderTable(searchEl.value); setStatus(`Готово. Строк: ${rows.length}`); } catch (err) { if (err?.status === CONFIG.AUTH_ERROR_STATUS) { + // если даже renew не помог — сессия умерла await logout(); location.replace("./login.html"); return; @@ -150,58 +160,3 @@ async function loadData() { loadData(); })(); - - -function filenameFromDisposition(cd) { - if (!cd) return null; - - // filename*=UTF-8''... - let m = cd.match(/filename\*\s*=\s*UTF-8''([^;]+)/i); - if (m?.[1]) return decodeURIComponent(m[1].replace(/"/g, "").trim()); - - // filename="..." - m = cd.match(/filename\s*=\s*"?([^";]+)"?/i); - if (m?.[1]) return m[1].trim(); - - return null; -} - -export async function apiFetchBlob(path, { method = "GET", headers = {}, auth = true } = {}) { - const s = loadSession(); - const reqHeaders = { ...headers }; - - if (auth) { - if (!s?.short_token) throw new Error("NO_AUTH"); - reqHeaders["short_token"] = String(s.short_token); - reqHeaders["Authorization"] = `Bearer ${t}`; - } - - const doRequest = async () => { - return await fetch(path.startsWith("http") ? path : (CONFIG.API_BASE_URL ? CONFIG.API_BASE_URL.replace(/\/+$/, "") + path : path), { - method, - headers: reqHeaders - }); - }; - - let res = await doRequest(); - - if (auth && res.status === CONFIG.AUTH_ERROR_STATUS) { - // renew + retry один раз - await (await import("./api.js")).renewTokens?.(); // на случай циклического импорта (если будет) — безопасно - const s2 = loadSession(); - reqHeaders["short_token"] = String(s2.short_token); - res = await doRequest(); - } - - if (!res.ok) { - const text = await res.text().catch(() => ""); - const err = new Error("API_BLOB_ERROR"); - err.status = res.status; - err.payload = text; - throw err; - } - - const blob = await res.blob(); - const filename = filenameFromDisposition(res.headers.get("content-disposition")); - return { blob, filename }; -}