121 lines
5.1 KiB
JavaScript
121 lines
5.1 KiB
JavaScript
/**
|
||
* K-hub «Мультисервер»: manual conflict resolution (orchestrator vs farm hub).
|
||
*/
|
||
(function (global) {
|
||
const API = "/api/v1/sync/conflicts";
|
||
|
||
function escapeHtml(value) {
|
||
return String(value)
|
||
.replace(/&/g, "&")
|
||
.replace(/</g, "<")
|
||
.replace(/>/g, ">")
|
||
.replace(/"/g, """);
|
||
}
|
||
|
||
function renderList(root, items, enterpriseId) {
|
||
if (!items.length) {
|
||
root.innerHTML = '<p class="text-muted small">Нет конфликтов синхронизации.</p>';
|
||
return;
|
||
}
|
||
root.innerHTML =
|
||
'<table class="table table-sm zt-multiserver-table"><thead><tr>' +
|
||
"<th>Таблица</th><th>Запись</th><th>Ферма</th><th></th></tr></thead><tbody>" +
|
||
items
|
||
.map(
|
||
(c) =>
|
||
`<tr data-conflict-id="${escapeHtml(c.id)}">` +
|
||
`<td>${escapeHtml(c.table_name)}</td>` +
|
||
`<td>${escapeHtml(c.record_id)}</td>` +
|
||
`<td>${escapeHtml(c.farm_hub_id || "—")}</td>` +
|
||
`<td><button type="button" class="btn btn-sm btn-outline-primary" data-action="open-conflict">Открыть</button></td>` +
|
||
"</tr>",
|
||
)
|
||
.join("") +
|
||
"</tbody></table>";
|
||
|
||
root.querySelectorAll("[data-action=open-conflict]").forEach((btn) => {
|
||
btn.addEventListener("click", () => {
|
||
const row = btn.closest("tr");
|
||
const id = row && row.getAttribute("data-conflict-id");
|
||
if (id) openDetail(root, id, enterpriseId);
|
||
});
|
||
});
|
||
}
|
||
|
||
function openDetail(root, conflictId, enterpriseId) {
|
||
fetch(`${API}/${conflictId}?enterprise_id=${encodeURIComponent(enterpriseId)}`)
|
||
.then((r) => r.json())
|
||
.then((detail) => {
|
||
root.innerHTML =
|
||
`<div class="zt-multiserver-detail">` +
|
||
`<h4>${escapeHtml(detail.table_name)} · ${escapeHtml(detail.record_id)}</h4>` +
|
||
`<div class="row"><div class="col-md-6"><h5>Оркестр</h5><pre class="zt-multiserver-pre">${escapeHtml(JSON.stringify(detail.orchestrator_snapshot, null, 2))}</pre></div>` +
|
||
`<div class="col-md-6"><h5>Сервер</h5><pre class="zt-multiserver-pre">${escapeHtml(JSON.stringify(detail.hub_snapshot, null, 2))}</pre></div></div>` +
|
||
`<div class="zt-multiserver-actions">` +
|
||
`<button type="button" class="btn btn-primary btn-sm" data-resolution="keep_orchestrator">Принять с оркестра</button> ` +
|
||
`<button type="button" class="btn btn-secondary btn-sm" data-resolution="keep_hub">Принять с сервера</button> ` +
|
||
`<button type="button" class="btn btn-link btn-sm" data-action="back-list">Назад</button>` +
|
||
`</div></div>`;
|
||
root.querySelector("[data-action=back-list]")?.addEventListener("click", () => load(root, enterpriseId));
|
||
root.querySelectorAll("[data-resolution]").forEach((btn) => {
|
||
btn.addEventListener("click", () => {
|
||
const resolution = btn.getAttribute("data-resolution");
|
||
fetch(`${API}/${conflictId}/resolve?enterprise_id=${encodeURIComponent(enterpriseId)}`, {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json" },
|
||
body: JSON.stringify({ resolution }),
|
||
}).then(() => load(root, enterpriseId));
|
||
});
|
||
});
|
||
});
|
||
}
|
||
|
||
function load(root, enterpriseId) {
|
||
root.innerHTML = '<p class="text-muted small">Загрузка конфликтов…</p>';
|
||
const orch = global.__WESP_ORCH__ || {};
|
||
if (orch.configured === false) {
|
||
root.innerHTML =
|
||
'<p class="text-muted small">Orchestrator не настроен. Укажите upstream, hub_site_id и api_key в админке.</p>';
|
||
return;
|
||
}
|
||
if (orch.reachable === false) {
|
||
const host = orch.upstream ? ` (${orch.upstream})` : "";
|
||
root.innerHTML =
|
||
`<p class="text-warning small">Orchestrator недоступен${escapeHtml(host)}. Проверьте upstream URL и что сервис запущен.</p>`;
|
||
return;
|
||
}
|
||
fetch(`${API}?enterprise_id=${encodeURIComponent(enterpriseId)}`)
|
||
.then((r) => {
|
||
if (!r.ok) {
|
||
return r.json().catch(() => ({})).then((body) => {
|
||
throw new Error(body.message || `HTTP ${r.status}`);
|
||
});
|
||
}
|
||
return r.json();
|
||
})
|
||
.then((items) => {
|
||
if (items && items.error) {
|
||
throw new Error(items.message || "Orchestrator error");
|
||
}
|
||
renderList(root, Array.isArray(items) ? items : [], enterpriseId);
|
||
})
|
||
.catch((err) => {
|
||
const msg = err && err.message ? err.message : "Не удалось загрузить конфликты.";
|
||
root.innerHTML = `<p class="text-danger small">${escapeHtml(msg)}</p>`;
|
||
});
|
||
}
|
||
|
||
global.WespMultiserverPanel = {
|
||
mount(rootEl, enterpriseId) {
|
||
if (!rootEl) return;
|
||
load(rootEl, enterpriseId || "");
|
||
},
|
||
pendingCount(enterpriseId) {
|
||
return fetch(`${API}?enterprise_id=${encodeURIComponent(enterpriseId)}`)
|
||
.then((r) => r.json())
|
||
.then((items) => (Array.isArray(items) ? items.length : 0))
|
||
.catch(() => 0);
|
||
},
|
||
};
|
||
})(window);
|