const $ = (selector) => document.querySelector(selector);
let allEvents = [];

const esc = (value) => String(value ?? "").replace(/[&<>"']/g, (char) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[char]));
const duration = (value) => {
  const seconds = Math.max(0, Number(value || 0) / 1000);
  const format = (number) => Number(number.toFixed(1)).toString().replace(".", ",");
  if (seconds < 60) return `${format(seconds)} с`;
  const minutes = Math.floor(seconds / 60), restSeconds = seconds - minutes * 60;
  if (minutes < 60) return `${minutes} мин ${format(restSeconds)} с`;
  return `${Math.floor(minutes / 60)} ч ${minutes % 60} мин ${format(restSeconds)} с`;
};
async function api(path) { const response = await fetch(path); if (!response.ok) throw new Error(await response.text()); return response.json(); }
function displayDetails(value) { if (Array.isArray(value)) return value.map(displayDetails); if (!value || typeof value !== "object") return value; return Object.fromEntries(Object.entries(value).map(([key, item]) => /^(duration|result_duration|p50|p95|max)_ms$/.test(key) ? [key.slice(0, -3), duration(item)] : [key, displayDetails(item)])); }
function showDetails(row) { const received = row.received || row, observer = row.observer || received.observer || {}, method = row.action || observer.method || received.method || "Детали"; $("#detail-title").textContent = actionLabels?.[method] || method; $("#detail-meta").textContent = [row.ref || received.ref || received.base_id, received.status, observer.duration_ms === undefined ? "" : duration(observer.duration_ms)].filter(Boolean).join(" · "); $("#detail-json").textContent = JSON.stringify(displayDetails(row), null, 2); $("#detail").showModal(); }

function renderEvents() {
  const method = $("#method").value.trim(), status = $("#status").value, base = $("#base").value.trim(), minimum = Number($("#min-duration").value || 0) * 1000, since = $("#since").value, until = $("#until").value;
  const rows = allEvents.filter((event) => (!method || event.method.includes(method)) && (!status || event.status === status) && (!base || event.base_id === base) && event.duration_ms >= minimum && (!since || event.time >= since) && (!until || event.time <= until));
  $("#events").innerHTML = rows.map((event, index) => `<tr><td class="muted">${esc(event.time)}</td><td><b>${esc(event.method)}</b><br><span class="muted">${esc(event.selector.ref || event.selector.object_name || event.base_id || "—")}</span></td><td class="status ${esc(event.status)}">${esc(event.status)}</td><td>${duration(event.duration_ms)}</td><td>${esc(event.error || event.exception_type || "—")}</td><td><button data-row="${index}">Детали</button></td></tr>`).join("") || '<tr><td colspan="6" class="muted">Запросов по выбранному фильтру нет.</td></tr>';
  $("#events").querySelectorAll("button").forEach((button) => { button.onclick = () => showDetails(rows[Number(button.dataset.row)]); });
}

const treeGroups = [["Общие",["CommonModule","CommonForm","CommonCommand","CommonAttribute","CommonPicture","CommonTemplate","Constant","DefinedType","Role","Subsystem","ScheduledJob","EventSubscription","FunctionalOption","SessionParameter"]],["Справочники",["Catalog"]],["Документы",["Document","DocumentJournal","DocumentNumerator","Sequence"]],["Перечисления",["Enum"]],["Отчёты и обработки",["Report","DataProcessor"]],["Планы",["ChartOfAccounts","ChartOfCharacteristicTypes","ChartOfCalculationTypes","ExchangePlan"]],["Регистры",["InformationRegister","AccumulationRegister","AccountingRegister","CalculationRegister"]],["Бизнес-процессы и задачи",["BusinessProcess","Task"]],["Сервисы и интеграции",["WebService","HTTPService","IntegrationService","ExternalDataSource","XDTOPackage"]]];
const catalogActions = [["card", "Карточка"], ["properties", "Свойства"], ["attributes", "Реквизиты"], ["forms", "Формы"], ["commands", "Команды"], ["modules", "Модули"], ["templates", "Макеты"], ["related", "Связи"]];
const actionLabels = Object.fromEntries(catalogActions);
document.addEventListener("click", (event) => { const button = event.target.closest("summary button[data-kind]"); if (button) { event.preventDefault(); button.closest("details").open = true; } }, true);
function objectActions(base, object) { const ref = object.ref || `Catalog.${object.name}`; return `<span class="object-actions">${catalogActions.map(([action, label]) => `<button data-action="${action}" data-base="${esc(base)}" data-ref="${esc(ref)}">${label}</button>`).join("")}</span>`; }
async function runObjectAction(button) { const { action, base, ref } = button.dataset; button.disabled = true; const label = button.textContent; button.textContent = "…"; try { const result = await api(`/api/object/action?base_id=${encodeURIComponent(base)}&ref=${encodeURIComponent(ref)}&action=${encodeURIComponent(action)}`); showDetails({ action, ref, duration: duration(result.observer?.duration_ms), received: result }); } catch (error) { showDetails({ action, ref, error: error.message }); } finally { button.disabled = false; button.textContent = label; } }
function bindObjectActions(root) { root.querySelectorAll("button[data-action]").forEach((button) => { button.onclick = () => runObjectAction(button); }); if (!root.querySelector(".object-actions")) return; const search = document.createElement("input"), status = document.createElement("small"), rows = [...root.querySelectorAll(".object-row")]; search.className = "catalog-search"; search.type = "search"; search.placeholder = "Найти справочник"; search.setAttribute("aria-label", "Найти справочник"); status.className = "catalog-filter-status muted"; root.prepend(status); root.prepend(search); const filter = () => { const query = search.value.trim().toLocaleLowerCase(); let visible = 0; rows.forEach((row) => { const matches = !query || row.textContent.toLocaleLowerCase().includes(query); row.hidden = !matches; if (matches) visible += 1; }); status.textContent = `Показано: ${visible} из ${rows.length}`; }; search.oninput = filter; filter(); }
async function loadAllKindObjects(base, kind) {
  const pageSize = 1000, first = await api(`/api/objects?base_id=${encodeURIComponent(base)}&kind=${encodeURIComponent(kind)}&limit=${pageSize}&offset=0`), objects = [...(first.objects || [])], total = Number(first.counts?.total_visible ?? first.counts?.total ?? objects.length);
  let elapsed = Number(first.observer?.duration_ms || 0);
  for (let offset = objects.length; offset < total; offset += pageSize) { const page = await api(`/api/objects?base_id=${encodeURIComponent(base)}&kind=${encodeURIComponent(kind)}&limit=${pageSize}&offset=${offset}`), rows = page.objects || []; elapsed += Number(page.observer?.duration_ms || 0); objects.push(...rows); if (!rows.length) break; }
  return { ...first, objects, observer: { ...(first.observer || {}), duration_ms: elapsed } };
}
async function tree() {
  const root = $("#tree-result"), base = $("#tree-base").value.trim() || "upo"; root.textContent = "Читаю структуру конфигурации…";
  try {
    const data = await api(`/api/coverage?base_id=${encodeURIComponent(base)}`), kinds = (data.audit || {}).metadata_kinds || [], byKind = Object.fromEntries(kinds.map((x) => [x.kind, x]));
    root.innerHTML = `<h2>Конфигурация: ${esc(base)}</h2>${treeGroups.map(([title, names]) => { const rows = names.map((name) => byKind[name]).filter(Boolean); if (!rows.length) return ""; if (rows.length === 1) { const item = rows[0]; return `<details class="finding"><summary><b>▸ ${title}</b> · ${item.count || 0} объектов · <button data-kind="${esc(item.kind)}">↻ Читать</button></summary><div id="kind-${esc(item.kind)}"></div></details>`; } return `<details class="finding"><summary><b>▸ ${title}</b> · ${rows.reduce((sum, item) => sum + Number(item.count || 0), 0)} объектов</summary>${rows.map((item) => `<div>　├─ ${esc(item.kind_ru || item.kind)} · ${item.count || 0} <button data-kind="${esc(item.kind)}">↻ Читать</button><div id="kind-${esc(item.kind)}"></div></div>`).join("")}</details>`; }).join("")}`;
    root.querySelectorAll("button[data-kind]").forEach((button) => { button.onclick = async () => { const box = $("#kind-" + button.dataset.kind); box.textContent = "⏳ чтение всех записей…"; try { const result = await loadAllKindObjects(base, button.dataset.kind), objects = result.objects || [], observer = result.observer || {}; box.innerHTML = `<small class="muted">${objects.length} объектов · ${duration(observer.duration_ms)} · ${observer.method || ""} · <button class="node-log">Журнал</button></small>${objects.map((object) => `<div class="object-row">　└─ <span>${esc(object.name || object.ref || "—")}</span>${button.dataset.kind === "Catalog" ? objectActions(base, object) : ""}</div>`).join("") || "Нет объектов."}`; box.querySelector(".node-log").onclick = () => showDetails({ received: result, observer }); bindObjectActions(box); } catch (error) { box.textContent = `Не удалось загрузить объекты: ${error.message}`; } }; });
  } catch (error) { root.textContent = error.message; }
}

function renderSummary(summary) { $("#cards").innerHTML = [["Запросов", summary.events], ["Исключений", summary.exceptions], ["p50", duration(summary.p50_ms)], ["p95", duration(summary.p95_ms)]].map(([label, value]) => `<div class="card">${label}<b>${value}</b></div>`).join(""); const maximum = Math.max(...summary.methods.map((item) => item.p95_ms), 1); $("#methods").innerHTML = summary.methods.slice(0, 12).map((item) => `<div class="bar"><span>${esc(item.method)} <small class="muted">${item.calls}</small></span><i style="width:${Math.max(2, item.p95_ms / maximum * 100)}%"></i><span>${duration(item.p95_ms)}</span></div>`).join(""); const findings = summary.findings.map((item) => `<div class="finding"><b>${esc(item.method)}</b> · ${esc(item.status)} · ${item.count} раз<br><span class="muted">${esc(item.error || "без кода")}</span><p>${esc(item.recommendation)}</p></div>`).join("") || '<p class="muted">Отклонений нет.</p>'; const slow = (summary.slow_events || []).map((item) => `<div class="finding"><b>${esc(item.method)}</b> · ${duration(item.duration_ms)} · <span class="muted">${esc(item.request_id || "—")}</span><br>${esc(item.selector.ref || item.selector.object_name || item.base_id || "без selector-а")}</div>`).join("") || '<p class="muted">Нет.</p>'; $("#findings").innerHTML = `${findings}<h2>Выбросы ≥ 5 сек</h2>${slow}`; }
function renderCorrelations(data) { $("#correlations").innerHTML = data.correlations.slice(0, 250).map((item) => `<tr><td class="muted">${esc(item.request_id)}</td><td>${esc(item.mcp.method)}</td><td class="${esc(item.mcp.status)}">${esc(item.mcp.status)}</td><td class="${item.rest ? "ok" : "exception"}">${item.rest ? esc(item.rest.status) : "не достиг REST"}</td><td>${duration(item.mcp.duration_ms)}${item.rest ? ` / ${duration(item.rest.duration_ms)}` : ""}</td></tr>`).join("") || '<tr><td colspan="5" class="muted">Коррелируемых событий нет.</td></tr>'; }
async function openKind(kind, baseId) { const root = $("#object-list"); root.textContent = `Загружаю ${kind}…`; try { const data = await api(`/api/objects?base_id=${encodeURIComponent(baseId)}&kind=${encodeURIComponent(kind)}`), objects = data.objects || []; root.innerHTML = `<h2>${esc(kind)} · ${objects.length}</h2><div class="table-wrap"><table><thead><tr><th>Объект</th><th>Синоним</th><th>Происхождение</th></tr></thead><tbody>${objects.map((item) => `<tr><td>${esc(item.name || item.ref || "—")}</td><td>${esc(item.synonym || "—")}</td><td>${esc((item.origin || {}).source || "—")}</td></tr>`).join("") || '<tr><td colspan="3" class="muted">Объектов нет.</td></tr>'}</tbody></table></div>`; } catch (error) { root.textContent = `Не удалось загрузить объекты: ${error.message}`; } }
async function coverage() { const root = $("#coverage-result"); root.textContent = "Читаю контракт и coverage snapshot…"; try { const baseId = $("#coverage-base").value.trim() || "upo", data = await api(`/api/coverage?base_id=${encodeURIComponent(baseId)}`), audit = data.audit || {}; if (audit.status && audit.status !== "ok") { root.innerHTML = `<div class="finding"><b>База недоступна для metadata coverage: ${esc(audit.status)}</b><p>Это не нулевое покрытие.</p></div>`; return; } const kinds = audit.metadata_kinds || []; root.innerHTML = `<h2>База ${esc(baseId)} → типы метаданных</h2><div class="table-wrap"><table><thead><tr><th>Тип</th><th>Объектов</th><th></th></tr></thead><tbody>${kinds.map((item) => `<tr><td>└ ${esc(item.kind || "—")}</td><td>${esc(item.count || 0)}</td><td><button class="open-kind" data-kind="${esc(item.kind)}">Открыть</button></td></tr>`).join("")}</tbody></table></div><div id="object-list" class="muted"></div>`; root.querySelectorAll(".open-kind").forEach((button) => { button.onclick = () => openKind(button.dataset.kind, baseId); }); } catch (error) { root.textContent = `Не удалось получить coverage: ${error.message}`; } }
async function load() { const [events, summary, correlations] = await Promise.all([api("/api/events?limit=500"), api("/api/summary"), api("/api/correlations")]); allEvents = events.events; renderEvents(); renderSummary(summary); renderCorrelations(correlations); }
document.querySelectorAll(".tab").forEach((button) => { button.onclick = () => { document.querySelectorAll(".tab,.panel").forEach((node) => node.classList.remove("active")); button.classList.add("active"); $(`#${button.dataset.tab}`).classList.add("active"); }; });
["method", "base", "min-duration", "since", "until"].forEach((id) => { $(`#${id}`).oninput = renderEvents; });
$("#status").onchange = renderEvents; $("#refresh").onclick = load; $("#load-coverage").onclick = coverage; $("#close").onclick = () => $("#detail").close(); $("#load-tree").onclick = tree;
load().catch((error) => { $("#events").innerHTML = `<tr><td colspan="6">Ошибка загрузки: ${esc(error.message)}</td></tr>`; });
