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.
1880 lines
103 KiB
JavaScript
1880 lines
103 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"]');
|
|
const toolIntakePage = document.querySelector('[data-admin-tool-intake="true"]');
|
|
const collaboratorBoard = document.querySelector('[data-admin-collaborator-board="true"]');
|
|
const systemConfigurationPage = document.querySelector('[data-admin-system-configuration="true"]');
|
|
const salesRevenueReportsPage = document.querySelector('[data-admin-sales-revenue-reports="true"]');
|
|
const rentalReportsPage = document.querySelector('[data-admin-rental-reports="true"]');
|
|
const botMonitoringPage = document.querySelector('[data-admin-bot-monitoring="true"]');
|
|
|
|
if (loginForm) {
|
|
mountLoginForm(loginForm);
|
|
}
|
|
|
|
if (reviewBoard) {
|
|
mountToolReviewBoard(reviewBoard);
|
|
}
|
|
|
|
if (toolIntakePage) {
|
|
mountToolIntakePage(toolIntakePage);
|
|
}
|
|
|
|
if (collaboratorBoard) {
|
|
mountCollaboratorBoard(collaboratorBoard);
|
|
}
|
|
|
|
if (systemConfigurationPage) {
|
|
mountSystemConfigurationPage(systemConfigurationPage);
|
|
}
|
|
|
|
if (salesRevenueReportsPage) {
|
|
mountSalesRevenueReportsPage(salesRevenueReportsPage);
|
|
}
|
|
|
|
if (rentalReportsPage) {
|
|
mountRentalReportsPage(rentalReportsPage);
|
|
}
|
|
|
|
if (botMonitoringPage) {
|
|
mountBotMonitoringPage(botMonitoringPage);
|
|
}
|
|
|
|
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]");
|
|
const detailStatus = board.querySelector("[data-tool-review-detail-status]");
|
|
const detailSummary = board.querySelector("[data-tool-review-detail-summary]");
|
|
const detailTitle = board.querySelector("[data-tool-review-detail-title]");
|
|
const detailMeta = board.querySelector("[data-tool-review-detail-meta]");
|
|
const validationList = board.querySelector("[data-tool-review-validation-list]");
|
|
const historyList = board.querySelector("[data-tool-review-history-list]");
|
|
const nextStepsList = board.querySelector("[data-tool-review-next-steps]");
|
|
const codeField = board.querySelector("[data-tool-review-code]");
|
|
const decisionNotes = board.querySelector("[data-tool-review-decision-notes]");
|
|
const decisionHint = board.querySelector("[data-tool-review-decision-hint]");
|
|
const reviewedGeneratedCode = board.querySelector("[data-tool-review-reviewed-code]");
|
|
const authorizeGenerationButton = board.querySelector('[data-tool-review-action="authorize-generation"]');
|
|
const runPipelineButton = board.querySelector('[data-tool-review-action="run-pipeline"]');
|
|
const reviewButton = board.querySelector('[data-tool-review-action="review"]');
|
|
const requestChangesButton = board.querySelector('[data-tool-review-action="request-changes"]');
|
|
const approveButton = board.querySelector('[data-tool-review-action="approve"]');
|
|
const publishButton = board.querySelector('[data-tool-review-action="publish"]');
|
|
const closeProposalButton = board.querySelector('[data-tool-review-action="close"]');
|
|
const deactivateButton = board.querySelector('[data-tool-review-action="deactivate"]');
|
|
const rollbackButton = board.querySelector('[data-tool-review-action="rollback"]');
|
|
|
|
let selectedVersionId = "";
|
|
let lastRenderedHumanGate = null;
|
|
let lastRenderedHasSourceCode = false;
|
|
|
|
if (refreshButton) {
|
|
refreshButton.addEventListener("click", () => {
|
|
void loadBoard(selectedVersionId);
|
|
});
|
|
}
|
|
|
|
if (queueList) {
|
|
queueList.addEventListener("click", (event) => {
|
|
const target = event.target;
|
|
if (!(target instanceof HTMLElement)) {
|
|
return;
|
|
}
|
|
const trigger = target.closest("[data-tool-review-select]");
|
|
if (!(trigger instanceof HTMLElement)) {
|
|
return;
|
|
}
|
|
const nextVersionId = String(trigger.dataset.versionId || "").trim();
|
|
if (!nextVersionId) {
|
|
return;
|
|
}
|
|
void loadReviewDetail(nextVersionId);
|
|
});
|
|
}
|
|
|
|
if (publicationList) {
|
|
publicationList.addEventListener("click", (event) => {
|
|
const target = event.target;
|
|
if (!(target instanceof HTMLElement)) {
|
|
return;
|
|
}
|
|
const trigger = target.closest("[data-tool-publication-select]");
|
|
if (!(trigger instanceof HTMLElement)) {
|
|
return;
|
|
}
|
|
const nextVersionId = String(trigger.dataset.versionId || "").trim();
|
|
if (!nextVersionId) {
|
|
return;
|
|
}
|
|
void loadReviewDetail(nextVersionId);
|
|
});
|
|
}
|
|
|
|
[authorizeGenerationButton, runPipelineButton, reviewButton, requestChangesButton, approveButton, publishButton, closeProposalButton, deactivateButton, rollbackButton].forEach((button) => {
|
|
if (!(button instanceof HTMLButtonElement)) {
|
|
return;
|
|
}
|
|
button.dataset.defaultLabel = button.textContent || "";
|
|
button.addEventListener("click", () => {
|
|
const actionKey = String(button.dataset.toolReviewAction || "").trim();
|
|
if (!actionKey) {
|
|
return;
|
|
}
|
|
void submitGovernanceAction(actionKey);
|
|
});
|
|
});
|
|
|
|
renderEmptyDetail("Selecione um item da fila para carregar o contexto completo da revisao humana.");
|
|
void loadBoard();
|
|
|
|
async function loadBoard(preferredVersionId = "") {
|
|
toggleRefreshing(true);
|
|
clearFeedback();
|
|
|
|
const [overviewResult, contractsResult, reviewQueueResult, publicationsResult] = await Promise.all([
|
|
fetchPanelJson(board.dataset.overviewEndpoint),
|
|
fetchPanelJson(board.dataset.contractsEndpoint),
|
|
fetchPanelJson(board.dataset.reviewQueueEndpoint),
|
|
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, preferredVersionId || selectedVersionId);
|
|
const items = Array.isArray(reviewQueueResult.body?.items) ? reviewQueueResult.body.items : [];
|
|
if (items.length > 0) {
|
|
const nextVersionId = items.some((item) => item?.version_id === (preferredVersionId || selectedVersionId))
|
|
? (preferredVersionId || selectedVersionId)
|
|
: String(items[0]?.version_id || "").trim();
|
|
if (nextVersionId) {
|
|
await loadReviewDetail(nextVersionId);
|
|
} else {
|
|
renderEmptyDetail(reviewQueueResult.body?.message || "Nenhuma versao pronta para detalhe.");
|
|
}
|
|
} else {
|
|
const fallbackVersionId = String(preferredVersionId || selectedVersionId || "").trim();
|
|
if (fallbackVersionId) {
|
|
await loadReviewDetail(fallbackVersionId);
|
|
} else {
|
|
selectedVersionId = "";
|
|
renderEmptyDetail(reviewQueueResult.body?.message || "Nenhuma versao aguardando revisao neste momento.");
|
|
}
|
|
}
|
|
} else {
|
|
renderLockedQueue(reviewQueueResult.message);
|
|
renderLockedDetail(reviewQueueResult.message || "A sessao atual nao pode acessar o detalhe de revisao.");
|
|
}
|
|
if (publicationsResult.ok) {
|
|
renderPublications(publicationsResult.body);
|
|
} else {
|
|
renderLockedPublications(publicationsResult.message);
|
|
}
|
|
|
|
setText("[data-tool-review-last-sync]", formatNow());
|
|
toggleRefreshing(false);
|
|
}
|
|
|
|
async function loadReviewDetail(versionId) {
|
|
const normalizedVersionId = String(versionId || "").trim();
|
|
if (!normalizedVersionId) {
|
|
renderEmptyDetail("Selecione uma versao valida para abrir o detalhe da revisao.");
|
|
return;
|
|
}
|
|
|
|
selectedVersionId = normalizedVersionId;
|
|
renderDetailLoading();
|
|
|
|
const detailUrl = `${board.dataset.reviewQueueEndpoint}/${encodeURIComponent(normalizedVersionId)}`;
|
|
const detailResult = await fetchPanelJson(detailUrl);
|
|
if (!detailResult.ok) {
|
|
renderLockedDetail(detailResult.message || "Nao foi possivel carregar o detalhe da versao selecionada.");
|
|
showFeedback("warning", detailResult.message || "Nao foi possivel carregar o detalhe da revisao humana.");
|
|
return;
|
|
}
|
|
|
|
renderReviewDetail(detailResult.body);
|
|
renderReviewQueueSelection(normalizedVersionId);
|
|
}
|
|
|
|
async function submitGovernanceAction(actionKey) {
|
|
if (!selectedVersionId) {
|
|
showFeedback("warning", "Selecione uma versao da fila antes de registrar uma decisao humana.");
|
|
return;
|
|
}
|
|
|
|
const actionUrl = resolveGovernanceActionUrl(actionKey, selectedVersionId);
|
|
if (!actionUrl) {
|
|
showFeedback("warning", "A acao solicitada nao esta disponivel para esta versao.");
|
|
return;
|
|
}
|
|
|
|
let payload;
|
|
if (actionKey === "review") {
|
|
payload = {
|
|
decision_notes: String(decisionNotes?.value || "").trim(),
|
|
reviewed_generated_code: Boolean(reviewedGeneratedCode?.checked),
|
|
};
|
|
} else if (["authorize-generation", "request-changes", "approve", "deactivate", "rollback"].includes(actionKey)) {
|
|
payload = {
|
|
decision_notes: String(decisionNotes?.value || "").trim(),
|
|
};
|
|
} else if (actionKey === "close") {
|
|
const normalizedNotes = String(decisionNotes?.value || "").trim();
|
|
payload = normalizedNotes ? { decision_notes: normalizedNotes } : {};
|
|
}
|
|
|
|
toggleActionLoading(actionKey, true);
|
|
clearFeedback();
|
|
|
|
try {
|
|
const response = await fetch(actionUrl, {
|
|
method: "POST",
|
|
credentials: "same-origin",
|
|
headers: {
|
|
Accept: "application/json",
|
|
...(payload ? { "Content-Type": "application/json" } : {}),
|
|
},
|
|
...(payload ? { body: JSON.stringify(payload) } : {}),
|
|
});
|
|
const body = await readJson(response);
|
|
if (!response.ok) {
|
|
throw new Error(body?.detail || "Nao foi possivel registrar a decisao humana desta versao.");
|
|
}
|
|
|
|
if (decisionNotes instanceof HTMLTextAreaElement) {
|
|
decisionNotes.value = "";
|
|
}
|
|
if (reviewedGeneratedCode instanceof HTMLInputElement) {
|
|
reviewedGeneratedCode.checked = false;
|
|
}
|
|
showFeedback("success", body?.message || "Decisao humana registrada com sucesso.");
|
|
await loadBoard(body?.version_id || selectedVersionId);
|
|
} catch (error) {
|
|
showFeedback("danger", error instanceof Error ? error.message : "Erro inesperado ao registrar a decisao humana.");
|
|
} finally {
|
|
toggleActionLoading(actionKey, false);
|
|
}
|
|
}
|
|
|
|
function resolveGovernanceActionUrl(actionKey, versionId) {
|
|
const encodedVersionId = encodeURIComponent(String(versionId || "").trim());
|
|
if (!encodedVersionId) {
|
|
return "";
|
|
}
|
|
const reviewQueueBase = String(board.dataset.reviewQueueEndpoint || "").replace(/\/+$/, "");
|
|
const publicationsBase = String(board.dataset.publicationsEndpoint || "").replace(/\/+$/, "");
|
|
const draftsBase = reviewQueueBase.endsWith("/review-queue")
|
|
? `${reviewQueueBase.slice(0, -"/review-queue".length)}/drafts`
|
|
: "";
|
|
const pipelineBase = reviewQueueBase.endsWith("/review-queue")
|
|
? `${reviewQueueBase.slice(0, -"/review-queue".length)}/pipeline`
|
|
: "";
|
|
const currentGate = String(lastRenderedHumanGate?.current_gate || "").trim();
|
|
const usesDraftGovernanceRoute = ["generation_decision_required", "generation_pipeline_required", "changes_requested"].includes(currentGate);
|
|
if (actionKey === "publish" || actionKey === "deactivate" || actionKey === "rollback") {
|
|
return `${publicationsBase}/${encodedVersionId}/${actionKey}`;
|
|
}
|
|
if (actionKey === "review" || actionKey === "approve" || actionKey === "request-changes") {
|
|
return `${reviewQueueBase}/${encodedVersionId}/${actionKey}`;
|
|
}
|
|
if (actionKey === "authorize-generation") {
|
|
return draftsBase ? `${draftsBase}/${encodedVersionId}/authorize-generation` : "";
|
|
}
|
|
if (actionKey === "run-pipeline") {
|
|
return pipelineBase ? `${pipelineBase}/${encodedVersionId}/run` : "";
|
|
}
|
|
if (actionKey === "close") {
|
|
if (usesDraftGovernanceRoute && draftsBase) {
|
|
return `${draftsBase}/${encodedVersionId}/close`;
|
|
}
|
|
return `${reviewQueueBase}/${encodedVersionId}/close`;
|
|
}
|
|
return "";
|
|
}
|
|
|
|
function toggleRefreshing(isLoading) {
|
|
if (!refreshButton || !refreshLabel || !refreshSpinner) {
|
|
return;
|
|
}
|
|
refreshButton.disabled = isLoading;
|
|
refreshSpinner.classList.toggle("d-none", !isLoading);
|
|
refreshLabel.textContent = isLoading ? "Atualizando..." : "Atualizar leitura";
|
|
}
|
|
|
|
function toggleActionLoading(actionKey, isLoading) {
|
|
const buttonsByAction = {
|
|
"authorize-generation": authorizeGenerationButton,
|
|
"run-pipeline": runPipelineButton,
|
|
review: reviewButton,
|
|
"request-changes": requestChangesButton,
|
|
approve: approveButton,
|
|
publish: publishButton,
|
|
close: closeProposalButton,
|
|
deactivate: deactivateButton,
|
|
rollback: rollbackButton,
|
|
};
|
|
const button = buttonsByAction[actionKey];
|
|
if (!(button instanceof HTMLButtonElement)) {
|
|
return;
|
|
}
|
|
const defaultLabel = button.dataset.defaultLabel || button.textContent || "";
|
|
button.disabled = isLoading || button.disabled;
|
|
button.textContent = isLoading ? "Processando..." : defaultLabel;
|
|
if (!isLoading) {
|
|
configureActionPanel(lastRenderedHumanGate, lastRenderedHasSourceCode);
|
|
}
|
|
}
|
|
|
|
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 describeReviewGate(gate) {
|
|
const normalizedGate = String(gate || "").trim();
|
|
const gateMap = {
|
|
generation_decision_required: {
|
|
label: "Aguardando triagem para gerar",
|
|
description: "A proposta foi criada, mas ainda nao deve consumir geracao de codigo."
|
|
},
|
|
generation_pipeline_required: {
|
|
label: "Pronta para gerar",
|
|
description: "A triagem humana autorizou a pipeline. Falta executar a geracao isolada."
|
|
},
|
|
generation_pipeline_queued: {
|
|
label: "Pipeline enfileirada",
|
|
description: "O worker do admin ja recebeu a geracao e vai iniciar a iteracao assim que houver slot."
|
|
},
|
|
generation_pipeline_running: {
|
|
label: "Pipeline em execucao",
|
|
description: "O worker do admin esta gerando ou validando a iteracao atual desta proposta."
|
|
},
|
|
generation_worker_failed: {
|
|
label: "Worker falhou",
|
|
description: "A execucao ass?ncrona falhou antes de concluir a pipeline."
|
|
},
|
|
changes_requested: {
|
|
label: "Ajustes solicitados",
|
|
description: "A mesma versao aguarda uma nova iteracao guiada pelo ultimo parecer humano."
|
|
},
|
|
validation_required: {
|
|
label: "Codigo aguardando leitura",
|
|
description: "A geracao terminou e o codigo atual precisa de leitura humana da diretoria."
|
|
},
|
|
director_approval_required: {
|
|
label: "Aguardando aprovacao final",
|
|
description: "A leitura do codigo ja aconteceu e falta a aprovacao formal antes da ativacao."
|
|
},
|
|
director_publication_required: {
|
|
label: "Pronta para ativacao",
|
|
description: "A iteracao atual ja foi revisada e aprovada. Falta ativar no catalogo governado."
|
|
},
|
|
publication_active: {
|
|
label: "Publicacao ativa",
|
|
description: "A versao ja abastece o catalogo governado do produto."
|
|
},
|
|
pipeline_retry_required: {
|
|
label: "Retry tecnico necessario",
|
|
description: "A pipeline falhou tecnicamente e precisa de nova execucao antes de voltar para a revisao humana."
|
|
},
|
|
archived_history: {
|
|
label: "Proposta arquivada",
|
|
description: "A proposta saiu da esteira ativa e permanece apenas para historico e auditoria."
|
|
}
|
|
};
|
|
return gateMap[normalizedGate] || {
|
|
label: normalizedGate || "Governanca pendente",
|
|
description: "A proposta continua no fluxo governado aguardando a proxima decisao humana ou tecnica."
|
|
};
|
|
}
|
|
|
|
function describeGenerationMode(mode) {
|
|
const normalizedMode = String(mode || "").trim();
|
|
const modeMap = {
|
|
initial_generation: "Geracao inicial",
|
|
change_request_refinement: "Refatoracao guiada por feedback",
|
|
failed_pipeline_retry: "Retry tecnico da pipeline",
|
|
regeneration_with_context: "Regeneracao com contexto anterior",
|
|
legacy_generation: "Geracao legada"
|
|
};
|
|
return modeMap[normalizedMode] || normalizedMode || "Nenhuma geracao concluida ainda";
|
|
}
|
|
|
|
function renderReviewQueue(payload, preferredVersionId = "") {
|
|
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) => {
|
|
const isSelected = String(item?.version_id || "") === String(preferredVersionId || selectedVersionId || "");
|
|
const gatePresentation = describeReviewGate(item?.gate);
|
|
const validationSummary = item?.automated_validation_summary
|
|
? `<div class="small text-secondary mt-2"><strong>Pipeline:</strong> ${escapeHtml(item.automated_validation_summary)}</div>`
|
|
: "";
|
|
const ownerMarkup = item?.owner_name
|
|
? `<div class="small text-secondary mt-2"><strong>Owner:</strong> ${escapeHtml(item.owner_name)}</div>`
|
|
: "";
|
|
const queuedAt = item?.queued_at
|
|
? `<div class="small text-secondary mt-2"><strong>Atualizado:</strong> ${escapeHtml(formatDateTime(item.queued_at))}</div>`
|
|
: "";
|
|
return `<article class="admin-tool-review-card rounded-4 p-4 ${isSelected ? "border border-dark" : ""}"><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(gatePresentation.label)}</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-2">${escapeHtml(item.summary || payload?.message || "Item aguardando analise do time.")}</p><div class="small text-secondary">${escapeHtml(gatePresentation.description)}</div>${validationSummary}${ownerMarkup}${queuedAt}<div class="pt-3"><button class="btn btn-sm ${isSelected ? "btn-dark" : "btn-outline-dark"} rounded-pill" type="button" data-tool-review-select="true" data-version-id="${escapeHtml(item.version_id || "")}">${isSelected ? "Versao selecionada" : "Abrir detalhe"}</button></div></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 renderReviewQueueSelection(versionId) {
|
|
const normalizedVersionId = String(versionId || "").trim();
|
|
queueList.querySelectorAll("[data-tool-review-select]").forEach((button) => {
|
|
if (!(button instanceof HTMLButtonElement)) {
|
|
return;
|
|
}
|
|
const isSelected = String(button.dataset.versionId || "") === normalizedVersionId;
|
|
button.classList.toggle("btn-dark", isSelected);
|
|
button.classList.toggle("btn-outline-dark", !isSelected);
|
|
button.textContent = isSelected ? "Versao selecionada" : "Abrir detalhe";
|
|
});
|
|
queueList.querySelectorAll(".admin-tool-review-card").forEach((card) => {
|
|
if (!(card instanceof HTMLElement)) {
|
|
return;
|
|
}
|
|
const cardButton = card.querySelector("[data-tool-review-select]");
|
|
const isSelected = cardButton instanceof HTMLElement && String(cardButton.dataset.versionId || "") === normalizedVersionId;
|
|
card.classList.toggle("border", isSelected);
|
|
card.classList.toggle("border-dark", isSelected);
|
|
});
|
|
}
|
|
|
|
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) => {
|
|
const manageButton = item?.version_id
|
|
? `<div class="pt-3"><button class="btn btn-sm btn-outline-dark rounded-pill" type="button" data-tool-publication-select="true" data-version-id="${escapeHtml(item.version_id || "")}">Abrir detalhe</button></div>`
|
|
: "";
|
|
const rollbackBadge = item?.rollback_action_available
|
|
? `<span class="badge rounded-pill bg-warning-subtle text-warning-emphasis border border-warning-subtle">Rollback disponivel</span>`
|
|
: "";
|
|
const deactivateBadge = item?.deactivation_action_available
|
|
? `<span class="badge rounded-pill bg-body-tertiary text-secondary border">Desativacao disponivel</span>`
|
|
: "";
|
|
return `<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="d-flex flex-wrap gap-2 mb-3">${deactivateBadge}${rollbackBadge}</div><div class="small text-secondary mb-1"><strong>Status:</strong> ${escapeHtml(item.status || "draft")}</div><div class="small text-secondary mb-1"><strong>Parametros:</strong> ${escapeHtml(String(item.parameter_count || 0))}</div><div class="small text-secondary mb-3"><strong>Autor:</strong> ${escapeHtml(item.author_name || item.published_by || "Nao informado")}</div><div class="small text-secondary">${escapeHtml(item.implementation_module || "")}</div>${manageButton}</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>`;
|
|
}
|
|
|
|
function renderDetailLoading() {
|
|
detailStatus.textContent = "Carregando";
|
|
detailTitle.textContent = "Sincronizando detalhe da versao";
|
|
detailSummary.innerHTML = `<div class="fw-semibold mb-2">Carregando contexto governado</div><p class="text-secondary mb-0">A leitura do detalhe da versao esta em andamento.</p>`;
|
|
detailMeta.innerHTML = `<div class="admin-tool-inline-note rounded-4 p-3 small text-secondary">Carregando metadados persistidos...</div>`;
|
|
validationList.innerHTML = `<div class="admin-tool-inline-note rounded-4 p-3 small text-secondary">Carregando validacoes automaticas...</div>`;
|
|
historyList.innerHTML = `<div class="admin-tool-inline-note rounded-4 p-3 small text-secondary">Carregando historico humano...</div>`;
|
|
nextStepsList.innerHTML = `<div class="admin-tool-inline-note rounded-4 p-3 small text-secondary">Carregando proximos passos...</div>`;
|
|
if (codeField instanceof HTMLTextAreaElement) {
|
|
codeField.value = "Carregando codigo gerado...";
|
|
}
|
|
configureActionPanel(null, false);
|
|
}
|
|
|
|
function renderEmptyDetail(message) {
|
|
detailStatus.textContent = "Nenhum item";
|
|
detailTitle.textContent = "Selecione um item da fila";
|
|
detailSummary.innerHTML = `<div class="fw-semibold mb-2">Revisao humana aguardando selecao</div><p class="text-secondary mb-0">${escapeHtml(message || "Escolha uma versao da fila para abrir o detalhe governado.")}</p>`;
|
|
detailMeta.innerHTML = `<div class="admin-tool-inline-note rounded-4 p-3 small text-secondary">Os metadados persistidos e os parametros da versao aparecem aqui.</div>`;
|
|
validationList.innerHTML = `<div class="admin-tool-inline-note rounded-4 p-3 small text-secondary">As validacoes automaticas da pipeline aparecem aqui.</div>`;
|
|
historyList.innerHTML = `<div class="admin-tool-inline-note rounded-4 p-3 small text-secondary">As decisoes humanas de revisao, aprovacao e publicacao aparecem aqui.</div>`;
|
|
nextStepsList.innerHTML = `<div class="admin-tool-inline-note rounded-4 p-3 small text-secondary">Selecione uma versao para visualizar os proximos passos recomendados.</div>`;
|
|
if (codeField instanceof HTMLTextAreaElement) {
|
|
codeField.value = "O codigo completo da funcao gerada aparecera aqui assim que uma versao for selecionada.";
|
|
}
|
|
if (decisionNotes instanceof HTMLTextAreaElement) {
|
|
decisionNotes.value = "";
|
|
}
|
|
if (reviewedGeneratedCode instanceof HTMLInputElement) {
|
|
reviewedGeneratedCode.checked = false;
|
|
}
|
|
configureActionPanel(null, false);
|
|
}
|
|
|
|
function renderLockedDetail(message) {
|
|
detailStatus.textContent = "Bloqueado";
|
|
detailTitle.textContent = "Detalhe indisponivel";
|
|
detailSummary.innerHTML = `<div class="fw-semibold mb-2">Leitura protegida</div><p class="text-secondary mb-0">${escapeHtml(message || "A sessao atual nao pode visualizar o detalhe de revisao desta versao.")}</p>`;
|
|
detailMeta.innerHTML = `<div class="admin-tool-inline-note rounded-4 p-3 small text-secondary">Sem acesso aos metadados desta versao.</div>`;
|
|
validationList.innerHTML = `<div class="admin-tool-inline-note rounded-4 p-3 small text-secondary">Sem acesso ao relatorio de validacao automatica.</div>`;
|
|
historyList.innerHTML = `<div class="admin-tool-inline-note rounded-4 p-3 small text-secondary">Sem acesso ao historico de governanca.</div>`;
|
|
nextStepsList.innerHTML = `<div class="admin-tool-inline-note rounded-4 p-3 small text-secondary">Entre com uma sessao com permissao de revisao para continuar.</div>`;
|
|
if (codeField instanceof HTMLTextAreaElement) {
|
|
codeField.value = message || "A leitura do codigo gerado esta protegida pela permissao de revisao.";
|
|
}
|
|
if (decisionNotes instanceof HTMLTextAreaElement) {
|
|
decisionNotes.value = "";
|
|
}
|
|
if (reviewedGeneratedCode instanceof HTMLInputElement) {
|
|
reviewedGeneratedCode.checked = false;
|
|
}
|
|
configureActionPanel(null, false);
|
|
}
|
|
|
|
function renderReviewDetail(payload) {
|
|
const parameters = Array.isArray(payload?.parameters) ? payload.parameters : [];
|
|
const validations = Array.isArray(payload?.automated_validations) ? payload.automated_validations : [];
|
|
const history = Array.isArray(payload?.decision_history) ? payload.decision_history : [];
|
|
const nextSteps = Array.isArray(payload?.next_steps) ? payload.next_steps : [];
|
|
const humanGate = payload?.human_gate || null;
|
|
const generationContext = payload?.generation_context || null;
|
|
const hasSourceCode = Boolean(String(payload?.generated_source_code || "").trim());
|
|
|
|
detailStatus.textContent = payload?.status || "versao";
|
|
detailTitle.innerHTML = `${escapeHtml(payload?.display_name || payload?.tool_name || "Tool")} <span class="small text-secondary">v${escapeHtml(String(payload?.version_number || 1))}</span>`;
|
|
detailSummary.innerHTML = `<div class="fw-semibold mb-2">${escapeHtml(payload?.summary || "Resumo indisponivel")}</div><p class="text-secondary mb-0">${escapeHtml(payload?.description || "Sem descricao detalhada para esta versao.")}</p>`;
|
|
|
|
const parameterMarkup = parameters.length > 0
|
|
? parameters.map((item) => `<div class="admin-tool-inline-note rounded-4 p-3"><div class="d-flex justify-content-between gap-3"><div><div class="fw-semibold">${escapeHtml(item.name || "parametro")}</div><div class="small text-secondary mt-1">${escapeHtml(item.description || "")}</div></div><span class="badge rounded-pill bg-body-tertiary text-secondary border">${escapeHtml(item.parameter_type || "string")}${item.required ? " *" : ""}</span></div></div>`).join("")
|
|
: `<div class="admin-tool-inline-note rounded-4 p-3 small text-secondary">Esta versao nao declarou parametros de entrada.</div>`;
|
|
const latestGenerationMode = describeGenerationMode(generationContext?.latest_generation_mode);
|
|
const nextGenerationMode = describeGenerationMode(generationContext?.next_generation_mode || "initial_generation");
|
|
const changeRequestMarkup = generationContext?.latest_change_request_notes
|
|
? `<div><strong>Ultimo parecer para ajuste:</strong> ${escapeHtml(generationContext.latest_change_request_notes)}</div>`
|
|
: "";
|
|
detailMeta.innerHTML = `
|
|
<div class="admin-tool-inline-note rounded-4 p-3 small text-secondary">
|
|
<div><strong>Tool:</strong> ${escapeHtml(payload?.tool_name || "-")}</div>
|
|
<div><strong>Dominio:</strong> ${escapeHtml(payload?.domain || "-")}</div>
|
|
<div><strong>Owner:</strong> ${escapeHtml(payload?.owner_name || "Nao informado")}</div>
|
|
<div><strong>Gate atual:</strong> ${escapeHtml(humanGate?.current_gate || payload?.queue_entry?.gate || "governance_required")}</div>
|
|
<div><strong>Modulo:</strong> ${escapeHtml(payload?.generated_module || "-")}</div>
|
|
<div><strong>Entrypoint:</strong> ${escapeHtml(payload?.generated_callable || "run")}</div>
|
|
<div><strong>Resumo da pipeline:</strong> ${escapeHtml(payload?.automated_validation_summary || "Sem resumo de validacao automatica.")}</div>
|
|
<div><strong>Ultima iteracao de geracao:</strong> ${escapeHtml(String(generationContext?.latest_generation_iteration || 0))}</div>
|
|
<div><strong>Modo da ultima geracao:</strong> ${escapeHtml(latestGenerationMode)}</div>
|
|
<div><strong>Proxima iteracao prevista:</strong> ${escapeHtml(String(generationContext?.next_generation_iteration || 1))}</div>
|
|
<div><strong>Proxima execucao da pipeline:</strong> ${escapeHtml(nextGenerationMode)}</div>
|
|
<div><strong>Total de iteracoes registradas:</strong> ${escapeHtml(String(generationContext?.generation_iterations_count || 0))}</div>
|
|
${changeRequestMarkup}
|
|
</div>
|
|
${parameterMarkup}
|
|
<div class="admin-tool-inline-note rounded-4 p-3 small text-secondary"><strong>Objetivo de negocio:</strong> ${escapeHtml(payload?.business_goal || "Nao informado")}</div>
|
|
`;
|
|
|
|
validationList.innerHTML = validations.length > 0
|
|
? validations.map((item) => {
|
|
const issues = Array.isArray(item?.blocking_issues) && item.blocking_issues.length > 0
|
|
? `<div class="small text-secondary mt-2"><strong>Bloqueios:</strong> ${escapeHtml(item.blocking_issues.join("; "))}</div>`
|
|
: `<div class="small text-secondary mt-2">Sem bloqueios nesta checagem.</div>`;
|
|
return `<div class="admin-tool-inline-note rounded-4 p-3"><div class="d-flex justify-content-between align-items-start gap-3"><div><div class="fw-semibold">${escapeHtml(item.label || item.key || "Validacao")}</div><div class="small text-secondary mt-1">${escapeHtml(item.summary || "")}</div>${issues}</div><span class="badge rounded-pill ${String(item.status || "").toLowerCase() === "passed" ? "bg-success-subtle text-success-emphasis border border-success-subtle" : "bg-danger-subtle text-danger-emphasis border border-danger-subtle"}">${escapeHtml(item.status || "pendente")}</span></div></div>`;
|
|
}).join("")
|
|
: `<div class="admin-tool-inline-note rounded-4 p-3 small text-secondary">Nenhuma validacao automatica registrada para esta versao.</div>`;
|
|
|
|
historyList.innerHTML = history.length > 0
|
|
? history.map((item) => {
|
|
const statusTransition = item?.previous_status || item?.current_status
|
|
? `<div class="small text-secondary mt-2"><strong>Status:</strong> ${escapeHtml(item.previous_status || "-")} -> ${escapeHtml(item.current_status || "-")}</div>`
|
|
: "";
|
|
const decisionNotesMarkup = item?.decision_notes
|
|
? `<div class="small text-secondary mt-2"><strong>Parecer:</strong> ${escapeHtml(item.decision_notes)}</div>`
|
|
: "";
|
|
const reviewedMarkup = item?.reviewed_generated_code === true
|
|
? `<div class="small text-secondary mt-2"><strong>Codigo revisado:</strong> confirmado</div>`
|
|
: "";
|
|
const actorMarkup = item?.actor_name
|
|
? `<div class="small text-secondary mt-1">${escapeHtml(item.actor_name)}${item?.actor_role ? ` ? ${escapeHtml(item.actor_role)}` : ""}${item?.recorded_at ? ` ? ${escapeHtml(formatDateTime(item.recorded_at))}` : ""}</div>`
|
|
: "";
|
|
return `<div class="admin-tool-inline-note rounded-4 p-3"><div class="fw-semibold">${escapeHtml(item.label || "Governanca registrada")}</div><div class="small text-secondary mt-1">${escapeHtml(item.summary || "")}</div>${actorMarkup}${statusTransition}${decisionNotesMarkup}${reviewedMarkup}</div>`;
|
|
}).join("")
|
|
: `<div class="admin-tool-inline-note rounded-4 p-3 small text-secondary">Nenhuma decisao humana registrada ainda para esta versao.</div>`;
|
|
|
|
nextStepsList.innerHTML = nextSteps.length > 0
|
|
? nextSteps.map((item) => `<div class="admin-tool-inline-note rounded-4 p-3 small text-secondary">${escapeHtml(item)}</div>`).join("")
|
|
: `<div class="admin-tool-inline-note rounded-4 p-3 small text-secondary">Nenhum proximo passo retornado para esta versao.</div>`;
|
|
|
|
if (codeField instanceof HTMLTextAreaElement) {
|
|
codeField.value = hasSourceCode
|
|
? String(payload.generated_source_code)
|
|
: "A pipeline ainda nao registrou o codigo completo gerado para esta versao.";
|
|
}
|
|
if (decisionNotes instanceof HTMLTextAreaElement) {
|
|
decisionNotes.value = "";
|
|
}
|
|
if (reviewedGeneratedCode instanceof HTMLInputElement) {
|
|
reviewedGeneratedCode.checked = false;
|
|
}
|
|
if (decisionHint instanceof HTMLElement) {
|
|
decisionHint.textContent = buildDecisionHint(humanGate, hasSourceCode, generationContext);
|
|
}
|
|
configureActionPanel(humanGate, hasSourceCode);
|
|
}
|
|
|
|
function configureActionPanel(humanGate, hasSourceCode) {
|
|
lastRenderedHumanGate = humanGate;
|
|
lastRenderedHasSourceCode = hasSourceCode;
|
|
configureActionButton(authorizeGenerationButton, Boolean(humanGate?.authorize_generation_action_available));
|
|
configureActionButton(runPipelineButton, Boolean(humanGate?.run_pipeline_action_available));
|
|
configureActionButton(reviewButton, Boolean(humanGate?.review_action_available) && hasSourceCode);
|
|
configureActionButton(requestChangesButton, Boolean(humanGate?.request_changes_action_available) && hasSourceCode);
|
|
configureActionButton(approveButton, Boolean(humanGate?.approval_action_available));
|
|
configureActionButton(publishButton, Boolean(humanGate?.publication_action_available));
|
|
configureActionButton(closeProposalButton, Boolean(humanGate?.close_proposal_action_available));
|
|
configureActionButton(deactivateButton, Boolean(humanGate?.deactivation_action_available));
|
|
configureActionButton(rollbackButton, Boolean(humanGate?.rollback_action_available));
|
|
|
|
const notesEnabled = Boolean(humanGate?.requires_decision_notes);
|
|
if (decisionNotes instanceof HTMLTextAreaElement) {
|
|
decisionNotes.disabled = !notesEnabled;
|
|
if (!notesEnabled) {
|
|
decisionNotes.value = "";
|
|
}
|
|
}
|
|
if (reviewedGeneratedCode instanceof HTMLInputElement) {
|
|
reviewedGeneratedCode.disabled = !Boolean(humanGate?.requires_code_review_confirmation);
|
|
if (reviewedGeneratedCode.disabled) {
|
|
reviewedGeneratedCode.checked = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
function configureActionButton(button, isEnabled) {
|
|
if (!(button instanceof HTMLButtonElement)) {
|
|
return;
|
|
}
|
|
const defaultLabel = button.dataset.defaultLabel || button.textContent || "";
|
|
button.textContent = defaultLabel;
|
|
button.disabled = !isEnabled;
|
|
}
|
|
|
|
function buildDecisionHint(humanGate, hasSourceCode, generationContext) {
|
|
if (!humanGate) {
|
|
return "As notas da decisao ficam persistidas na trilha administrativa da versao.";
|
|
}
|
|
if (humanGate.authorize_generation_action_available) {
|
|
return "A proposta ainda esta em draft. Registre um parecer e autorize a geracao somente quando fizer sentido consumir codigo.";
|
|
}
|
|
if (humanGate.run_pipeline_action_available) {
|
|
return `A proposta ja pode seguir para a pipeline. A proxima execucao prevista e ${escapeHtml(describeGenerationMode(generationContext?.next_generation_mode || "initial_generation")).toLowerCase()} na iteracao ${escapeHtml(String(generationContext?.next_generation_iteration || 1))}.`;
|
|
}
|
|
if (humanGate.current_gate === "generation_pipeline_required") {
|
|
return `A triagem humana ja autorizou a pipeline. A proxima execucao prevista e ${escapeHtml(describeGenerationMode(generationContext?.next_generation_mode || "initial_generation")).toLowerCase()} na iteracao ${escapeHtml(String(generationContext?.next_generation_iteration || 1))}.`;
|
|
}
|
|
if (humanGate.current_gate === "changes_requested") {
|
|
return `A diretoria solicitou ajustes. A proxima execucao prevista e ${escapeHtml(describeGenerationMode(generationContext?.next_generation_mode || "change_request_refinement")).toLowerCase()} na iteracao ${escapeHtml(String(generationContext?.next_generation_iteration || 1))}.`;
|
|
}
|
|
if (humanGate.review_action_available && !hasSourceCode) {
|
|
return "A revisao humana fica habilitada assim que o codigo completo gerado estiver disponivel para leitura.";
|
|
}
|
|
if (humanGate.review_action_available) {
|
|
return "Para validar a versao, registre o parecer e confirme explicitamente que o codigo completo foi revisado.";
|
|
}
|
|
if (humanGate.request_changes_action_available) {
|
|
return "Se o codigo ainda nao estiver bom, registre o parecer para solicitar ajustes ou encerre a proposta sem seguir para ativacao.";
|
|
}
|
|
if (humanGate.approval_action_available) {
|
|
return "A aprovacao formal ainda exige um parecer explicito da diretoria antes da publicacao.";
|
|
}
|
|
if (humanGate.publication_action_available) {
|
|
return "A revisao e a aprovacao humanas ja ficaram registradas. Agora a diretoria pode publicar a versao no catalogo.";
|
|
}
|
|
if (humanGate.deactivation_action_available && humanGate.rollback_action_available) {
|
|
return `A versao esta ativa. Registre um parecer para desativar a publicacao ou executar rollback para v${escapeHtml(String(humanGate.rollback_target_version_number || "?"))}.`;
|
|
}
|
|
if (humanGate.deactivation_action_available) {
|
|
return "A versao esta ativa. Registre um parecer para desativar a publicacao ativa com trilha auditavel.";
|
|
}
|
|
return "As notas da decisao ficam persistidas na trilha administrativa da versao.";
|
|
}
|
|
}
|
|
|
|
function mountToolIntakePage(page) {
|
|
const form = page.querySelector('[data-admin-tool-intake-form="true"]');
|
|
const addButton = page.querySelector("[data-add-parameter-row]");
|
|
const parameterList = page.querySelector("[data-parameter-list]");
|
|
const template = page.querySelector("#admin-tool-parameter-row-template");
|
|
const feedback = document.getElementById("admin-tool-intake-feedback");
|
|
const preview = page.querySelector("[data-tool-intake-preview]");
|
|
const storageStatus = page.querySelector("[data-tool-intake-storage-status]");
|
|
const submitLabel = page.querySelector("[data-intake-submit-label]");
|
|
const submitSpinner = page.querySelector("[data-intake-submit-spinner]");
|
|
const submitButton = form?.querySelector('button[type="submit"]');
|
|
|
|
if (!form || !parameterList || !template || !feedback || !preview || !storageStatus || !submitLabel || !submitSpinner || !submitButton) {
|
|
return;
|
|
}
|
|
|
|
if (addButton) {
|
|
addButton.addEventListener("click", () => {
|
|
appendParameterRow();
|
|
});
|
|
}
|
|
|
|
parameterList.addEventListener("click", (event) => {
|
|
const target = event.target;
|
|
if (!(target instanceof HTMLElement) || !target.matches("[data-remove-parameter-row]")) {
|
|
return;
|
|
}
|
|
|
|
const rows = parameterList.querySelectorAll("[data-parameter-row]");
|
|
const row = target.closest("[data-parameter-row]");
|
|
if (!row) {
|
|
return;
|
|
}
|
|
|
|
if (rows.length === 1) {
|
|
clearParameterRow(row);
|
|
return;
|
|
}
|
|
|
|
row.remove();
|
|
});
|
|
|
|
form.addEventListener("submit", async (event) => {
|
|
event.preventDefault();
|
|
toggleSubmitting(true);
|
|
clearFeedback();
|
|
|
|
const payload = buildPayload();
|
|
|
|
try {
|
|
const response = await fetch(page.dataset.intakeEndpoint, {
|
|
method: "POST",
|
|
credentials: "same-origin",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Accept: "application/json",
|
|
},
|
|
body: JSON.stringify(payload),
|
|
});
|
|
const body = await readJson(response);
|
|
if (!response.ok) {
|
|
throw new Error(body?.detail || "Nao foi possivel validar o pre-cadastro da tool.");
|
|
}
|
|
|
|
renderDraftPreview(body);
|
|
showFeedback("success", body?.message || "Pre-cadastro validado com sucesso.");
|
|
} catch (error) {
|
|
storageStatus.textContent = "Falha";
|
|
showFeedback("danger", error instanceof Error ? error.message : "Erro inesperado ao validar a nova tool.");
|
|
} finally {
|
|
toggleSubmitting(false);
|
|
}
|
|
});
|
|
|
|
function appendParameterRow() {
|
|
const fragment = template.content.cloneNode(true);
|
|
parameterList.appendChild(fragment);
|
|
}
|
|
|
|
function buildPayload() {
|
|
const formData = new FormData(form);
|
|
const parameters = Array.from(parameterList.querySelectorAll("[data-parameter-row]"))
|
|
.map((row) => {
|
|
const name = row.querySelector('[name="parameter_name"]')?.value?.trim() || "";
|
|
const parameterType = row.querySelector('[name="parameter_type"]')?.value || "string";
|
|
const description = row.querySelector('[name="parameter_description"]')?.value?.trim() || "";
|
|
const required = Boolean(row.querySelector('[name="parameter_required"]')?.checked);
|
|
return { name, parameter_type: parameterType, description, required };
|
|
})
|
|
.filter((item) => item.name || item.description);
|
|
|
|
return {
|
|
domain: String(formData.get("domain") || "").trim(),
|
|
tool_name: String(formData.get("tool_name") || "").trim(),
|
|
display_name: String(formData.get("display_name") || "").trim(),
|
|
description: String(formData.get("description") || "").trim(),
|
|
business_goal: String(formData.get("business_goal") || "").trim(),
|
|
parameters,
|
|
};
|
|
}
|
|
|
|
function clearParameterRow(row) {
|
|
row.querySelectorAll("input").forEach((input) => {
|
|
if (input.type === "checkbox") {
|
|
input.checked = true;
|
|
return;
|
|
}
|
|
input.value = "";
|
|
});
|
|
const select = row.querySelector('select[name="parameter_type"]');
|
|
if (select) {
|
|
select.value = "string";
|
|
}
|
|
}
|
|
|
|
function toggleSubmitting(isLoading) {
|
|
submitButton.disabled = isLoading;
|
|
submitSpinner.classList.toggle("d-none", !isLoading);
|
|
submitLabel.textContent = isLoading ? "Criando..." : "Criar proposta";
|
|
}
|
|
|
|
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 renderDraftPreview(payload) {
|
|
const draft = payload?.draft_preview;
|
|
const submissionPolicy = payload?.submission_policy || null;
|
|
const warnings = Array.isArray(payload?.warnings) ? payload.warnings : [];
|
|
const nextSteps = Array.isArray(payload?.next_steps) ? payload.next_steps : [];
|
|
const parameters = Array.isArray(draft?.parameters) ? draft.parameters : [];
|
|
|
|
storageStatus.textContent = payload?.storage_status || "Validado";
|
|
preview.innerHTML = `
|
|
<article class="admin-tool-preview-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(draft?.domain || "tool")}</div>
|
|
<h4 class="h4 fw-semibold mb-1">${escapeHtml(draft?.display_name || "Nova tool")}</h4>
|
|
<div class="small text-secondary">${escapeHtml(draft?.tool_name || "")}</div>
|
|
</div>
|
|
<span class="badge rounded-pill bg-info-subtle text-info-emphasis border border-info-subtle">${escapeHtml(draft?.status || "draft")}</span>
|
|
</div>
|
|
<p class="text-secondary mb-3">${escapeHtml(draft?.summary || "")}</p>
|
|
<div class="admin-tool-preview-meta small text-secondary mb-3">
|
|
<div><strong>Objetivo:</strong> ${escapeHtml(draft?.business_goal || "")}</div>
|
|
<div><strong>Versao atual:</strong> v${escapeHtml(String(draft?.version_number || 1))}</div>
|
|
<div><strong>Historico:</strong> ${escapeHtml(String(draft?.version_count || 1))} versao(oes)</div>
|
|
<div><strong>Parametros:</strong> ${escapeHtml(String(draft?.parameter_count || 0))}</div>
|
|
<div><strong>Obrigatorios:</strong> ${escapeHtml(String(draft?.required_parameter_count || 0))}</div>
|
|
<div><strong>Aprovacao:</strong> ${draft?.requires_director_approval ? "Diretor obrigatorio" : "Nao"}</div>
|
|
</div>
|
|
${submissionPolicy
|
|
? `<div class="admin-tool-inline-note rounded-4 p-3 mb-3"><div class="fw-semibold mb-1">Governanca desta proposta</div><div class="small text-secondary">Modo: ${escapeHtml(submissionPolicy.mode || "draft_only")}. Papel atual: ${escapeHtml(submissionPolicy.submitter_role || "nao informado")}. Esta tela apenas cria o draft administrativo; a geracao de codigo continua bloqueada ate a triagem humana. Permissao final de publicacao: ${escapeHtml(submissionPolicy.required_publish_permission || "publish_tools")}.<\/div><\/div>`
|
|
: ""}
|
|
<div class="vstack gap-2 mb-3">
|
|
${parameters.length > 0
|
|
? parameters.map((item) => `<div class="admin-tool-inline-note rounded-4 p-3"><div class="d-flex justify-content-between gap-3"><div><div class="fw-semibold">${escapeHtml(item.name)}</div><div class="small text-secondary mt-1">${escapeHtml(item.description)}</div></div><span class="badge rounded-pill bg-body-tertiary text-secondary border">${escapeHtml(item.parameter_type)}</span></div></div>`).join("")
|
|
: `<div class="admin-tool-inline-note rounded-4 p-3"><div class="fw-semibold">Sem parametros</div><div class="small text-secondary mt-1">A tool foi cadastrada sem parametros de entrada.</div></div>`}
|
|
</div>
|
|
<div class="admin-tool-preview-stack vstack gap-3">
|
|
<div>
|
|
<div class="small text-uppercase fw-semibold text-secondary mb-2">Avisos</div>
|
|
${warnings.length > 0
|
|
? `<ul class="small text-secondary ps-3 mb-0">${warnings.map((item) => `<li class="mb-2">${escapeHtml(item)}</li>`).join("")}</ul>`
|
|
: `<div class="small text-secondary">Nenhum aviso extra para esta proposta.</div>`}
|
|
</div>
|
|
<div>
|
|
<div class="small text-uppercase fw-semibold text-secondary mb-2">Proximos passos</div>
|
|
${nextSteps.length > 0
|
|
? `<ul class="small text-secondary ps-3 mb-0">${nextSteps.map((item) => `<li class="mb-2">${escapeHtml(item)}</li>`).join("")}</ul>`
|
|
: `<div class="small text-secondary">Sem orientacoes adicionais.</div>`}
|
|
</div>
|
|
</div>
|
|
</article>
|
|
`;
|
|
}
|
|
}
|
|
|
|
function mountCollaboratorBoard(board) {
|
|
const form = board.querySelector('[data-admin-collaborator-form="true"]');
|
|
const feedback = document.getElementById("admin-collaborator-feedback");
|
|
const list = board.querySelector("[data-collaborator-list]");
|
|
const refreshButton = board.querySelector("[data-admin-collaborator-refresh]");
|
|
const refreshLabel = board.querySelector("[data-collaborator-refresh-label]");
|
|
const refreshSpinner = board.querySelector("[data-collaborator-refresh-spinner]");
|
|
const submitButton = form?.querySelector('button[type="submit"]');
|
|
const submitLabel = form?.querySelector("[data-collaborator-submit-label]");
|
|
const submitSpinner = form?.querySelector("[data-collaborator-submit-spinner]");
|
|
|
|
if (!form || !feedback || !list || !refreshButton || !refreshLabel || !refreshSpinner || !submitButton || !submitLabel || !submitSpinner) {
|
|
return;
|
|
}
|
|
|
|
refreshButton.addEventListener("click", () => {
|
|
void loadCollaborators();
|
|
});
|
|
|
|
form.addEventListener("submit", async (event) => {
|
|
event.preventDefault();
|
|
toggleSubmitting(true);
|
|
clearFeedback();
|
|
|
|
const formData = new FormData(form);
|
|
const payload = {
|
|
display_name: String(formData.get("display_name") || "").trim(),
|
|
email: String(formData.get("email") || "").trim(),
|
|
password: String(formData.get("password") || ""),
|
|
is_active: Boolean(formData.get("is_active")),
|
|
};
|
|
|
|
try {
|
|
const response = await fetch(board.dataset.collaboratorCollectionEndpoint, {
|
|
method: "POST",
|
|
credentials: "same-origin",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Accept: "application/json",
|
|
},
|
|
body: JSON.stringify(payload),
|
|
});
|
|
const body = await readJson(response);
|
|
if (!response.ok) {
|
|
throw new Error(body?.detail || "Nao foi possivel criar o colaborador.");
|
|
}
|
|
|
|
showFeedback("success", body?.message || "Colaborador criado com sucesso.");
|
|
form.reset();
|
|
const activeField = form.querySelector('[name="is_active"]');
|
|
if (activeField instanceof HTMLInputElement) {
|
|
activeField.checked = true;
|
|
}
|
|
await loadCollaborators();
|
|
} catch (error) {
|
|
showFeedback("danger", error instanceof Error ? error.message : "Erro inesperado ao cadastrar colaborador.");
|
|
} finally {
|
|
toggleSubmitting(false);
|
|
}
|
|
});
|
|
|
|
list.addEventListener("click", async (event) => {
|
|
const target = event.target;
|
|
if (!(target instanceof HTMLElement) || !target.matches("[data-collaborator-toggle]")) {
|
|
return;
|
|
}
|
|
|
|
const collaboratorId = target.dataset.collaboratorId;
|
|
const nextState = target.dataset.collaboratorNextState === "true";
|
|
if (!collaboratorId) {
|
|
return;
|
|
}
|
|
|
|
toggleRefreshing(true);
|
|
clearFeedback();
|
|
|
|
try {
|
|
const response = await fetch(`${board.dataset.collaboratorCollectionEndpoint}/${collaboratorId}/status`, {
|
|
method: "PATCH",
|
|
credentials: "same-origin",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Accept: "application/json",
|
|
},
|
|
body: JSON.stringify({ is_active: nextState }),
|
|
});
|
|
const body = await readJson(response);
|
|
if (!response.ok) {
|
|
throw new Error(body?.detail || "Nao foi possivel atualizar o status do colaborador.");
|
|
}
|
|
|
|
showFeedback("success", body?.message || "Status do colaborador atualizado com sucesso.");
|
|
await loadCollaborators();
|
|
} catch (error) {
|
|
showFeedback("danger", error instanceof Error ? error.message : "Erro inesperado ao atualizar o colaborador.");
|
|
toggleRefreshing(false);
|
|
}
|
|
});
|
|
|
|
void loadCollaborators();
|
|
|
|
async function loadCollaborators() {
|
|
toggleRefreshing(true);
|
|
const result = await fetchPanelJson(board.dataset.collaboratorCollectionEndpoint);
|
|
|
|
if (result.ok) {
|
|
renderCollaborators(result.body);
|
|
} else {
|
|
list.innerHTML = `<div class="admin-tool-empty-state rounded-4 p-4"><h4 class="h5 fw-semibold mb-2">Leitura indisponivel</h4><p class="text-secondary mb-0">${escapeHtml(result.message || "Nao foi possivel carregar os colaboradores.")}</p></div>`;
|
|
setText("[data-collaborator-total]", "0");
|
|
setText("[data-collaborator-active-count]", "0");
|
|
setText("[data-collaborator-inactive-count]", "0");
|
|
showFeedback("warning", result.message || "A sessao atual nao pode consultar os colaboradores.");
|
|
}
|
|
|
|
toggleRefreshing(false);
|
|
}
|
|
|
|
function renderCollaborators(payload) {
|
|
const collaborators = Array.isArray(payload?.collaborators) ? payload.collaborators : [];
|
|
setText("[data-collaborator-total]", String(payload?.total || 0));
|
|
setText("[data-collaborator-active-count]", String(payload?.active_count || 0));
|
|
setText("[data-collaborator-inactive-count]", String(payload?.inactive_count || 0));
|
|
|
|
list.innerHTML = collaborators.length > 0
|
|
? collaborators.map((item) => {
|
|
const statusBadge = item?.is_active
|
|
? "bg-success-subtle text-success-emphasis border border-success-subtle"
|
|
: "bg-secondary-subtle text-secondary-emphasis border border-secondary-subtle";
|
|
const actionLabel = item?.is_active ? "Desativar acesso" : "Reativar acesso";
|
|
const buttonClass = item?.is_active ? "btn-outline-secondary" : "btn-outline-dark";
|
|
const lastLogin = item?.last_login_at ? formatDateTime(item.last_login_at) : "Ainda nao acessou";
|
|
return `<article class="admin-collaborator-card rounded-4 p-4"><div class="d-flex flex-wrap justify-content-between align-items-start gap-3 mb-3"><div><div class="small text-uppercase fw-semibold text-secondary mb-2">${escapeHtml(item?.role || "colaborador")}</div><h4 class="h5 fw-semibold mb-1">${escapeHtml(item?.display_name || "Colaborador")}</h4><div class="small text-secondary">${escapeHtml(item?.email || "")}</div></div><span class="badge rounded-pill ${statusBadge}">${item?.is_active ? "Ativo" : "Inativo"}</span></div><div class="small text-secondary admin-collaborator-meta mb-3"><div><strong>Ultimo login:</strong> ${escapeHtml(lastLogin)}</div><div><strong>ID:</strong> ${escapeHtml(String(item?.id || "-"))}</div></div><button class="btn btn-sm ${buttonClass} rounded-pill px-3" type="button" data-collaborator-toggle="true" data-collaborator-id="${escapeHtml(String(item?.id || ""))}" data-collaborator-next-state="${item?.is_active ? "false" : "true"}">${actionLabel}</button></article>`;
|
|
}).join("")
|
|
: `<div class="admin-tool-empty-state rounded-4 p-4"><h4 class="h5 fw-semibold mb-2">Nenhum colaborador cadastrado ainda</h4><p class="text-secondary mb-0">Use o formulario ao lado para criar o primeiro colaborador administrativo.</p></div>`;
|
|
}
|
|
|
|
function toggleSubmitting(isLoading) {
|
|
submitButton.disabled = isLoading;
|
|
submitSpinner.classList.toggle("d-none", !isLoading);
|
|
submitLabel.textContent = isLoading ? "Criando..." : "Criar colaborador";
|
|
}
|
|
|
|
function toggleRefreshing(isLoading) {
|
|
refreshButton.disabled = isLoading;
|
|
refreshSpinner.classList.toggle("d-none", !isLoading);
|
|
refreshLabel.textContent = isLoading ? "Atualizando..." : "Atualizar lista";
|
|
}
|
|
|
|
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 mountSystemConfigurationPage(page) {
|
|
const refreshButton = page.querySelector("[data-admin-system-refresh]");
|
|
const refreshLabel = page.querySelector("[data-system-refresh-label]");
|
|
const refreshSpinner = page.querySelector("[data-system-refresh-spinner]");
|
|
const feedback = document.getElementById("admin-system-configuration-feedback");
|
|
const functionalList = page.querySelector("[data-system-functional-list]");
|
|
const parentKeys = page.querySelector("[data-system-parent-keys]");
|
|
const botSettingsList = page.querySelector("[data-system-bot-settings-list]");
|
|
const runtimeSummary = page.querySelector("[data-system-runtime-summary]");
|
|
const securitySummary = page.querySelector("[data-system-security-summary]");
|
|
const modelRuntimeSummary = page.querySelector("[data-system-model-runtime-summary]");
|
|
const sourceList = page.querySelector("[data-system-source-list]");
|
|
|
|
if (!refreshButton || !refreshLabel || !refreshSpinner || !feedback || !functionalList || !parentKeys || !botSettingsList || !runtimeSummary || !securitySummary || !modelRuntimeSummary || !sourceList) {
|
|
return;
|
|
}
|
|
|
|
refreshButton.addEventListener("click", () => {
|
|
void loadConfiguration();
|
|
});
|
|
|
|
void loadConfiguration();
|
|
|
|
async function loadConfiguration() {
|
|
toggleRefreshing(true);
|
|
clearFeedback();
|
|
|
|
const [overviewResult, runtimeResult, securityResult, modelRuntimesResult, functionalResult, botGovernanceResult] = await Promise.all([
|
|
fetchPanelJson(page.dataset.overviewEndpoint),
|
|
fetchPanelJson(page.dataset.runtimeEndpoint),
|
|
fetchPanelJson(page.dataset.securityEndpoint),
|
|
fetchPanelJson(page.dataset.modelRuntimesEndpoint),
|
|
fetchPanelJson(page.dataset.functionalEndpoint),
|
|
fetchPanelJson(page.dataset.botGovernanceEndpoint),
|
|
]);
|
|
|
|
if (functionalResult.ok) {
|
|
renderFunctionalCatalog(functionalResult.body);
|
|
} else {
|
|
renderLockedState(functionalList, "Catalogo funcional indisponivel", functionalResult.message || "Nao foi possivel carregar o catalogo funcional.");
|
|
setText("[data-system-config-count]", "0");
|
|
setText("[data-system-functional-mode]", "Bloqueado");
|
|
}
|
|
|
|
if (botGovernanceResult.ok) {
|
|
renderBotGovernance(botGovernanceResult.body);
|
|
} else {
|
|
parentKeys.innerHTML = '<span class="badge rounded-pill bg-body-tertiary text-secondary border">Bloqueado</span>';
|
|
renderLockedState(botSettingsList, "Governanca do bot indisponivel", botGovernanceResult.message || "Nao foi possivel carregar os campos governados pelo bot.");
|
|
setText("[data-system-bot-setting-count]", "0");
|
|
}
|
|
|
|
if (runtimeResult.ok) {
|
|
renderRuntime(runtimeResult.body);
|
|
} else {
|
|
renderLockedState(runtimeSummary, "Runtime protegido", runtimeResult.message || "A sessao atual nao pode ler o runtime administrativo.");
|
|
}
|
|
|
|
if (securityResult.ok) {
|
|
renderSecurity(securityResult.body);
|
|
} else {
|
|
renderLockedState(securitySummary, "Seguranca protegida", securityResult.message || "A sessao atual nao pode ler o snapshot de seguranca.");
|
|
}
|
|
|
|
if (modelRuntimesResult.ok) {
|
|
renderModelRuntimes(modelRuntimesResult.body);
|
|
} else {
|
|
renderLockedState(modelRuntimeSummary, "Separacao tecnica protegida", modelRuntimesResult.message || "A sessao atual nao pode ler os perfis de runtime.");
|
|
setText("[data-system-runtime-profile-count]", "0");
|
|
}
|
|
|
|
if (overviewResult.ok) {
|
|
renderSources(overviewResult.body);
|
|
} else {
|
|
renderLockedState(sourceList, "Overview tecnico protegido", overviewResult.message || "A sessao atual nao pode ler as fontes do snapshot.");
|
|
setText("[data-system-source-count]", "0");
|
|
}
|
|
|
|
const directorOnlyLocked = [overviewResult, runtimeResult, securityResult, modelRuntimesResult].some((result) => !result.ok);
|
|
if (functionalResult.ok && botGovernanceResult.ok && directorOnlyLocked) {
|
|
showFeedback("info", "A sessao atual consegue consultar a configuracao funcional do sistema. Blocos de runtime, seguranca e separacao tecnica exigem manage_settings.");
|
|
} else if (functionalResult.ok && botGovernanceResult.ok) {
|
|
showFeedback("success", "Snapshot de configuracoes do sistema carregado com sucesso.");
|
|
} else {
|
|
showFeedback("warning", "A tela nao conseguiu carregar todas as superficies de configuracao com a sessao atual.");
|
|
}
|
|
|
|
setText("[data-system-last-sync]", formatNow());
|
|
toggleRefreshing(false);
|
|
}
|
|
|
|
function renderFunctionalCatalog(payload) {
|
|
const configurations = Array.isArray(payload?.configurations) ? payload.configurations : [];
|
|
setText("[data-system-config-count]", String(configurations.length));
|
|
setText("[data-system-functional-mode]", formatModeLabel(payload?.mode));
|
|
|
|
functionalList.innerHTML = configurations.length > 0
|
|
? configurations.map((item) => {
|
|
const writableCount = Array.isArray(item?.fields)
|
|
? item.fields.filter((field) => field?.writable).length
|
|
: 0;
|
|
const editingLabel = writableCount > 0 ? `${writableCount} campo(s) ajustavel(is)` : "Somente leitura";
|
|
const impactLabel = item?.affects_product_runtime ? "Impacta o atendimento" : "Uso interno do admin";
|
|
return `<article class="admin-system-item rounded-4 p-4"><div class="d-flex flex-wrap justify-content-between align-items-start gap-3 mb-3"><div><div class="small text-uppercase fw-semibold text-secondary mb-2">${escapeHtml(formatDomainLabel(item?.domain || "sistema"))}</div><h4 class="h5 fw-semibold mb-1">${escapeHtml(formatConfigTitle(item?.config_key || "configuracao"))}</h4><div class="small text-secondary">${escapeHtml(item?.description || "")}</div></div><span class="badge rounded-pill bg-body-tertiary text-secondary border">${escapeHtml(formatMutabilityLabel(item?.mutability || "readonly"))}</span></div><div class="admin-system-meta small text-secondary"><div><strong>Campos visiveis:</strong> ${escapeHtml(String(Array.isArray(item?.fields) ? item.fields.length : 0))}</div><div><strong>Ajustes nesta fase:</strong> ${escapeHtml(editingLabel)}</div><div><strong>Impacto:</strong> ${escapeHtml(impactLabel)}</div></div></article>`;
|
|
}).join("")
|
|
: `<div class="admin-tool-empty-state rounded-4 p-4"><h4 class="h5 fw-semibold mb-2">Nenhuma configuracao encontrada</h4><p class="text-secondary mb-0">O catalogo funcional nao retornou itens nesta leitura.</p></div>`;
|
|
}
|
|
|
|
function renderBotGovernance(payload) {
|
|
const settings = Array.isArray(payload?.settings) ? payload.settings : [];
|
|
const parentConfigKeys = Array.isArray(payload?.parent_config_keys) ? payload.parent_config_keys : [];
|
|
setText("[data-system-bot-setting-count]", String(settings.length));
|
|
|
|
parentKeys.innerHTML = parentConfigKeys.length > 0
|
|
? parentConfigKeys.map((item) => `<span class="badge rounded-pill bg-body-tertiary text-secondary border">${escapeHtml(item)}</span>`).join("")
|
|
: '<span class="badge rounded-pill bg-body-tertiary text-secondary border">Sem parent keys</span>';
|
|
|
|
botSettingsList.innerHTML = settings.length > 0
|
|
? settings.slice(0, 12).map((item) => `<article class="admin-system-item rounded-4 p-4"><div class="d-flex flex-wrap justify-content-between align-items-start gap-3 mb-3"><div><div class="small text-uppercase fw-semibold text-secondary mb-2">${escapeHtml(formatDomainLabel(item?.area || "bot"))}</div><h4 class="h5 fw-semibold mb-1">${escapeHtml(humanizeKey(item?.setting_key || "setting"))}</h4><div class="small text-secondary">${escapeHtml(item?.description || "")}</div></div><span class="badge rounded-pill bg-body-tertiary text-secondary border">${escapeHtml(formatMutabilityLabel(item?.mutability || "versioned"))}</span></div><div class="admin-system-meta small text-secondary"><div><strong>Grupo:</strong> ${escapeHtml(formatConfigTitle(item?.parent_config_key || "-"))}</div><div><strong>Escrita direta:</strong> ${item?.direct_product_write_allowed ? 'Permitida' : 'Bloqueada'}</div></div></article>`).join("")
|
|
: `<div class="admin-tool-empty-state rounded-4 p-4"><h4 class="h5 fw-semibold mb-2">Nenhum campo governado encontrado</h4><p class="text-secondary mb-0">A governanca do bot nao retornou itens nesta leitura.</p></div>`;
|
|
}
|
|
|
|
function renderRuntime(payload) {
|
|
const runtime = payload?.runtime;
|
|
if (!runtime) {
|
|
renderLockedState(runtimeSummary, "Runtime indisponivel", "Nao foi possivel interpretar a resposta do runtime.");
|
|
return;
|
|
}
|
|
|
|
runtimeSummary.innerHTML = `<div class="admin-system-stack"><div class="admin-system-item rounded-4 p-4"><div class="small text-uppercase fw-semibold text-secondary mb-2">Aplicacao</div><div class="admin-system-meta small text-secondary"><div><strong>Nome:</strong> ${escapeHtml(runtime?.application?.app_name || "-")}</div><div><strong>Ambiente:</strong> ${escapeHtml(runtime?.application?.environment || "-")}</div><div><strong>Versao:</strong> ${escapeHtml(runtime?.application?.version || "-")}</div><div><strong>Modo debug:</strong> ${runtime?.application?.debug ? 'Ativo' : 'Desligado'}</div></div><p class="small text-secondary mb-0 mt-3">Detalhes internos de infraestrutura e cookies nao aparecem aqui para manter a tela mais limpa.</p></div></div>`;
|
|
}
|
|
|
|
function renderSecurity(payload) {
|
|
const security = payload?.security;
|
|
if (!security) {
|
|
renderLockedState(securitySummary, "Seguranca indisponivel", "Nao foi possivel interpretar a resposta de seguranca.");
|
|
return;
|
|
}
|
|
|
|
securitySummary.innerHTML = `<div class="admin-system-stack"><div class="admin-system-item rounded-4 p-4"><div class="small text-uppercase fw-semibold text-secondary mb-2">Senha e sessao</div><div class="admin-system-meta small text-secondary"><div><strong>Tamanho minimo:</strong> ${escapeHtml(String(security?.password?.min_length || 0))} caracteres</div><div><strong>Requisitos:</strong> ${renderPasswordRequirements(security?.password)}</div><div><strong>Acesso expira em:</strong> ${escapeHtml(String(security?.tokens?.access_token_ttl_minutes || 0))} min</div><div><strong>Renovacao disponivel por:</strong> ${escapeHtml(String(security?.tokens?.refresh_token_ttl_days || 0))} dias</div></div><p class="small text-secondary mb-0 mt-3">Informacoes internas de assinatura e bootstrap ficam fora desta tela para reduzir ruido.</p></div></div>`;
|
|
}
|
|
|
|
function renderModelRuntimes(payload) {
|
|
const modelRuntimes = payload?.model_runtimes;
|
|
const profiles = Array.isArray(modelRuntimes?.runtime_profiles) ? modelRuntimes.runtime_profiles : [];
|
|
const separationRules = Array.isArray(modelRuntimes?.separation_rules) ? modelRuntimes.separation_rules : [];
|
|
setText("[data-system-runtime-profile-count]", String(profiles.length));
|
|
|
|
modelRuntimeSummary.innerHTML = profiles.length > 0
|
|
? `<div class="admin-system-stack"><div class="admin-system-item rounded-4 p-4"><div class="small text-uppercase fw-semibold text-secondary mb-3">Regras principais</div><ul class="small text-secondary ps-3 mb-0">${separationRules.map((rule) => `<li class="mb-2">${escapeHtml(rule)}</li>`).join("")}</ul></div>${profiles.map((profile) => `<article class="admin-system-item rounded-4 p-4"><div class="d-flex flex-wrap justify-content-between align-items-start gap-3 mb-3"><div><div class="small text-uppercase fw-semibold text-secondary mb-2">${escapeHtml(formatRuntimeTargetLabel(profile?.runtime_target || "runtime"))}</div><h4 class="h5 fw-semibold mb-1">${escapeHtml(formatConfigTitle(profile?.config_key || "perfil"))}</h4><div class="small text-secondary">${escapeHtml(profile?.description || "")}</div></div><span class="badge rounded-pill bg-body-tertiary text-secondary border">${profile?.affects_customer_response ? 'Atendimento' : 'Interno'}</span></div><div class="admin-system-meta small text-secondary"><div><strong>Servico:</strong> ${escapeHtml(humanizeKey(profile?.consumed_by_service || "-"))}</div><div><strong>Uso principal:</strong> ${escapeHtml(formatPurposeLabel(profile?.purpose || "-"))}</div><div><strong>Gera tools:</strong> ${profile?.can_generate_code ? 'Sim' : 'Nao'}</div><div><strong>Rollback separado:</strong> ${profile?.rollback_independently ? 'Sim' : 'Nao'}</div></div></article>`).join("")}</div>`
|
|
: `<div class="admin-tool-empty-state rounded-4 p-4"><h4 class="h5 fw-semibold mb-2">Nenhum perfil retornado</h4><p class="text-secondary mb-0">A separacao de runtime nao retornou perfis nesta leitura.</p></div>`;
|
|
}
|
|
|
|
function renderSources(payload) {
|
|
const sources = Array.isArray(payload?.sources) ? payload.sources : [];
|
|
setText("[data-system-source-count]", String(sources.length));
|
|
|
|
sourceList.innerHTML = sources.length > 0
|
|
? sources.map((item) => `<article class="admin-system-item rounded-4 p-4"><div class="d-flex flex-wrap justify-content-between align-items-start gap-3 mb-3"><div><div class="small text-uppercase fw-semibold text-secondary mb-2">${escapeHtml(formatSourceLabel(item?.source || "origem"))}</div><h4 class="h5 fw-semibold mb-1">${escapeHtml(formatConfigTitle(item?.key || "configuracao"))}</h4><div class="small text-secondary">${escapeHtml(item?.description || "")}</div></div><span class="badge rounded-pill ${item?.mutable ? 'bg-warning-subtle text-warning-emphasis border border-warning-subtle' : 'bg-success-subtle text-success-emphasis border border-success-subtle'}">${item?.mutable ? 'Pode mudar' : 'Base fixa'}</span></div></article>`).join("")
|
|
: `<div class="admin-tool-empty-state rounded-4 p-4"><h4 class="h5 fw-semibold mb-2">Nenhuma fonte encontrada</h4><p class="text-secondary mb-0">O overview tecnico nao retornou fontes nesta leitura.</p></div>`;
|
|
}
|
|
|
|
function renderPasswordRequirements(passwordPolicy) {
|
|
const requirements = [];
|
|
if (passwordPolicy?.require_uppercase) requirements.push("letra maiuscula");
|
|
if (passwordPolicy?.require_lowercase) requirements.push("letra minuscula");
|
|
if (passwordPolicy?.require_digit) requirements.push("numero");
|
|
if (passwordPolicy?.require_symbol) requirements.push("simbolo");
|
|
return requirements.length > 0 ? escapeHtml(requirements.join(", ")) : "nenhum requisito adicional";
|
|
}
|
|
|
|
function renderLockedState(container, title, message) {
|
|
container.innerHTML = `<div class="admin-tool-empty-state rounded-4 p-4"><h4 class="h5 fw-semibold mb-2">${escapeHtml(title)}</h4><p class="text-secondary mb-0">${escapeHtml(message)}</p></div>`;
|
|
}
|
|
|
|
function toggleRefreshing(isLoading) {
|
|
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 mountSalesRevenueReportsPage(page) {
|
|
const refreshButton = page.querySelector("[data-admin-commercial-refresh]");
|
|
const refreshLabel = page.querySelector("[data-commercial-refresh-label]");
|
|
const refreshSpinner = page.querySelector("[data-commercial-refresh-spinner]");
|
|
const feedback = document.getElementById("admin-commercial-feedback");
|
|
const salesMetrics = page.querySelector("[data-sales-overview-metrics]");
|
|
const salesMaterialization = page.querySelector("[data-sales-materialization]");
|
|
const salesReportList = page.querySelector("[data-sales-report-list]");
|
|
const salesNextSteps = page.querySelector("[data-sales-next-steps]");
|
|
const revenueMetrics = page.querySelector("[data-revenue-overview-metrics]");
|
|
const revenueMaterialization = page.querySelector("[data-revenue-materialization]");
|
|
const revenueReportList = page.querySelector("[data-revenue-report-list]");
|
|
const revenueNextSteps = page.querySelector("[data-revenue-next-steps]");
|
|
|
|
if (!refreshButton || !refreshLabel || !refreshSpinner || !feedback || !salesMetrics || !salesMaterialization || !salesReportList || !salesNextSteps || !revenueMetrics || !revenueMaterialization || !revenueReportList || !revenueNextSteps) {
|
|
return;
|
|
}
|
|
|
|
refreshButton.addEventListener("click", () => {
|
|
void loadReports();
|
|
});
|
|
|
|
void loadReports();
|
|
|
|
async function loadReports() {
|
|
toggleRefreshing(true);
|
|
clearFeedback();
|
|
|
|
const [salesResult, revenueResult] = await Promise.all([
|
|
fetchPanelJson(page.dataset.salesOverviewEndpoint),
|
|
fetchPanelJson(page.dataset.revenueOverviewEndpoint),
|
|
]);
|
|
|
|
if (salesResult.ok) {
|
|
renderDomainOverview({ kind: "sales", payload: salesResult.body, metricsTarget: salesMetrics, materializationTarget: salesMaterialization, reportsTarget: salesReportList, nextStepsTarget: salesNextSteps });
|
|
} else {
|
|
renderLockedState(salesMetrics, "Vendas indisponivel", salesResult.message || "Nao foi possivel carregar o overview de vendas.");
|
|
salesMaterialization.innerHTML = "";
|
|
salesReportList.innerHTML = "";
|
|
salesNextSteps.innerHTML = "";
|
|
setText("[data-sales-report-count]", "0");
|
|
}
|
|
|
|
if (revenueResult.ok) {
|
|
renderDomainOverview({ kind: "revenue", payload: revenueResult.body, metricsTarget: revenueMetrics, materializationTarget: revenueMaterialization, reportsTarget: revenueReportList, nextStepsTarget: revenueNextSteps });
|
|
} else {
|
|
renderLockedState(revenueMetrics, "Arrecadacao indisponivel", revenueResult.message || "Nao foi possivel carregar o overview de arrecadacao.");
|
|
revenueMaterialization.innerHTML = "";
|
|
revenueReportList.innerHTML = "";
|
|
revenueNextSteps.innerHTML = "";
|
|
setText("[data-revenue-report-count]", "0");
|
|
}
|
|
|
|
if (salesResult.ok && revenueResult.ok) {
|
|
const salesPayload = salesResult.body;
|
|
const revenuePayload = revenueResult.body;
|
|
const datasetCount = uniqueCount(salesPayload?.source_dataset_keys, revenuePayload?.source_dataset_keys);
|
|
const syncStrategy = salesPayload?.materialization?.sync_strategy === revenuePayload?.materialization?.sync_strategy
|
|
? salesPayload?.materialization?.sync_strategy
|
|
: "mixed";
|
|
setText("[data-commercial-dataset-count]", String(datasetCount));
|
|
setText("[data-commercial-sync-strategy]", formatSyncStrategyLabel(syncStrategy || "--"));
|
|
showFeedback("success", "Relatorios de vendas e arrecadacao carregados com sucesso na sessao do painel.");
|
|
} else if (salesResult.ok || revenueResult.ok) {
|
|
const onlyLoaded = salesResult.ok ? "vendas" : "arrecadacao";
|
|
const datasetCount = salesResult.ok
|
|
? (Array.isArray(salesResult.body?.source_dataset_keys) ? salesResult.body.source_dataset_keys.length : 0)
|
|
: (Array.isArray(revenueResult.body?.source_dataset_keys) ? revenueResult.body.source_dataset_keys.length : 0);
|
|
const syncStrategy = salesResult.ok ? salesResult.body?.materialization?.sync_strategy : revenueResult.body?.materialization?.sync_strategy;
|
|
setText("[data-commercial-dataset-count]", String(datasetCount));
|
|
setText("[data-commercial-sync-strategy]", formatSyncStrategyLabel(syncStrategy || "--"));
|
|
showFeedback("warning", `A tela carregou apenas o overview de ${onlyLoaded} com a sessao atual.`);
|
|
} else {
|
|
setText("[data-commercial-dataset-count]", "0");
|
|
setText("[data-commercial-sync-strategy]", "--");
|
|
showFeedback("warning", "Nao foi possivel carregar os relatorios comerciais na sessao atual.");
|
|
}
|
|
|
|
setText("[data-commercial-last-sync]", formatNow());
|
|
toggleRefreshing(false);
|
|
}
|
|
|
|
function renderDomainOverview({ kind, payload, metricsTarget, materializationTarget, reportsTarget, nextStepsTarget }) {
|
|
const reports = Array.isArray(payload?.reports) ? payload.reports : [];
|
|
const metrics = Array.isArray(payload?.metrics) ? payload.metrics : [];
|
|
const nextSteps = Array.isArray(payload?.next_steps) ? payload.next_steps : [];
|
|
const reportCountSelector = kind === "sales" ? "[data-sales-report-count]" : "[data-revenue-report-count]";
|
|
setText(reportCountSelector, String(reports.length));
|
|
|
|
metricsTarget.innerHTML = metrics.length > 0
|
|
? `<div class="admin-commercial-grid">${metrics.map((item) => `<article class="admin-commercial-item rounded-4 p-4"><div class="small text-uppercase fw-semibold text-secondary mb-2">${escapeHtml(item?.label || item?.key || "metrica")}</div><div class="h3 fw-semibold mb-2">${escapeHtml(item?.value || "0")}</div><div class="small text-secondary">${escapeHtml(item?.description || "")}</div></article>`).join("")}</div>`
|
|
: `<div class="admin-tool-empty-state rounded-4 p-4"><h4 class="h5 fw-semibold mb-2">Metricas nao disponiveis</h4><p class="text-secondary mb-0">O overview nao retornou metricas nesta leitura.</p></div>`;
|
|
|
|
materializationTarget.innerHTML = payload?.materialization
|
|
? `<article class="admin-commercial-item rounded-4 p-4"><div class="small text-uppercase fw-semibold text-secondary mb-3">Atualizacao da tela</div><div class="admin-commercial-meta small text-secondary"><div><strong>Ritmo:</strong> ${escapeHtml(formatSyncStrategyLabel(payload?.materialization?.sync_strategy || "-"))}</div><div><strong>Camada:</strong> ${escapeHtml(formatStorageLabel(payload?.materialization?.storage_shape || "-"))}</div><div><strong>Consulta:</strong> ${escapeHtml(formatQuerySurfaceLabel(payload?.materialization?.query_surface || "-"))}</div></div></article>`
|
|
: "";
|
|
|
|
reportsTarget.innerHTML = reports.length > 0
|
|
? reports.map((item) => `<article class="admin-commercial-item rounded-4 p-4"><div class="d-flex flex-wrap justify-content-between align-items-start gap-3 mb-3"><div><h4 class="h5 fw-semibold mb-1">${escapeHtml(item?.label || humanizeKey(item?.report_key || "relatorio"))}</h4><div class="small text-secondary">${escapeHtml(item?.description || "")}</div></div><span class="badge rounded-pill bg-body-tertiary text-secondary border">${escapeHtml(formatGranularityLabel(item?.default_granularity || "aggregate"))}</span></div><div class="admin-commercial-chip-group"><span class="badge rounded-pill bg-body-tertiary text-secondary border">Indicadores: ${escapeHtml(String((item?.supported_metric_keys || []).length))}</span><span class="badge rounded-pill bg-body-tertiary text-secondary border">Recortes: ${escapeHtml(String((item?.supported_dimension_fields || []).length))}</span></div></article>`).join("")
|
|
: `<div class="admin-tool-empty-state rounded-4 p-4"><h4 class="h5 fw-semibold mb-2">Nenhum relatorio previsto</h4><p class="text-secondary mb-0">O overview nao retornou relatorios para este dominio.</p></div>`;
|
|
|
|
nextStepsTarget.innerHTML = nextSteps.length > 0
|
|
? nextSteps.map((item) => `<div class="admin-commercial-item rounded-4 p-3 small text-secondary">${escapeHtml(item)}</div>`).join("")
|
|
: `<div class="admin-tool-empty-state rounded-4 p-4"><h4 class="h5 fw-semibold mb-2">Sem proximos passos</h4><p class="text-secondary mb-0">Nenhuma orientacao adicional foi retornada para este overview.</p></div>`;
|
|
}
|
|
|
|
function renderLockedState(container, title, message) {
|
|
container.innerHTML = `<div class="admin-tool-empty-state rounded-4 p-4"><h4 class="h5 fw-semibold mb-2">${escapeHtml(title)}</h4><p class="text-secondary mb-0">${escapeHtml(message)}</p></div>`;
|
|
}
|
|
|
|
function toggleRefreshing(isLoading) {
|
|
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 mountRentalReportsPage(page) {
|
|
const refreshButton = page.querySelector("[data-admin-rental-refresh]");
|
|
const refreshLabel = page.querySelector("[data-rental-refresh-label]");
|
|
const refreshSpinner = page.querySelector("[data-rental-refresh-spinner]");
|
|
const feedback = document.getElementById("admin-rental-feedback");
|
|
const overviewMetrics = page.querySelector("[data-rental-overview-metrics]");
|
|
const materialization = page.querySelector("[data-rental-materialization]");
|
|
const reportList = page.querySelector("[data-rental-report-list]");
|
|
const nextSteps = page.querySelector("[data-rental-next-steps]");
|
|
|
|
if (!refreshButton || !refreshLabel || !refreshSpinner || !feedback || !overviewMetrics || !materialization || !reportList || !nextSteps) {
|
|
return;
|
|
}
|
|
|
|
refreshButton.addEventListener("click", () => {
|
|
void loadOverview();
|
|
});
|
|
|
|
void loadOverview();
|
|
|
|
async function loadOverview() {
|
|
toggleRefreshing(true);
|
|
clearFeedback();
|
|
|
|
const result = await fetchPanelJson(page.dataset.rentalOverviewEndpoint);
|
|
if (result.ok) {
|
|
const payload = result.body;
|
|
const metrics = Array.isArray(payload?.metrics) ? payload.metrics : [];
|
|
const reports = Array.isArray(payload?.reports) ? payload.reports : [];
|
|
const datasets = Array.isArray(payload?.source_dataset_keys) ? payload.source_dataset_keys : [];
|
|
const plannedSteps = Array.isArray(payload?.next_steps) ? payload.next_steps : [];
|
|
|
|
setText("[data-rental-report-count]", String(reports.length));
|
|
setText("[data-rental-dataset-count]", String(datasets.length));
|
|
setText("[data-rental-sync-strategy]", formatSyncStrategyLabel(payload?.materialization?.sync_strategy || "--"));
|
|
setText("[data-rental-source-domain]", formatDomainLabel(payload?.source_domain || "--"));
|
|
|
|
overviewMetrics.innerHTML = metrics.length > 0
|
|
? `<div class="admin-rental-grid">${metrics.map((item) => `<article class="admin-rental-item rounded-4 p-4"><div class="small text-uppercase fw-semibold text-secondary mb-2">${escapeHtml(item?.label || item?.key || "metrica")}</div><div class="h3 fw-semibold mb-2">${escapeHtml(item?.value || "0")}</div><div class="small text-secondary">${escapeHtml(item?.description || "")}</div></article>`).join("")}</div>`
|
|
: `<div class="admin-tool-empty-state rounded-4 p-4"><h4 class="h5 fw-semibold mb-2">Metricas nao disponiveis</h4><p class="text-secondary mb-0">O overview de locacao nao retornou metricas nesta leitura.</p></div>`;
|
|
|
|
materialization.innerHTML = payload?.materialization
|
|
? `<article class="admin-rental-item rounded-4 p-4"><div class="small text-uppercase fw-semibold text-secondary mb-3">Atualizacao da tela</div><div class="admin-rental-meta small text-secondary"><div><strong>Ritmo:</strong> ${escapeHtml(formatSyncStrategyLabel(payload?.materialization?.sync_strategy || "-"))}</div><div><strong>Camada:</strong> ${escapeHtml(formatStorageLabel(payload?.materialization?.storage_shape || "-"))}</div><div><strong>Consulta:</strong> ${escapeHtml(formatQuerySurfaceLabel(payload?.materialization?.query_surface || "-"))}</div></div></article>`
|
|
: "";
|
|
|
|
reportList.innerHTML = reports.length > 0
|
|
? reports.map((item) => `<article class="admin-rental-item rounded-4 p-4"><div class="d-flex flex-wrap justify-content-between align-items-start gap-3 mb-3"><div><h4 class="h5 fw-semibold mb-1">${escapeHtml(item?.label || humanizeKey(item?.report_key || "relatorio"))}</h4><div class="small text-secondary">${escapeHtml(item?.description || "")}</div></div><span class="badge rounded-pill bg-body-tertiary text-secondary border">${escapeHtml(formatGranularityLabel(item?.default_granularity || "aggregate"))}</span></div><div class="admin-rental-chip-group"><span class="badge rounded-pill bg-body-tertiary text-secondary border">Indicadores: ${escapeHtml(String((item?.supported_metric_keys || []).length))}</span><span class="badge rounded-pill bg-body-tertiary text-secondary border">Recortes: ${escapeHtml(String((item?.supported_dimension_fields || []).length))}</span><span class="badge rounded-pill bg-body-tertiary text-secondary border">Filtros: ${escapeHtml(String((item?.supported_filter_fields || []).length))}</span></div></article>`).join("")
|
|
: `<div class="admin-tool-empty-state rounded-4 p-4"><h4 class="h5 fw-semibold mb-2">Nenhum relatorio previsto</h4><p class="text-secondary mb-0">O overview nao retornou relatorios de locacao nesta leitura.</p></div>`;
|
|
|
|
nextSteps.innerHTML = plannedSteps.length > 0
|
|
? plannedSteps.map((item) => `<div class="admin-rental-item rounded-4 p-3 small text-secondary">${escapeHtml(item)}</div>`).join("")
|
|
: `<div class="admin-tool-empty-state rounded-4 p-4"><h4 class="h5 fw-semibold mb-2">Sem proximos passos</h4><p class="text-secondary mb-0">Nenhuma orientacao adicional foi retornada para locacao.</p></div>`;
|
|
|
|
showFeedback("success", "Relatorios de locacao carregados com sucesso na sessao do painel.");
|
|
} else {
|
|
setText("[data-rental-report-count]", "0");
|
|
setText("[data-rental-dataset-count]", "0");
|
|
setText("[data-rental-sync-strategy]", "--");
|
|
setText("[data-rental-source-domain]", "--");
|
|
overviewMetrics.innerHTML = `<div class="admin-tool-empty-state rounded-4 p-4"><h4 class="h5 fw-semibold mb-2">Locacao indisponivel</h4><p class="text-secondary mb-0">${escapeHtml(result.message || "Nao foi possivel carregar o overview de locacao.")}</p></div>`;
|
|
materialization.innerHTML = "";
|
|
reportList.innerHTML = "";
|
|
nextSteps.innerHTML = "";
|
|
showFeedback("warning", result.message || "Nao foi possivel carregar os relatorios de locacao na sessao atual.");
|
|
}
|
|
|
|
setText("[data-rental-last-sync]", formatNow());
|
|
toggleRefreshing(false);
|
|
}
|
|
|
|
function toggleRefreshing(isLoading) {
|
|
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 mountBotMonitoringPage(page) {
|
|
const refreshButton = page.querySelector("[data-admin-bot-monitoring-refresh]");
|
|
const refreshLabel = page.querySelector("[data-bot-monitoring-refresh-label]");
|
|
const refreshSpinner = page.querySelector("[data-bot-monitoring-refresh-spinner]");
|
|
const feedback = document.getElementById("admin-bot-monitoring-feedback");
|
|
const botFlowMetrics = page.querySelector("[data-bot-flow-overview-metrics]");
|
|
const botFlowMaterialization = page.querySelector("[data-bot-flow-materialization]");
|
|
const botFlowReportList = page.querySelector("[data-bot-flow-report-list]");
|
|
const botFlowNextSteps = page.querySelector("[data-bot-flow-next-steps]");
|
|
const telemetryMetrics = page.querySelector("[data-bot-telemetry-overview-metrics]");
|
|
const telemetryMaterialization = page.querySelector("[data-bot-telemetry-materialization]");
|
|
const telemetryReportList = page.querySelector("[data-bot-telemetry-report-list]");
|
|
const telemetryNextSteps = page.querySelector("[data-bot-telemetry-next-steps]");
|
|
|
|
if (!refreshButton || !refreshLabel || !refreshSpinner || !feedback || !botFlowMetrics || !botFlowMaterialization || !botFlowReportList || !botFlowNextSteps || !telemetryMetrics || !telemetryMaterialization || !telemetryReportList || !telemetryNextSteps) {
|
|
return;
|
|
}
|
|
|
|
refreshButton.addEventListener("click", () => {
|
|
void loadMonitoring();
|
|
});
|
|
|
|
void loadMonitoring();
|
|
|
|
async function loadMonitoring() {
|
|
toggleRefreshing(true);
|
|
clearFeedback();
|
|
|
|
const [botFlowResult, telemetryResult] = await Promise.all([
|
|
fetchPanelJson(page.dataset.botFlowOverviewEndpoint),
|
|
fetchPanelJson(page.dataset.telemetryOverviewEndpoint),
|
|
]);
|
|
|
|
if (botFlowResult.ok) {
|
|
renderDomainOverview({ kind: "flow", payload: botFlowResult.body, metricsTarget: botFlowMetrics, materializationTarget: botFlowMaterialization, reportsTarget: botFlowReportList, nextStepsTarget: botFlowNextSteps });
|
|
} else {
|
|
renderLockedState(botFlowMetrics, "Fluxo do bot indisponivel", botFlowResult.message || "Nao foi possivel carregar o overview operacional do bot.");
|
|
botFlowMaterialization.innerHTML = "";
|
|
botFlowReportList.innerHTML = "";
|
|
botFlowNextSteps.innerHTML = "";
|
|
setText("[data-bot-flow-report-count]", "0");
|
|
}
|
|
|
|
if (telemetryResult.ok) {
|
|
renderDomainOverview({ kind: "telemetry", payload: telemetryResult.body, metricsTarget: telemetryMetrics, materializationTarget: telemetryMaterialization, reportsTarget: telemetryReportList, nextStepsTarget: telemetryNextSteps });
|
|
} else {
|
|
renderLockedState(telemetryMetrics, "Telemetria indisponivel", telemetryResult.message || "Nao foi possivel carregar o overview de telemetria conversacional.");
|
|
telemetryMaterialization.innerHTML = "";
|
|
telemetryReportList.innerHTML = "";
|
|
telemetryNextSteps.innerHTML = "";
|
|
setText("[data-bot-telemetry-report-count]", "0");
|
|
}
|
|
|
|
if (botFlowResult.ok && telemetryResult.ok) {
|
|
const botFlowPayload = botFlowResult.body;
|
|
const telemetryPayload = telemetryResult.body;
|
|
const datasetCount = uniqueCount(botFlowPayload?.source_dataset_keys, telemetryPayload?.source_dataset_keys);
|
|
const syncStrategy = botFlowPayload?.materialization?.sync_strategy === telemetryPayload?.materialization?.sync_strategy
|
|
? botFlowPayload?.materialization?.sync_strategy
|
|
: "mixed";
|
|
setText("[data-bot-monitoring-dataset-count]", String(datasetCount));
|
|
setText("[data-bot-monitoring-sync-strategy]", formatSyncStrategyLabel(syncStrategy || "--"));
|
|
showFeedback("success", "Fluxo operacional do bot e telemetria conversacional carregados com sucesso na sessao do painel.");
|
|
} else if (botFlowResult.ok || telemetryResult.ok) {
|
|
const onlyLoaded = botFlowResult.ok ? "fluxo do bot" : "telemetria conversacional";
|
|
const datasetCount = botFlowResult.ok
|
|
? (Array.isArray(botFlowResult.body?.source_dataset_keys) ? botFlowResult.body.source_dataset_keys.length : 0)
|
|
: (Array.isArray(telemetryResult.body?.source_dataset_keys) ? telemetryResult.body.source_dataset_keys.length : 0);
|
|
const syncStrategy = botFlowResult.ok ? botFlowResult.body?.materialization?.sync_strategy : telemetryResult.body?.materialization?.sync_strategy;
|
|
setText("[data-bot-monitoring-dataset-count]", String(datasetCount));
|
|
setText("[data-bot-monitoring-sync-strategy]", formatSyncStrategyLabel(syncStrategy || "--"));
|
|
showFeedback("warning", `A tela carregou apenas ${onlyLoaded} com a sessao atual.`);
|
|
} else {
|
|
setText("[data-bot-monitoring-dataset-count]", "0");
|
|
setText("[data-bot-monitoring-sync-strategy]", "--");
|
|
showFeedback("warning", "Nao foi possivel carregar o monitoramento operacional do bot na sessao atual.");
|
|
}
|
|
|
|
setText("[data-bot-monitoring-last-sync]", formatNow());
|
|
toggleRefreshing(false);
|
|
}
|
|
|
|
function renderDomainOverview({ kind, payload, metricsTarget, materializationTarget, reportsTarget, nextStepsTarget }) {
|
|
const reports = Array.isArray(payload?.reports) ? payload.reports : [];
|
|
const metrics = Array.isArray(payload?.metrics) ? payload.metrics : [];
|
|
const nextSteps = Array.isArray(payload?.next_steps) ? payload.next_steps : [];
|
|
const reportCountSelector = kind === "flow" ? "[data-bot-flow-report-count]" : "[data-bot-telemetry-report-count]";
|
|
setText(reportCountSelector, String(reports.length));
|
|
|
|
metricsTarget.innerHTML = metrics.length > 0
|
|
? `<div class="admin-bot-monitoring-grid">${metrics.map((item) => `<article class="admin-bot-monitoring-item rounded-4 p-4"><div class="small text-uppercase fw-semibold text-secondary mb-2">${escapeHtml(item?.label || item?.key || "metrica")}</div><div class="h3 fw-semibold mb-2">${escapeHtml(item?.value || "0")}</div><div class="small text-secondary">${escapeHtml(item?.description || "")}</div></article>`).join("")}</div>`
|
|
: `<div class="admin-tool-empty-state rounded-4 p-4"><h4 class="h5 fw-semibold mb-2">Metricas nao disponiveis</h4><p class="text-secondary mb-0">O overview nao retornou metricas nesta leitura.</p></div>`;
|
|
|
|
materializationTarget.innerHTML = payload?.materialization
|
|
? `<article class="admin-bot-monitoring-item rounded-4 p-4"><div class="small text-uppercase fw-semibold text-secondary mb-3">Atualizacao da tela</div><div class="admin-bot-monitoring-meta small text-secondary"><div><strong>Ritmo:</strong> ${escapeHtml(formatSyncStrategyLabel(payload?.materialization?.sync_strategy || "-"))}</div><div><strong>Camada:</strong> ${escapeHtml(formatStorageLabel(payload?.materialization?.storage_shape || "-"))}</div><div><strong>Consulta:</strong> ${escapeHtml(formatQuerySurfaceLabel(payload?.materialization?.query_surface || "-"))}</div></div></article>`
|
|
: "";
|
|
|
|
reportsTarget.innerHTML = reports.length > 0
|
|
? reports.map((item) => `<article class="admin-bot-monitoring-item rounded-4 p-4"><div class="d-flex flex-wrap justify-content-between align-items-start gap-3 mb-3"><div><h4 class="h5 fw-semibold mb-1">${escapeHtml(item?.label || humanizeKey(item?.report_key || "relatorio"))}</h4><div class="small text-secondary">${escapeHtml(item?.description || "")}</div></div><span class="badge rounded-pill bg-body-tertiary text-secondary border">${escapeHtml(formatGranularityLabel(item?.default_granularity || "aggregate"))}</span></div><div class="admin-bot-monitoring-chip-group"><span class="badge rounded-pill bg-body-tertiary text-secondary border">Indicadores: ${escapeHtml(String((item?.supported_metric_keys || []).length))}</span><span class="badge rounded-pill bg-body-tertiary text-secondary border">Recortes: ${escapeHtml(String((item?.supported_dimension_fields || []).length))}</span></div></article>`).join("")
|
|
: `<div class="admin-tool-empty-state rounded-4 p-4"><h4 class="h5 fw-semibold mb-2">Nenhum relatorio previsto</h4><p class="text-secondary mb-0">O overview nao retornou relatorios para este dominio.</p></div>`;
|
|
|
|
nextStepsTarget.innerHTML = nextSteps.length > 0
|
|
? nextSteps.map((item) => `<div class="admin-bot-monitoring-item rounded-4 p-3 small text-secondary">${escapeHtml(item)}</div>`).join("")
|
|
: `<div class="admin-tool-empty-state rounded-4 p-4"><h4 class="h5 fw-semibold mb-2">Sem proximos passos</h4><p class="text-secondary mb-0">Nenhuma orientacao adicional foi retornada para este overview.</p></div>`;
|
|
}
|
|
|
|
function renderLockedState(container, title, message) {
|
|
container.innerHTML = `<div class="admin-tool-empty-state rounded-4 p-4"><h4 class="h5 fw-semibold mb-2">${escapeHtml(title)}</h4><p class="text-secondary mb-0">${escapeHtml(message)}</p></div>`;
|
|
}
|
|
|
|
function toggleRefreshing(isLoading) {
|
|
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 humanizeKey(value) {
|
|
const raw = String(value || "").trim();
|
|
if (!raw) {
|
|
return "-";
|
|
}
|
|
return raw
|
|
.replace(/[_-]+/g, " ")
|
|
.replace(/\b\w/g, (char) => char.toUpperCase());
|
|
}
|
|
|
|
function formatFriendlyLabel(value, mapping, fallback = "-") {
|
|
const normalized = String(value || "").trim().toLowerCase();
|
|
if (!normalized) {
|
|
return fallback;
|
|
}
|
|
return mapping[normalized] || humanizeKey(normalized);
|
|
}
|
|
|
|
function formatConfigTitle(value) {
|
|
return formatFriendlyLabel(value, {
|
|
application: "Aplicacao",
|
|
database: "Banco administrativo",
|
|
security: "Politicas de acesso",
|
|
panel_session: "Sessao do painel",
|
|
functional_configuration_contracts: "Catalogo funcional",
|
|
bot_governed_configuration_contracts: "Ajustes do atendimento",
|
|
model_runtime_separation: "Separacao de modelos",
|
|
write_governance: "Protecao de escrita",
|
|
atendimento_runtime_profile: "Modelo do atendimento",
|
|
tool_generation_runtime_profile: "Geracao de tools",
|
|
published_runtime_state: "Estado publicado"
|
|
}, "Configuracao");
|
|
}
|
|
|
|
function formatModeLabel(value) {
|
|
return formatFriendlyLabel(value, {
|
|
shared_contract_bootstrap: "Contrato base",
|
|
sales_contract_bootstrap: "Estrutura inicial",
|
|
revenue_contract_bootstrap: "Estrutura inicial",
|
|
rental_contract_bootstrap: "Estrutura inicial",
|
|
bot_flow_contract_bootstrap: "Estrutura inicial",
|
|
conversation_telemetry_contract_bootstrap: "Estrutura inicial",
|
|
mixed: "Leituras combinadas"
|
|
}, "Leitura base");
|
|
}
|
|
|
|
function formatMutabilityLabel(value) {
|
|
return formatFriendlyLabel(value, {
|
|
readonly: "Somente leitura",
|
|
read_only: "Somente leitura",
|
|
versioned: "Versionado",
|
|
mutable: "Editavel",
|
|
governed: "Governado"
|
|
}, "Somente leitura");
|
|
}
|
|
|
|
function formatGranularityLabel(value) {
|
|
return formatFriendlyLabel(value, {
|
|
aggregate: "Visao consolidada",
|
|
daily: "Por dia",
|
|
weekly: "Por semana",
|
|
monthly: "Por mes"
|
|
}, "Visao consolidada");
|
|
}
|
|
|
|
function formatSyncStrategyLabel(value) {
|
|
return formatFriendlyLabel(value, {
|
|
etl_incremental: "Atualizacao em lote",
|
|
snapshot_refresh: "Atualizacao por snapshot",
|
|
mixed: "Leituras combinadas"
|
|
}, "Nao informado");
|
|
}
|
|
|
|
function formatStorageLabel(value) {
|
|
return formatFriendlyLabel(value, {
|
|
snapshot_table: "Snapshot consolidado",
|
|
dedicated_view: "Visao preparada"
|
|
}, "Nao informado");
|
|
}
|
|
|
|
function formatQuerySurfaceLabel(value) {
|
|
return formatFriendlyLabel(value, {
|
|
dedicated_view: "Consulta preparada",
|
|
analytical_view: "Consulta analitica",
|
|
report_endpoint: "Consulta do painel"
|
|
}, "Nao informado");
|
|
}
|
|
|
|
function formatDomainLabel(value) {
|
|
return formatFriendlyLabel(value, {
|
|
sistema: "Sistema",
|
|
sales: "Vendas",
|
|
arrecadacao: "Arrecadacao",
|
|
rental: "Locacao",
|
|
fluxo_bot: "Fluxo do bot",
|
|
telemetria_conversacional: "Telemetria conversacional",
|
|
bot: "Atendimento"
|
|
}, "-");
|
|
}
|
|
|
|
function formatSourceLabel(value) {
|
|
return formatFriendlyLabel(value, {
|
|
env: "Ambiente",
|
|
runtime: "Aplicacao",
|
|
shared_contract: "Contrato compartilhado",
|
|
runtime_guard: "Protecao ativa"
|
|
}, "Origem");
|
|
}
|
|
|
|
function formatRuntimeTargetLabel(value) {
|
|
return formatFriendlyLabel(value, {
|
|
atendimento: "Atendimento",
|
|
tool_generation: "Geracao de tools"
|
|
}, "Runtime");
|
|
}
|
|
|
|
function formatPurposeLabel(value) {
|
|
return formatFriendlyLabel(value, {
|
|
customer_response: "Resposta ao cliente",
|
|
tool_generation: "Geracao de tools",
|
|
decision_support: "Apoio a decisao"
|
|
}, "Uso interno");
|
|
}
|
|
|
|
function uniqueCount(...collections) {
|
|
return new Set(
|
|
collections.flatMap((items) => Array.isArray(items) ? items : []).filter(Boolean)
|
|
).size;
|
|
}
|
|
|
|
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("&", "&")
|
|
.replaceAll("<", "<")
|
|
.replaceAll(">", ">")
|
|
.replaceAll('"', """)
|
|
.replaceAll("'", "'");
|
|
}
|
|
|
|
function formatDateTime(value) {
|
|
const parsed = new Date(value);
|
|
if (Number.isNaN(parsed.getTime())) {
|
|
return String(value || "");
|
|
}
|
|
return parsed.toLocaleString("pt-BR", {
|
|
day: "2-digit",
|
|
month: "2-digit",
|
|
year: "numeric",
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
});
|
|
}
|