const $ = (id) => document.getElementById(id); const state = { bases: [], requests: [] }; function headers(json = false) { const value = {}; if (json) value["Content-Type"] = "application/json"; return value; } async function request(path, options = {}) { const response = await fetch(path, { ...options, headers: { ...headers(Boolean(options.body)), ...(options.headers || {}) } }); let data = {}; try { data = await response.json(); } catch (_) { /* empty response */ } if (!response.ok) throw new Error(data.message || data.error || `HTTP ${response.status}`); return data; } function notice(message = "", error = false) { $("notice").hidden = !message; $("notice").textContent = message; $("notice").className = `notice${error ? " error" : ""}`; } function render() { $("bases").replaceChildren(...state.bases.map((base) => { const row = document.createElement("tr"); const cells = [base.base_id, base.server, base.database, base.user]; cells.forEach((value) => { const td = document.createElement("td"); td.textContent = value; row.append(td); }); const password = document.createElement("td"); password.innerHTML = `${base.password_env ? "ENV · " + escapeHtml(base.password_env) : base.has_password ? "Сохранён" : "Не задан"}`; row.append(password); const actions = document.createElement("td"); actions.className = "actions"; const edit = document.createElement("button"); edit.className = "text-button"; edit.textContent = "Изменить"; edit.onclick = () => openEditor(base); const remove = document.createElement("button"); remove.className = "text-button danger"; remove.textContent = "Удалить"; remove.onclick = () => deleteBase(base); actions.append(edit, remove); row.append(actions); return row; })); $("empty").hidden = state.bases.length > 0; $("count").textContent = `${state.bases.length} ${state.bases.length === 1 ? "подключение" : "подключений"}`; } function renderRequests() { $("requests").replaceChildren(...state.requests.map((item) => { const row = document.createElement("tr"); const values = [item.request_id, item.base_id, item.status, (item.objects || []).join(", "), item.created_at ? new Date(item.created_at * 1000).toLocaleString("ru-RU") : "—"]; values.forEach((value, index) => { const td = document.createElement("td"); td.textContent = value || "—"; if (index === 2) td.className = `request-status ${item.status || ""}`; if (index === 3) td.className = "request-objects"; row.append(td); }); return row; })); $("requestsEmpty").hidden = state.requests.length > 0; $("requestCount").textContent = `${state.requests.length} заявок`; } function escapeHtml(value) { const div = document.createElement("div"); div.textContent = value; return div.innerHTML; } async function load() { try { notice(); const [data, repository] = await Promise.all([request("/admin/api/bases"), request("/admin/api/repository/requests")]); state.bases = data.bases || []; state.requests = repository.requests || []; $("statusDot").classList.add("online"); $("connectionText").textContent = "Адаптер подключён"; render(); renderRequests(); } catch (error) { $("statusDot").classList.remove("online"); $("connectionText").textContent = "Требуется подключение"; notice(error.message === "bearer_token_required" ? "Введите Bearer-токен адаптера." : error.message, true); } } function openEditor(base = null) { $("editorTitle").textContent = base ? "Редактировать базу" : "Новая база"; $("originalId").value = base?.base_id || ""; $("baseId").value = base?.base_id || ""; $("server").value = base?.server || ""; $("database").value = base?.database || ""; $("user").value = base?.user || ""; $("password").value = ""; $("passwordEnv").value = base?.password_env || ""; $("repositoryEnabled").checked = Boolean(base?.repository); $("repositoryBackend").value = base?.repository?.backend || "direct"; $("repositoryLayer").value = base?.repository?.layer || "base"; $("repositoryLockMode").value = base?.repository?.lock_mode || "automatic"; $("repositoryBridgeId").value = base?.repository?.bridge_id || ""; $("repositoryUser").value = base?.repository?.repository_user || ""; $("passwordHint").textContent = base?.has_password ? "Пароль уже задан. Оставьте пустым, чтобы сохранить текущий." : "Задайте пароль или переменную окружения."; $("editor").showModal(); setTimeout(() => $("baseId").focus(), 30); } async function save(event) { event.preventDefault(); if (!$("baseForm").reportValidity()) return; const original = $("originalId").value; const payload = { base_id: $("baseId").value.trim(), server: $("server").value.trim(), database: $("database").value.trim(), user: $("user").value.trim(), password: $("password").value, password_env: $("passwordEnv").value.trim(), repository: $("repositoryEnabled").checked ? { backend: $("repositoryBackend").value, layer: $("repositoryLayer").value, lock_mode: $("repositoryLockMode").value, bridge_id: $("repositoryBridgeId").value.trim(), repository_user: $("repositoryUser").value.trim() } : { enabled: false } }; $("saveButton").disabled = true; try { await request(original ? `/admin/api/bases/${encodeURIComponent(original)}` : "/admin/api/bases", { method: original ? "PUT" : "POST", body: JSON.stringify(payload) }); $("editor").close(); await load(); notice(original ? "Подключение обновлено." : "Подключение добавлено."); } catch (error) { notice(error.message, true); } finally { $("saveButton").disabled = false; } } async function deleteBase(base) { if (!confirm(`Удалить подключение «${base.base_id}»?`)) return; try { await request(`/admin/api/bases/${encodeURIComponent(base.base_id)}`, { method: "DELETE" }); await load(); notice("Подключение удалено."); } catch (error) { notice(error.message, true); } } $("refreshButton").onclick = load; $("addButton").onclick = () => openEditor(); $("baseForm").onsubmit = save; load();