Initial commit: site monorepo with API, web, and infra.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* 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>';
|
||||
fetch(`${API}?enterprise_id=${encodeURIComponent(enterpriseId)}`)
|
||||
.then((r) => r.json())
|
||||
.then((items) => renderList(root, items || [], enterpriseId))
|
||||
.catch(() => {
|
||||
root.innerHTML = '<p class="text-danger small">Не удалось загрузить конфликты.</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);
|
||||
Reference in New Issue
Block a user