You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
orquestrador/admin_app/view/static/scripts/panel.js

250 lines
11 KiB
JavaScript

document.documentElement.dataset.panelReady = "true";
const loginForm = document.querySelector('[data-admin-login-form="true"]');
const reviewBoard = document.querySelector('[data-admin-tool-review-board="true"]');
if (loginForm) {
mountLoginForm(loginForm);
}
if (reviewBoard) {
mountToolReviewBoard(reviewBoard);
}
function mountLoginForm(form) {
const feedback = document.getElementById("admin-login-feedback");
const submitButton = form.querySelector('button[type="submit"]');
const submitLabel = form.querySelector("[data-submit-label]");
const submitSpinner = form.querySelector("[data-submit-spinner]");
form.addEventListener("submit", async (event) => {
event.preventDefault();
toggleLoading(true);
clearFeedback();
const formData = new FormData(form);
const payload = {
email: String(formData.get("email") || "").trim(),
password: String(formData.get("password") || ""),
};
try {
const response = await fetch(form.dataset.authEndpoint, {
method: "POST",
credentials: "same-origin",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify(payload),
});
const authBody = await readJson(response);
if (!response.ok) {
throw new Error(authBody?.detail || "Nao foi possivel autenticar no admin.");
}
showFeedback("success", authBody?.message || "Sessao administrativa web iniciada com sucesso.");
form.reset();
const redirectTo = authBody?.redirect_to || form.dataset.dashboardHref;
if (redirectTo) {
window.setTimeout(() => {
window.location.assign(redirectTo);
}, 250);
}
} catch (error) {
showFeedback("danger", error instanceof Error ? error.message : "Erro inesperado durante o login.");
} finally {
toggleLoading(false);
}
});
function toggleLoading(isLoading) {
submitButton.disabled = isLoading;
submitSpinner.classList.toggle("d-none", !isLoading);
submitLabel.textContent = isLoading ? "Validando acesso..." : "Entrar no painel";
}
function clearFeedback() {
feedback.className = "alert d-none mt-4 mb-0 rounded-4";
feedback.textContent = "";
}
function showFeedback(variant, message) {
feedback.className = `alert alert-${variant} mt-4 mb-0 rounded-4`;
feedback.textContent = message;
}
}
function mountToolReviewBoard(board) {
const refreshButton = board.querySelector("[data-admin-tool-refresh]");
const refreshLabel = board.querySelector("[data-tool-refresh-label]");
const refreshSpinner = board.querySelector("[data-tool-refresh-spinner]");
const feedback = document.getElementById("admin-tool-review-feedback");
const queueList = board.querySelector("[data-tool-review-queue-list]");
const publicationList = board.querySelector("[data-tool-publication-list]");
const lifecycleList = board.querySelector("[data-tool-contract-lifecycle]");
const parameterTypes = board.querySelector("[data-tool-parameter-types]");
if (refreshButton) {
refreshButton.addEventListener("click", () => {
void loadBoard();
});
}
void loadBoard();
async function loadBoard() {
toggleRefreshing(true);
clearFeedback();
const overviewResult = await fetchPanelJson(board.dataset.overviewEndpoint);
const contractsResult = await fetchPanelJson(board.dataset.contractsEndpoint);
const reviewQueueResult = await fetchPanelJson(board.dataset.reviewQueueEndpoint);
const publicationsResult = await fetchPanelJson(board.dataset.publicationsEndpoint);
if (!overviewResult.ok && !contractsResult.ok && !reviewQueueResult.ok && !publicationsResult.ok) {
showFeedback("warning", overviewResult.message || "Entre com uma sessao administrativa web para carregar esta tela.");
}
if (overviewResult.ok) {
renderOverview(overviewResult.body);
}
if (contractsResult.ok) {
renderContracts(contractsResult.body);
} else {
renderLockedLifecycle(contractsResult.message);
}
if (reviewQueueResult.ok) {
renderReviewQueue(reviewQueueResult.body);
} else {
renderLockedQueue(reviewQueueResult.message);
}
if (publicationsResult.ok) {
renderPublications(publicationsResult.body);
} else {
renderLockedPublications(publicationsResult.message);
}
setText("[data-tool-review-last-sync]", formatNow());
toggleRefreshing(false);
}
function toggleRefreshing(isLoading) {
if (!refreshButton || !refreshLabel || !refreshSpinner) {
return;
}
refreshButton.disabled = isLoading;
refreshSpinner.classList.toggle("d-none", !isLoading);
refreshLabel.textContent = isLoading ? "Atualizando..." : "Atualizar leitura";
}
function clearFeedback() {
feedback.className = "alert d-none rounded-4 mb-4";
feedback.textContent = "";
}
function showFeedback(variant, message) {
feedback.className = `alert alert-${variant} rounded-4 mb-4`;
feedback.textContent = message;
}
function renderOverview(payload) {
const workflow = Array.isArray(payload?.workflow) ? payload.workflow : [];
const nextSteps = Array.isArray(payload?.next_steps) ? payload.next_steps : [];
setText("[data-tool-review-lifecycle-count]", String(workflow.length || 0));
if (nextSteps.length > 0 && !feedback.textContent) {
showFeedback("info", `Proximos passos: ${nextSteps[0]}`);
}
}
function renderContracts(payload) {
const lifecycle = Array.isArray(payload?.lifecycle_statuses) ? payload.lifecycle_statuses : [];
const parameterTypeList = Array.isArray(payload?.parameter_types) ? payload.parameter_types : [];
lifecycleList.innerHTML = lifecycle.length > 0
? lifecycle.map((item) => `<div class="admin-tool-inline-note rounded-4 p-3"><div class="fw-semibold">${escapeHtml(item.label)}</div><div class="small text-secondary mt-1">${escapeHtml(item.description)}</div></div>`).join("")
: `<div class="small text-secondary">Nenhuma etapa disponivel.</div>`;
parameterTypes.innerHTML = parameterTypeList.length > 0
? parameterTypeList.map((item) => `<span class="badge rounded-pill bg-body-tertiary text-secondary border">${escapeHtml(item.label)}</span>`).join("")
: `<span class="badge rounded-pill bg-body-tertiary text-secondary border">Sem tipos</span>`;
}
function renderLockedLifecycle(message) {
lifecycleList.innerHTML = `<div class="admin-tool-inline-note rounded-4 p-3"><div class="fw-semibold">Leitura indisponivel</div><div class="small text-secondary mt-1">${escapeHtml(message || "A sessao atual nao pode ler o contrato compartilhado.")}</div></div>`;
parameterTypes.innerHTML = `<span class="badge rounded-pill bg-body-tertiary text-secondary border">Bloqueado</span>`;
}
function renderReviewQueue(payload) {
const items = Array.isArray(payload?.items) ? payload.items : [];
setText("[data-tool-review-queue-count]", String(items.length));
setText("[data-tool-review-queue-mode]", payload?.queue_mode || "Fila web");
queueList.innerHTML = items.length > 0
? items.map((item) => `<article class="admin-tool-review-card rounded-4 p-4"><div class="d-flex justify-content-between align-items-start gap-3 mb-3"><div><div class="small text-uppercase fw-semibold text-secondary mb-2">${escapeHtml(item.gate || "revisao")}</div><h4 class="h5 fw-semibold mb-1">${escapeHtml(item.display_name || item.tool_name || "Tool")}</h4><div class="small text-secondary">${escapeHtml(item.tool_name || "")}</div></div><span class="badge rounded-pill bg-warning-subtle text-warning-emphasis border border-warning-subtle">${escapeHtml(item.status || "pendente")}</span></div><p class="text-secondary mb-0">${escapeHtml(item.summary || payload?.message || "Item aguardando analise do time.")}</p></article>`).join("")
: `<div class="admin-tool-empty-state rounded-4 p-4"><h4 class="h5 fw-semibold mb-2">Fila sem itens no momento</h4><p class="text-secondary mb-0">${escapeHtml(payload?.message || "Nenhuma tool aguardando revisao agora.")}</p></div>`;
}
function renderLockedQueue(message) {
setText("[data-tool-review-queue-count]", "0");
setText("[data-tool-review-queue-mode]", "Bloqueado");
queueList.innerHTML = `<div class="admin-tool-empty-state rounded-4 p-4"><h4 class="h5 fw-semibold mb-2">Fila indisponivel</h4><p class="text-secondary mb-0">${escapeHtml(message || "A sessao atual nao pode acessar a fila de revisao.")}</p></div>`;
}
function renderPublications(payload) {
const items = Array.isArray(payload?.publications) ? payload.publications : [];
setText("[data-tool-review-publication-count]", String(items.length));
setText("[data-tool-publication-source]", payload?.source || "Catalogo web");
publicationList.innerHTML = items.length > 0
? items.slice(0, 9).map((item) => `<div class="col-12 col-md-6 col-xxl-4"><article class="admin-tool-publication-card rounded-4 p-4 h-100"><div class="d-flex justify-content-between align-items-start gap-3 mb-3"><div><div class="small text-uppercase fw-semibold text-secondary mb-2">${escapeHtml(item.domain || "tool")}</div><h4 class="h5 fw-semibold mb-1">${escapeHtml(item.display_name || item.tool_name || "Tool")}</h4><div class="small text-secondary">${escapeHtml(item.tool_name || "")}</div></div><span class="badge rounded-pill bg-success-subtle text-success-emphasis border border-success-subtle">v${escapeHtml(String(item.version || 1))}</span></div><p class="text-secondary mb-3">${escapeHtml(item.description || "Publicacao ativa no catalogo do produto.")}</p><div class="small text-secondary">${escapeHtml(item.implementation_module || "")}</div></article></div>`).join("")
: `<div class="col-12"><div class="admin-tool-empty-state rounded-4 p-4"><h4 class="h5 fw-semibold mb-2">Catalogo ativo vazio</h4><p class="text-secondary mb-0">Nenhuma publicacao ativa retornada pela sessao web.</p></div></div>`;
}
function renderLockedPublications(message) {
setText("[data-tool-review-publication-count]", "0");
setText("[data-tool-publication-source]", "Bloqueado");
publicationList.innerHTML = `<div class="col-12"><div class="admin-tool-empty-state rounded-4 p-4"><h4 class="h5 fw-semibold mb-2">Catalogo protegido</h4><p class="text-secondary mb-0">${escapeHtml(message || "A sessao atual nao possui permissao para ler as publicacoes ativas.")}</p></div></div>`;
}
}
async function fetchPanelJson(url) {
const response = await fetch(url, {
credentials: "same-origin",
headers: { Accept: "application/json" },
});
const body = await readJson(response);
if (response.ok) {
return { ok: true, body };
}
const defaultMessage = response.status === 401
? "Entre com uma sessao administrativa web para visualizar esta area."
: body?.detail || "Nao foi possivel carregar os dados desta superficie.";
return { ok: false, body, message: defaultMessage };
}
async function readJson(response) {
try {
return await response.json();
} catch {
return null;
}
}
function setText(selector, value) {
const target = document.querySelector(selector);
if (target) {
target.textContent = value;
}
}
function formatNow() {
return new Date().toLocaleTimeString("pt-BR", { hour: "2-digit", minute: "2-digit" });
}
function escapeHtml(value) {
return String(value || "")
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;")
.replaceAll("'", "&#39;");
}