Initial commit: site monorepo with API, web, and infra.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
влад
2026-07-16 10:11:54 +03:00
co-authored by Cursor
commit 016910ffb7
447 changed files with 73972 additions and 0 deletions
@@ -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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
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);