feat(admin): concluir telas da fase 4 no painel interno

Entrega as telas de configuracoes do sistema, relatorios comerciais, locacao e monitoramento operacional do bot dentro da sessao web do admin, com navegacao integrada ao dashboard e carregamento real pela sessao do painel.

Tambem simplifica a linguagem das superficies, remove detalhes tecnicos desnecessarios para o usuario, corrige o ponto quebrado que abria contrato em JSON bruto e ajusta grids, cards e quebra de conteudo para melhorar a leitura nas telas da fase 4.
feat/self-evolving-tools-foundation
parent 9a31b0c5ae
commit d6e765ce3c

@ -1,6 +1,7 @@
from html import escape
from admin_app.view.view_models import (
AdminBotMonitoringPageView,
AdminCollaboratorManagementPageView,
AdminLoginPageView,
AdminPanelHomeView,
@ -10,6 +11,9 @@ from admin_app.view.view_models import (
AdminPanelQuickAction,
AdminPanelRoadmapItem,
AdminPanelSurfaceLink,
AdminRentalReportsPageView,
AdminSalesRevenueReportsPageView,
AdminSystemConfigurationPageView,
AdminToolIntakePageView,
AdminToolIntakeParameterTypeOption,
AdminToolReviewPageView,
@ -1237,3 +1241,511 @@ def _render_collaborator_cards(items: list[dict]) -> str:
for item in items
)
def render_system_configuration_page(
view: AdminSystemConfigurationPageView,
*,
css_href: str,
js_href: str,
) -> str:
access_notes_markup = _render_text_list(view.access_notes)
governance_notes_markup = _render_text_list(view.governance_notes)
return f'''<!DOCTYPE html>
<html lang="pt-BR" data-bs-theme="light">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{escape(view.title)}</title>
<meta name="description" content="{escape(view.subtitle)}">
<link rel="stylesheet" href="{BOOTSTRAP_CSS_HREF}">
<link rel="stylesheet" href="{escape(css_href, quote=True)}">
</head>
<body class="admin-view-body admin-system-page">
<div class="container-xxl py-4 py-lg-5"
data-admin-system-configuration="true"
data-overview-endpoint="{escape(view.overview_endpoint, quote=True)}"
data-runtime-endpoint="{escape(view.runtime_endpoint, quote=True)}"
data-security-endpoint="{escape(view.security_endpoint, quote=True)}"
data-model-runtimes-endpoint="{escape(view.model_runtimes_endpoint, quote=True)}"
data-functional-endpoint="{escape(view.functional_endpoint, quote=True)}"
data-functional-detail-base="{escape(view.functional_detail_base, quote=True)}"
data-bot-governance-endpoint="{escape(view.bot_governance_endpoint, quote=True)}">
<div class="row g-4 align-items-start">
<aside class="col-12 col-xl-4 col-xxl-3">
<div class="card border-0 shadow-sm admin-shell-card admin-sidebar-sticky">
<div class="card-body p-4">
<div class="d-flex flex-wrap gap-2 mb-3">
<span class="badge rounded-pill text-bg-dark">Configuracoes</span>
<span class="badge rounded-pill bg-body-tertiary text-secondary border">Fase 4</span>
</div>
<h1 class="display-6 fw-semibold mb-3">{escape(view.title)}</h1>
<p class="text-secondary mb-4">{escape(view.subtitle)}</p>
<div class="d-grid gap-2 mb-4 admin-quick-actions">
<a class="btn btn-dark rounded-pill" href="{escape(view.dashboard_href, quote=True)}">Voltar ao dashboard</a>
<a class="btn btn-outline-dark rounded-pill" href="#functional-configuration-card">Ver catalogo funcional</a>
</div>
<div class="admin-runtime-block p-3 mb-3">
<p class="text-uppercase small fw-semibold text-secondary mb-3">Contexto atual</p>
<div class="d-grid gap-3 small">
<div class="d-flex justify-content-between gap-3"><span class="text-secondary">Aplicacao</span><strong class="text-end">{escape(view.app_name)}</strong></div>
<div class="d-flex justify-content-between gap-3"><span class="text-secondary">Ambiente</span><strong class="text-end text-uppercase">{escape(view.environment)}</strong></div>
<div class="d-flex justify-content-between gap-3"><span class="text-secondary">Versao</span><strong class="text-end">{escape(view.version)}</strong></div>
<div class="d-flex justify-content-between gap-3"><span class="text-secondary">Ultima leitura</span><strong class="text-end" data-system-last-sync>Aguardando</strong></div>
</div>
</div>
<div class="admin-tool-review-note p-4 mb-3">
<p class="text-uppercase small fw-semibold text-secondary mb-2">Leitura disponivel</p>
<ul class="small text-secondary ps-3 mb-0">{access_notes_markup}</ul>
</div>
<div class="admin-tool-review-note p-4">
<p class="text-uppercase small fw-semibold text-secondary mb-2">Governanca desta tela</p>
<ul class="small text-secondary ps-3 mb-0">{governance_notes_markup}</ul>
</div>
</div>
</div>
</aside>
<section class="col-12 col-xl-8 col-xxl-9">
<div class="card border-0 shadow-sm admin-hero-card overflow-hidden mb-4">
<div class="card-body p-4 p-lg-5">
<div class="d-flex flex-wrap justify-content-between align-items-start gap-3">
<div>
<div class="d-flex flex-wrap gap-2 mb-3">
<span class="badge rounded-pill bg-white text-dark border">Somente leitura</span>
<span class="badge rounded-pill bg-dark-subtle text-dark-emphasis border border-dark-subtle">Sessao protegida</span>
</div>
<h2 class="display-5 fw-semibold mb-3">Configuracao funcional e regras do painel em uma unica tela</h2>
<p class="lead text-secondary mb-0">Aqui voce acompanha o que esta disponivel para consulta agora, sem expor detalhes internos demais do ambiente.</p>
</div>
<button class="btn btn-outline-dark rounded-pill px-4" type="button" data-admin-system-refresh>
<span data-system-refresh-label>Atualizar leitura</span>
<span class="spinner-border spinner-border-sm d-none" data-system-refresh-spinner aria-hidden="true"></span>
</button>
</div>
</div>
</div>
<div class="alert d-none rounded-4 mb-4" id="admin-system-configuration-feedback" role="status"></div>
<div class="row g-3 mb-4">
<div class="col-12 col-md-6 col-xxl-3"><div class="card border-0 shadow-sm admin-metric-card h-100"><div class="card-body p-4"><p class="small text-uppercase fw-semibold text-secondary mb-3">Configuracoes funcionais</p><div class="display-6 fw-semibold mb-2" data-system-config-count>0</div><p class="text-secondary mb-0">Itens que o time pode acompanhar nesta etapa.</p></div></div></div>
<div class="col-12 col-md-6 col-xxl-3"><div class="card border-0 shadow-sm admin-metric-card h-100"><div class="card-body p-4"><p class="small text-uppercase fw-semibold text-secondary mb-3">Campos governados pelo bot</p><div class="display-6 fw-semibold mb-2" data-system-bot-setting-count>0</div><p class="text-secondary mb-0">Ajustes do atendimento sob controle do admin.</p></div></div></div>
<div class="col-12 col-md-6 col-xxl-3"><div class="card border-0 shadow-sm admin-metric-card h-100"><div class="card-body p-4"><p class="small text-uppercase fw-semibold text-secondary mb-3">Perfis de runtime</p><div class="display-6 fw-semibold mb-2" data-system-runtime-profile-count>0</div><p class="text-secondary mb-0">Separacao entre atendimento e geracao de tools quando liberada.</p></div></div></div>
<div class="col-12 col-md-6 col-xxl-3"><div class="card border-0 shadow-sm admin-metric-card h-100"><div class="card-body p-4"><p class="small text-uppercase fw-semibold text-secondary mb-3">Fontes de configuracao</p><div class="display-6 fw-semibold mb-2" data-system-source-count>0</div><p class="text-secondary mb-0">Base usada para montar os dados desta tela.</p></div></div></div>
</div>
<div class="row g-4 mb-4">
<div class="col-12 col-xxl-7"><div id="functional-configuration-card" class="card border-0 shadow-sm admin-surface-card h-100"><div class="card-body p-4 p-lg-5 d-flex flex-column gap-4"><div class="d-flex flex-wrap justify-content-between align-items-start gap-3"><div><p class="text-uppercase small fw-semibold text-secondary mb-2">Catalogo funcional</p><h3 class="h3 fw-semibold mb-2">Configuracoes funcionais do sistema</h3><p class="text-secondary mb-0">Cada card resume o que a configuracao cobre e como ela impacta a operacao.</p></div><span class="badge rounded-pill bg-body-tertiary text-secondary border" data-system-functional-mode>Contrato compartilhado</span></div><div class="admin-system-grid" data-system-functional-list><div class="admin-tool-empty-state rounded-4 p-4"><h4 class="h5 fw-semibold mb-2">Catalogo ainda nao carregado</h4><p class="text-secondary mb-0">Atualize a leitura para montar a superficie funcional desta etapa.</p></div></div></div></div></div>
<div class="col-12 col-xxl-5"><div class="card border-0 shadow-sm admin-surface-card h-100"><div class="card-body p-4 p-lg-5 d-flex flex-column gap-4"><div><p class="text-uppercase small fw-semibold text-secondary mb-2">Governanca do bot</p><h3 class="h3 fw-semibold mb-2">Campos sob governanca administrativa</h3><p class="text-secondary mb-0">Os ajustes do atendimento aparecem aqui de forma agrupada e clara.</p></div><div><div class="small text-uppercase fw-semibold text-secondary mb-2">Parent config keys</div><div class="admin-system-chip-group" data-system-parent-keys><span class="badge rounded-pill bg-body-tertiary text-secondary border">Aguardando</span></div></div><div class="admin-system-grid" data-system-bot-settings-list><div class="admin-tool-empty-state rounded-4 p-4"><h4 class="h5 fw-semibold mb-2">Governanca ainda nao carregada</h4><p class="text-secondary mb-0">Os campos governados pelo bot aparecem aqui depois da primeira leitura.</p></div></div></div></div></div>
</div>
<div class="row g-4 mb-4">
<div class="col-12 col-lg-6"><div class="card border-0 shadow-sm admin-surface-card h-100"><div class="card-body p-4 p-lg-5 d-flex flex-column gap-4"><div><p class="text-uppercase small fw-semibold text-secondary mb-2">Runtime administrativo</p><h3 class="h3 fw-semibold mb-2">Informacoes essenciais do painel</h3><p class="text-secondary mb-0">Mostra apenas o contexto util para a operacao.</p></div><div data-system-runtime-summary><div class="admin-tool-empty-state rounded-4 p-4"><h4 class="h5 fw-semibold mb-2">Runtime aguardando leitura</h4><p class="text-secondary mb-0">Esta area e carregada conforme a permissao da sessao atual.</p></div></div></div></div></div>
<div class="col-12 col-lg-6"><div class="card border-0 shadow-sm admin-surface-card h-100"><div class="card-body p-4 p-lg-5 d-flex flex-column gap-4"><div><p class="text-uppercase small fw-semibold text-secondary mb-2">Postura de seguranca</p><h3 class="h3 fw-semibold mb-2">Regras visiveis de senha e sessao</h3><p class="text-secondary mb-0">Exibe somente o necessario para orientar o uso da sessao.</p></div><div data-system-security-summary><div class="admin-tool-empty-state rounded-4 p-4"><h4 class="h5 fw-semibold mb-2">Seguranca aguardando leitura</h4><p class="text-secondary mb-0">A sessao atual precisa de permissao elevada para ver este snapshot.</p></div></div></div></div></div>
</div>
<div class="row g-4">
<div class="col-12 col-xxl-7"><div class="card border-0 shadow-sm admin-surface-card h-100"><div class="card-body p-4 p-lg-5 d-flex flex-column gap-4"><div><p class="text-uppercase small fw-semibold text-secondary mb-2">Separacao de runtime</p><h3 class="h3 fw-semibold mb-2">Modelos do atendimento versus geracao de tools</h3><p class="text-secondary mb-0">Aqui fica clara a separacao entre atendimento e geracao de tools.</p></div><div data-system-model-runtime-summary><div class="admin-tool-empty-state rounded-4 p-4"><h4 class="h5 fw-semibold mb-2">Perfis de runtime aguardando leitura</h4><p class="text-secondary mb-0">A superficie completa aparece quando a sessao pode consultar manage_settings.</p></div></div></div></div></div>
<div class="col-12 col-xxl-5"><div class="card border-0 shadow-sm admin-surface-card h-100"><div class="card-body p-4 p-lg-5 d-flex flex-column gap-4"><div><p class="text-uppercase small fw-semibold text-secondary mb-2">Fontes do snapshot</p><h3 class="h3 fw-semibold mb-2">De onde cada configuracao vem</h3><p class="text-secondary mb-0">Resumo das bases usadas para montar esta tela.</p></div><div class="admin-system-grid" data-system-source-list><div class="admin-tool-empty-state rounded-4 p-4"><h4 class="h5 fw-semibold mb-2">Fontes aguardando leitura</h4><p class="text-secondary mb-0">As fontes completas entram quando a sessao pode consultar o overview tecnico.</p></div></div></div></div></div>
</div>
</section>
</div>
</div>
<script src="{BOOTSTRAP_JS_HREF}" defer></script>
<script src="{escape(js_href, quote=True)}" defer></script>
</body>
</html>
'''
def render_sales_revenue_reports_page(
view: AdminSalesRevenueReportsPageView,
*,
css_href: str,
js_href: str,
) -> str:
access_notes_markup = _render_text_list(view.access_notes)
reading_notes_markup = _render_text_list(view.reading_notes)
return f'''<!DOCTYPE html>
<html lang="pt-BR" data-bs-theme="light">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{escape(view.title)}</title>
<meta name="description" content="{escape(view.subtitle)}">
<link rel="stylesheet" href="{BOOTSTRAP_CSS_HREF}">
<link rel="stylesheet" href="{escape(css_href, quote=True)}">
</head>
<body class="admin-view-body admin-commercial-reports-page">
<div class="container-xxl py-4 py-lg-5"
data-admin-sales-revenue-reports="true"
data-sales-overview-endpoint="{escape(view.sales_overview_endpoint, quote=True)}"
data-revenue-overview-endpoint="{escape(view.revenue_overview_endpoint, quote=True)}">
<div class="row g-4 align-items-start">
<aside class="col-12 col-xl-4 col-xxl-3">
<div class="card border-0 shadow-sm admin-shell-card admin-sidebar-sticky">
<div class="card-body p-4">
<div class="d-flex flex-wrap gap-2 mb-3">
<span class="badge rounded-pill text-bg-dark">Relatorios</span>
<span class="badge rounded-pill bg-body-tertiary text-secondary border">Vendas + arrecadacao</span>
</div>
<h1 class="display-6 fw-semibold mb-3">{escape(view.title)}</h1>
<p class="text-secondary mb-4">{escape(view.subtitle)}</p>
<div class="d-grid gap-2 mb-4 admin-quick-actions">
<a class="btn btn-dark rounded-pill" href="{escape(view.dashboard_href, quote=True)}">Voltar ao dashboard</a>
<a class="btn btn-outline-dark rounded-pill" href="#sales-reports-card">Ver vendas</a>
</div>
<div class="admin-runtime-block p-3 mb-3">
<p class="text-uppercase small fw-semibold text-secondary mb-3">Contexto atual</p>
<div class="d-grid gap-3 small">
<div class="d-flex justify-content-between gap-3"><span class="text-secondary">Aplicacao</span><strong class="text-end">{escape(view.app_name)}</strong></div>
<div class="d-flex justify-content-between gap-3"><span class="text-secondary">Ambiente</span><strong class="text-end text-uppercase">{escape(view.environment)}</strong></div>
<div class="d-flex justify-content-between gap-3"><span class="text-secondary">Versao</span><strong class="text-end">{escape(view.version)}</strong></div>
<div class="d-flex justify-content-between gap-3"><span class="text-secondary">Ultima leitura</span><strong class="text-end" data-commercial-last-sync>Aguardando</strong></div>
</div>
</div>
<div class="admin-tool-review-note p-4 mb-3">
<p class="text-uppercase small fw-semibold text-secondary mb-2">Escopo liberado</p>
<ul class="small text-secondary ps-3 mb-0">{access_notes_markup}</ul>
</div>
<div class="admin-tool-review-note p-4">
<p class="text-uppercase small fw-semibold text-secondary mb-2">Como ler esta tela</p>
<ul class="small text-secondary ps-3 mb-0">{reading_notes_markup}</ul>
</div>
</div>
</div>
</aside>
<section class="col-12 col-xl-8 col-xxl-9">
<div class="card border-0 shadow-sm admin-hero-card overflow-hidden mb-4">
<div class="card-body p-4 p-lg-5">
<div class="d-flex flex-wrap justify-content-between align-items-start gap-3">
<div>
<div class="d-flex flex-wrap gap-2 mb-3">
<span class="badge rounded-pill bg-white text-dark border">Leitura operacional</span>
<span class="badge rounded-pill bg-dark-subtle text-dark-emphasis border border-dark-subtle">Fase 4</span>
</div>
<h2 class="display-5 fw-semibold mb-3">Vendas e arrecadacao na mesma visao do painel</h2>
<p class="lead text-secondary mb-0">Aqui o time acompanha os principais blocos comerciais de forma simples e organizada.</p>
</div>
<button class="btn btn-outline-dark rounded-pill px-4" type="button" data-admin-commercial-refresh>
<span data-commercial-refresh-label>Atualizar leitura</span>
<span class="spinner-border spinner-border-sm d-none" data-commercial-refresh-spinner aria-hidden="true"></span>
</button>
</div>
</div>
</div>
<div class="alert d-none rounded-4 mb-4" id="admin-commercial-feedback" role="status"></div>
<div class="row g-3 mb-4">
<div class="col-12 col-md-6 col-xxl-3"><div class="card border-0 shadow-sm admin-metric-card h-100"><div class="card-body p-4"><p class="small text-uppercase fw-semibold text-secondary mb-3">Relatorios de vendas</p><div class="display-6 fw-semibold mb-2" data-sales-report-count>0</div><p class="text-secondary mb-0">Estrutura inicial do dominio comercial.</p></div></div></div>
<div class="col-12 col-md-6 col-xxl-3"><div class="card border-0 shadow-sm admin-metric-card h-100"><div class="card-body p-4"><p class="small text-uppercase fw-semibold text-secondary mb-3">Relatorios de arrecadacao</p><div class="display-6 fw-semibold mb-2" data-revenue-report-count>0</div><p class="text-secondary mb-0">Leitura inicial dos recebimentos de locacao.</p></div></div></div>
<div class="col-12 col-md-6 col-xxl-3"><div class="card border-0 shadow-sm admin-metric-card h-100"><div class="card-body p-4"><p class="small text-uppercase fw-semibold text-secondary mb-3">Bases de leitura</p><div class="display-6 fw-semibold mb-2" data-commercial-dataset-count>0</div><p class="text-secondary mb-0">Bases consolidadas usadas para montar esta tela.</p></div></div></div>
<div class="col-12 col-md-6 col-xxl-3"><div class="card border-0 shadow-sm admin-metric-card h-100"><div class="card-body p-4"><p class="small text-uppercase fw-semibold text-secondary mb-3">Atualizacao</p><div class="h4 fw-semibold mb-2" data-commercial-sync-strategy>--</div><p class="text-secondary mb-0">Ritmo atual da carga exibida no painel.</p></div></div></div>
</div>
<div class="row g-4">
<div class="col-12 col-xxl-6">
<div id="sales-reports-card" class="card border-0 shadow-sm admin-surface-card h-100">
<div class="card-body p-4 p-lg-5 d-flex flex-column gap-4">
<div>
<p class="text-uppercase small fw-semibold text-secondary mb-2">Vendas</p>
<h3 class="h3 fw-semibold mb-2">O que acompanhar em vendas</h3>
<p class="text-secondary mb-0">Volume de pedidos, ticket medio, cancelamentos e comparativos principais.</p>
</div>
<div class="admin-commercial-stack" data-sales-overview-metrics>
<div class="admin-tool-empty-state rounded-4 p-4"><h4 class="h5 fw-semibold mb-2">Vendas aguardando leitura</h4><p class="text-secondary mb-0">Clique em atualizar leitura para carregar o snapshot de vendas.</p></div>
</div>
<div class="admin-commercial-stack" data-sales-materialization></div>
<div class="admin-commercial-grid" data-sales-report-list></div>
<div>
<div class="small text-uppercase fw-semibold text-secondary mb-2">Proximas melhorias</div>
<div class="admin-commercial-list" data-sales-next-steps></div>
</div>
</div>
</div>
</div>
<div class="col-12 col-xxl-6">
<div class="card border-0 shadow-sm admin-surface-card h-100">
<div class="card-body p-4 p-lg-5 d-flex flex-column gap-4">
<div>
<p class="text-uppercase small fw-semibold text-secondary mb-2">Arrecadacao</p>
<h3 class="h3 fw-semibold mb-2">O que acompanhar em arrecadacao</h3>
<p class="text-secondary mb-0">Pagamentos liquidados, valor arrecadado e conciliacao por contrato.</p>
</div>
<div class="admin-commercial-stack" data-revenue-overview-metrics>
<div class="admin-tool-empty-state rounded-4 p-4"><h4 class="h5 fw-semibold mb-2">Arrecadacao aguardando leitura</h4><p class="text-secondary mb-0">Clique em atualizar leitura para carregar o snapshot de arrecadacao.</p></div>
</div>
<div class="admin-commercial-stack" data-revenue-materialization></div>
<div class="admin-commercial-grid" data-revenue-report-list></div>
<div>
<div class="small text-uppercase fw-semibold text-secondary mb-2">Proximas melhorias</div>
<div class="admin-commercial-list" data-revenue-next-steps></div>
</div>
</div>
</div>
</div>
</div>
</section>
</div>
</div>
<script src="{BOOTSTRAP_JS_HREF}" defer></script>
<script src="{escape(js_href, quote=True)}" defer></script>
</body>
</html>
'''
def render_rental_reports_page(
view: AdminRentalReportsPageView,
*,
css_href: str,
js_href: str,
) -> str:
access_notes_markup = _render_text_list(view.access_notes)
reading_notes_markup = _render_text_list(view.reading_notes)
return f'''<!DOCTYPE html>
<html lang="pt-BR" data-bs-theme="light">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{escape(view.title)}</title>
<meta name="description" content="{escape(view.subtitle)}">
<link rel="stylesheet" href="{BOOTSTRAP_CSS_HREF}">
<link rel="stylesheet" href="{escape(css_href, quote=True)}">
</head>
<body class="admin-view-body admin-rental-reports-page">
<div class="container-xxl py-4 py-lg-5"
data-admin-rental-reports="true"
data-rental-overview-endpoint="{escape(view.overview_endpoint, quote=True)}">
<div class="row g-4 align-items-start">
<aside class="col-12 col-xl-4 col-xxl-3">
<div class="card border-0 shadow-sm admin-shell-card admin-sidebar-sticky">
<div class="card-body p-4">
<div class="d-flex flex-wrap gap-2 mb-3">
<span class="badge rounded-pill text-bg-dark">Relatorios</span>
<span class="badge rounded-pill bg-body-tertiary text-secondary border">Locacao</span>
</div>
<h1 class="display-6 fw-semibold mb-3">{escape(view.title)}</h1>
<p class="text-secondary mb-4">{escape(view.subtitle)}</p>
<div class="d-grid gap-2 mb-4 admin-quick-actions">
<a class="btn btn-dark rounded-pill" href="{escape(view.dashboard_href, quote=True)}">Voltar ao dashboard</a>
<a class="btn btn-outline-dark rounded-pill" href="#rental-report-catalog">Ver catalogo</a>
</div>
<div class="admin-runtime-block p-3 mb-3">
<p class="text-uppercase small fw-semibold text-secondary mb-3">Contexto atual</p>
<div class="d-grid gap-3 small">
<div class="d-flex justify-content-between gap-3"><span class="text-secondary">Aplicacao</span><strong class="text-end">{escape(view.app_name)}</strong></div>
<div class="d-flex justify-content-between gap-3"><span class="text-secondary">Ambiente</span><strong class="text-end text-uppercase">{escape(view.environment)}</strong></div>
<div class="d-flex justify-content-between gap-3"><span class="text-secondary">Versao</span><strong class="text-end">{escape(view.version)}</strong></div>
<div class="d-flex justify-content-between gap-3"><span class="text-secondary">Ultima leitura</span><strong class="text-end" data-rental-last-sync>Aguardando</strong></div>
</div>
</div>
<div class="admin-tool-review-note p-4 mb-3">
<p class="text-uppercase small fw-semibold text-secondary mb-2">Escopo liberado</p>
<ul class="small text-secondary ps-3 mb-0">{access_notes_markup}</ul>
</div>
<div class="admin-tool-review-note p-4">
<p class="text-uppercase small fw-semibold text-secondary mb-2">Como ler esta tela</p>
<ul class="small text-secondary ps-3 mb-0">{reading_notes_markup}</ul>
</div>
</div>
</div>
</aside>
<section class="col-12 col-xl-8 col-xxl-9">
<div class="card border-0 shadow-sm admin-hero-card overflow-hidden mb-4">
<div class="card-body p-4 p-lg-5">
<div class="d-flex flex-wrap justify-content-between align-items-start gap-3">
<div>
<div class="d-flex flex-wrap gap-2 mb-3">
<span class="badge rounded-pill bg-white text-dark border">Leitura operacional</span>
<span class="badge rounded-pill bg-dark-subtle text-dark-emphasis border border-dark-subtle">Fase 4</span>
</div>
<h2 class="display-5 fw-semibold mb-3">Visao inicial de locacao para frota e contratos</h2>
<p class="lead text-secondary mb-0">Acompanhe os principais blocos do dominio em uma leitura organizada do painel.</p>
</div>
<button class="btn btn-outline-dark rounded-pill px-4" type="button" data-admin-rental-refresh>
<span data-rental-refresh-label>Atualizar leitura</span>
<span class="spinner-border spinner-border-sm d-none" data-rental-refresh-spinner aria-hidden="true"></span>
</button>
</div>
</div>
</div>
<div class="alert d-none rounded-4 mb-4" id="admin-rental-feedback" role="status"></div>
<div class="row g-3 mb-4">
<div class="col-12 col-md-6 col-xxl-3"><div class="card border-0 shadow-sm admin-metric-card h-100"><div class="card-body p-4"><p class="small text-uppercase fw-semibold text-secondary mb-3">Relatorios</p><div class="display-6 fw-semibold mb-2" data-rental-report-count>0</div><p class="text-secondary mb-0">Temas iniciais que o time ja consegue acompanhar.</p></div></div></div>
<div class="col-12 col-md-6 col-xxl-3"><div class="card border-0 shadow-sm admin-metric-card h-100"><div class="card-body p-4"><p class="small text-uppercase fw-semibold text-secondary mb-3">Bases de leitura</p><div class="display-6 fw-semibold mb-2" data-rental-dataset-count>0</div><p class="text-secondary mb-0">Bases consolidadas de frota e contratos.</p></div></div></div>
<div class="col-12 col-md-6 col-xxl-3"><div class="card border-0 shadow-sm admin-metric-card h-100"><div class="card-body p-4"><p class="small text-uppercase fw-semibold text-secondary mb-3">Atualizacao</p><div class="h4 fw-semibold mb-2" data-rental-sync-strategy>--</div><p class="text-secondary mb-0">Ritmo atual da carga exibida no painel.</p></div></div></div>
<div class="col-12 col-md-6 col-xxl-3"><div class="card border-0 shadow-sm admin-metric-card h-100"><div class="card-body p-4"><p class="small text-uppercase fw-semibold text-secondary mb-3">Area acompanhada</p><div class="h4 fw-semibold mb-2" data-rental-source-domain>--</div><p class="text-secondary mb-0">Dominio principal desta leitura.</p></div></div></div>
</div>
<div class="row g-4">
<div class="col-12 col-lg-5">
<div class="card border-0 shadow-sm admin-surface-card h-100">
<div class="card-body p-4 p-lg-5 d-flex flex-column gap-4">
<div>
<p class="text-uppercase small fw-semibold text-secondary mb-2">Overview de locacao</p>
<h3 class="h3 fw-semibold mb-2">Resumo inicial da operacao</h3>
<p class="text-secondary mb-0">Os indicadores mostram rapidamente a situacao atual de frota e contratos.</p>
</div>
<div class="admin-rental-stack" data-rental-overview-metrics>
<div class="admin-tool-empty-state rounded-4 p-4"><h4 class="h5 fw-semibold mb-2">Locacao aguardando leitura</h4><p class="text-secondary mb-0">Clique em atualizar leitura para montar o snapshot desta superficie.</p></div>
</div>
<div class="admin-rental-stack" data-rental-materialization></div>
<div>
<div class="small text-uppercase fw-semibold text-secondary mb-2">Proximas melhorias</div>
<div class="admin-rental-list" data-rental-next-steps></div>
</div>
</div>
</div>
</div>
<div class="col-12 col-lg-7">
<div id="rental-report-catalog" class="card border-0 shadow-sm admin-surface-card h-100">
<div class="card-body p-4 p-lg-5 d-flex flex-column gap-4">
<div>
<p class="text-uppercase small fw-semibold text-secondary mb-2">Relatorios desta etapa</p>
<h3 class="h3 fw-semibold mb-2">Relatorios disponiveis nesta etapa</h3>
<p class="text-secondary mb-0">Disponibilidade de frota, lifecycle de contratos, devolucoes em atraso, ocupacao e receita prevista versus final.</p>
</div>
<div class="admin-rental-grid" data-rental-report-list>
<div class="admin-tool-empty-state rounded-4 p-4"><h4 class="h5 fw-semibold mb-2">Catalogo aguardando leitura</h4><p class="text-secondary mb-0">Os cards de relatorio aparecem aqui depois da primeira leitura do painel.</p></div>
</div>
</div>
</div>
</div>
</div>
</section>
</div>
</div>
<script src="{BOOTSTRAP_JS_HREF}" defer></script>
<script src="{escape(js_href, quote=True)}" defer></script>
</body>
</html>
'''
def render_bot_monitoring_page(
view: AdminBotMonitoringPageView,
*,
css_href: str,
js_href: str,
) -> str:
access_notes_markup = _render_text_list(view.access_notes)
reading_notes_markup = _render_text_list(view.reading_notes)
return f'''<!DOCTYPE html>
<html lang="pt-BR" data-bs-theme="light">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{escape(view.title)}</title>
<meta name="description" content="{escape(view.subtitle)}">
<link rel="stylesheet" href="{BOOTSTRAP_CSS_HREF}">
<link rel="stylesheet" href="{escape(css_href, quote=True)}">
</head>
<body class="admin-view-body admin-bot-monitoring-page">
<div class="container-xxl py-4 py-lg-5"
data-admin-bot-monitoring="true"
data-bot-flow-overview-endpoint="{escape(view.bot_flow_overview_endpoint, quote=True)}"
data-telemetry-overview-endpoint="{escape(view.telemetry_overview_endpoint, quote=True)}">
<div class="row g-4 align-items-start">
<aside class="col-12 col-xl-4 col-xxl-3">
<div class="card border-0 shadow-sm admin-shell-card admin-sidebar-sticky">
<div class="card-body p-4">
<div class="d-flex flex-wrap gap-2 mb-3">
<span class="badge rounded-pill text-bg-dark">Monitoramento</span>
<span class="badge rounded-pill bg-body-tertiary text-secondary border">Bot operacional</span>
</div>
<h1 class="display-6 fw-semibold mb-3">{escape(view.title)}</h1>
<p class="text-secondary mb-4">{escape(view.subtitle)}</p>
<div class="d-grid gap-2 mb-4 admin-quick-actions">
<a class="btn btn-dark rounded-pill" href="{escape(view.dashboard_href, quote=True)}">Voltar ao dashboard</a>
<a class="btn btn-outline-dark rounded-pill" href="#bot-flow-monitoring-card">Ver fluxo do bot</a>
</div>
<div class="admin-runtime-block p-3 mb-3">
<p class="text-uppercase small fw-semibold text-secondary mb-3">Contexto atual</p>
<div class="d-grid gap-3 small">
<div class="d-flex justify-content-between gap-3"><span class="text-secondary">Aplicacao</span><strong class="text-end">{escape(view.app_name)}</strong></div>
<div class="d-flex justify-content-between gap-3"><span class="text-secondary">Ambiente</span><strong class="text-end text-uppercase">{escape(view.environment)}</strong></div>
<div class="d-flex justify-content-between gap-3"><span class="text-secondary">Versao</span><strong class="text-end">{escape(view.version)}</strong></div>
<div class="d-flex justify-content-between gap-3"><span class="text-secondary">Ultima leitura</span><strong class="text-end" data-bot-monitoring-last-sync>Aguardando</strong></div>
</div>
</div>
<div class="admin-tool-review-note p-4 mb-3">
<p class="text-uppercase small fw-semibold text-secondary mb-2">Escopo liberado</p>
<ul class="small text-secondary ps-3 mb-0">{access_notes_markup}</ul>
</div>
<div class="admin-tool-review-note p-4">
<p class="text-uppercase small fw-semibold text-secondary mb-2">Como ler esta tela</p>
<ul class="small text-secondary ps-3 mb-0">{reading_notes_markup}</ul>
</div>
</div>
</div>
</aside>
<section class="col-12 col-xl-8 col-xxl-9">
<div class="card border-0 shadow-sm admin-hero-card overflow-hidden mb-4">
<div class="card-body p-4 p-lg-5">
<div class="d-flex flex-wrap justify-content-between align-items-start gap-3">
<div>
<div class="d-flex flex-wrap gap-2 mb-3">
<span class="badge rounded-pill bg-white text-dark border">Observabilidade operacional</span>
<span class="badge rounded-pill bg-dark-subtle text-dark-emphasis border border-dark-subtle">Fase 4</span>
</div>
<h2 class="display-5 fw-semibold mb-3">Fluxo do bot e saude do atendimento na mesma tela</h2>
<p class="lead text-secondary mb-0">Acompanhe o basico da operacao e da telemetria sem entrar em detalhes de infraestrutura.</p>
</div>
<button class="btn btn-outline-dark rounded-pill px-4" type="button" data-admin-bot-monitoring-refresh>
<span data-bot-monitoring-refresh-label>Atualizar leitura</span>
<span class="spinner-border spinner-border-sm d-none" data-bot-monitoring-refresh-spinner aria-hidden="true"></span>
</button>
</div>
</div>
</div>
<div class="alert d-none rounded-4 mb-4" id="admin-bot-monitoring-feedback" role="status"></div>
<div class="row g-3 mb-4">
<div class="col-12 col-md-6 col-xxl-3"><div class="card border-0 shadow-sm admin-metric-card h-100"><div class="card-body p-4"><p class="small text-uppercase fw-semibold text-secondary mb-3">Relatorios de fluxo</p><div class="display-6 fw-semibold mb-2" data-bot-flow-report-count>0</div><p class="text-secondary mb-0">Status, roteamento, tools, fallback e falhas do turno.</p></div></div></div>
<div class="col-12 col-md-6 col-xxl-3"><div class="card border-0 shadow-sm admin-metric-card h-100"><div class="card-body p-4"><p class="small text-uppercase fw-semibold text-secondary mb-3">Relatorios de saude</p><div class="display-6 fw-semibold mb-2" data-bot-telemetry-report-count>0</div><p class="text-secondary mb-0">Volume, latencia, distribuicao por dominio e saude do atendimento.</p></div></div></div>
<div class="col-12 col-md-6 col-xxl-3"><div class="card border-0 shadow-sm admin-metric-card h-100"><div class="card-body p-4"><p class="small text-uppercase fw-semibold text-secondary mb-3">Bases de leitura</p><div class="display-6 fw-semibold mb-2" data-bot-monitoring-dataset-count>0</div><p class="text-secondary mb-0">Base consolidada usada pelas duas visoes.</p></div></div></div>
<div class="col-12 col-md-6 col-xxl-3"><div class="card border-0 shadow-sm admin-metric-card h-100"><div class="card-body p-4"><p class="small text-uppercase fw-semibold text-secondary mb-3">Atualizacao</p><div class="h4 fw-semibold mb-2" data-bot-monitoring-sync-strategy>--</div><p class="text-secondary mb-0">Ritmo atual da carga exibida no painel.</p></div></div></div>
</div>
<div class="row g-4">
<div class="col-12 col-xxl-6">
<div id="bot-flow-monitoring-card" class="card border-0 shadow-sm admin-surface-card h-100">
<div class="card-body p-4 p-lg-5 d-flex flex-column gap-4">
<div>
<p class="text-uppercase small fw-semibold text-secondary mb-2">Fluxo do bot</p>
<h3 class="h3 fw-semibold mb-2">Triagem da operacao</h3>
<p class="text-secondary mb-0">Veja status, roteamento, uso de tools, fallback, handoff e falhas do turno.</p>
</div>
<div class="admin-bot-monitoring-stack" data-bot-flow-overview-metrics>
<div class="admin-tool-empty-state rounded-4 p-4"><h4 class="h5 fw-semibold mb-2">Fluxo aguardando leitura</h4><p class="text-secondary mb-0">Clique em atualizar leitura para carregar o snapshot operacional do bot.</p></div>
</div>
<div class="admin-bot-monitoring-stack" data-bot-flow-materialization></div>
<div class="admin-bot-monitoring-grid" data-bot-flow-report-list></div>
<div>
<div class="small text-uppercase fw-semibold text-secondary mb-2">Proximas melhorias</div>
<div class="admin-bot-monitoring-list" data-bot-flow-next-steps></div>
</div>
</div>
</div>
</div>
<div class="col-12 col-xxl-6">
<div class="card border-0 shadow-sm admin-surface-card h-100">
<div class="card-body p-4 p-lg-5 d-flex flex-column gap-4">
<div>
<p class="text-uppercase small fw-semibold text-secondary mb-2">Telemetria conversacional</p>
<h3 class="h3 fw-semibold mb-2">Saude do atendimento</h3>
<p class="text-secondary mb-0">Volume, latencia, distribuicao por dominio e sinais de saude da conversa.</p>
</div>
<div class="admin-bot-monitoring-stack" data-bot-telemetry-overview-metrics>
<div class="admin-tool-empty-state rounded-4 p-4"><h4 class="h5 fw-semibold mb-2">Telemetria aguardando leitura</h4><p class="text-secondary mb-0">Clique em atualizar leitura para carregar o snapshot conversacional.</p></div>
</div>
<div class="admin-bot-monitoring-stack" data-bot-telemetry-materialization></div>
<div class="admin-bot-monitoring-grid" data-bot-telemetry-report-list></div>
<div>
<div class="small text-uppercase fw-semibold text-secondary mb-2">Proximas melhorias</div>
<div class="admin-bot-monitoring-list" data-bot-telemetry-next-steps></div>
</div>
</div>
</div>
</div>
</div>
</section>
</div>
</div>
<script src="{BOOTSTRAP_JS_HREF}" defer></script>
<script src="{escape(js_href, quote=True)}" defer></script>
</body>
</html>
'''

@ -6,13 +6,18 @@ from admin_app.core import AdminSettings, AuthenticatedStaffContext, get_admin_s
from admin_app.services import ToolManagementService
from admin_app.view.assets import PANEL_STATIC_MOUNT_NAME
from admin_app.view.rendering import (
render_bot_monitoring_page,
render_collaborator_management_page,
render_login_page,
render_panel_home,
render_rental_reports_page,
render_sales_revenue_reports_page,
render_system_configuration_page,
render_tool_intake_page,
render_tool_review_page,
)
from admin_app.view.view_models import (
AdminBotMonitoringPageView,
AdminCollaboratorManagementPageView,
AdminLoginPageView,
AdminPanelHomeView,
@ -22,6 +27,9 @@ from admin_app.view.view_models import (
AdminPanelQuickAction,
AdminPanelRoadmapItem,
AdminPanelSurfaceLink,
AdminRentalReportsPageView,
AdminSalesRevenueReportsPageView,
AdminSystemConfigurationPageView,
AdminToolIntakeDomainOption,
AdminToolIntakePageView,
AdminToolIntakeParameterTypeOption,
@ -119,6 +127,74 @@ def collaborator_management_page(
return HTMLResponse(render_collaborator_management_page(view, css_href=css_href, js_href=js_href))
@panel_router.get("/panel/sistema/configuracoes", response_class=HTMLResponse, name="admin_system_configuration_view")
def system_configuration_page(
request: Request,
current_context: AuthenticatedStaffContext | None = Depends(get_optional_panel_staff_context),
) -> Response:
if current_context is None:
return _redirect_to_route(request, "admin_login_view")
if not role_has_permission(current_context.principal.role, AdminPermission.VIEW_SYSTEM):
return _redirect_to_route(request, "panel_home")
settings = _resolve_settings(request)
view = _build_system_configuration_view(request, settings)
css_href = str(request.url_for(PANEL_STATIC_MOUNT_NAME, path="styles/panel.css"))
js_href = str(request.url_for(PANEL_STATIC_MOUNT_NAME, path="scripts/panel.js"))
return HTMLResponse(render_system_configuration_page(view, css_href=css_href, js_href=js_href))
@panel_router.get("/panel/relatorios/vendas-arrecadacao", response_class=HTMLResponse, name="admin_sales_revenue_reports_view")
def sales_revenue_reports_page(
request: Request,
current_context: AuthenticatedStaffContext | None = Depends(get_optional_panel_staff_context),
) -> Response:
if current_context is None:
return _redirect_to_route(request, "admin_login_view")
if not role_has_permission(current_context.principal.role, AdminPermission.VIEW_REPORTS):
return _redirect_to_route(request, "panel_home")
settings = _resolve_settings(request)
view = _build_sales_revenue_reports_view(request, settings)
css_href = str(request.url_for(PANEL_STATIC_MOUNT_NAME, path="styles/panel.css"))
js_href = str(request.url_for(PANEL_STATIC_MOUNT_NAME, path="scripts/panel.js"))
return HTMLResponse(render_sales_revenue_reports_page(view, css_href=css_href, js_href=js_href))
@panel_router.get("/panel/relatorios/locacao", response_class=HTMLResponse, name="admin_rental_reports_view")
def rental_reports_page(
request: Request,
current_context: AuthenticatedStaffContext | None = Depends(get_optional_panel_staff_context),
) -> Response:
if current_context is None:
return _redirect_to_route(request, "admin_login_view")
if not role_has_permission(current_context.principal.role, AdminPermission.VIEW_REPORTS):
return _redirect_to_route(request, "panel_home")
settings = _resolve_settings(request)
view = _build_rental_reports_view(request, settings)
css_href = str(request.url_for(PANEL_STATIC_MOUNT_NAME, path="styles/panel.css"))
js_href = str(request.url_for(PANEL_STATIC_MOUNT_NAME, path="scripts/panel.js"))
return HTMLResponse(render_rental_reports_page(view, css_href=css_href, js_href=js_href))
@panel_router.get("/panel/monitoramento/bot", response_class=HTMLResponse, name="admin_bot_monitoring_view")
def bot_monitoring_page(
request: Request,
current_context: AuthenticatedStaffContext | None = Depends(get_optional_panel_staff_context),
) -> Response:
if current_context is None:
return _redirect_to_route(request, "admin_login_view")
if not role_has_permission(current_context.principal.role, AdminPermission.VIEW_REPORTS):
return _redirect_to_route(request, "panel_home")
settings = _resolve_settings(request)
view = _build_bot_monitoring_view(request, settings)
css_href = str(request.url_for(PANEL_STATIC_MOUNT_NAME, path="styles/panel.css"))
js_href = str(request.url_for(PANEL_STATIC_MOUNT_NAME, path="scripts/panel.js"))
return HTMLResponse(render_bot_monitoring_page(view, css_href=css_href, js_href=js_href))
def _build_home_view(
request: Request,
settings: AdminSettings,
@ -128,7 +204,10 @@ def _build_home_view(
tool_intake_view_href = str(request.url_for("admin_tool_intake_view"))
tool_review_view_href = str(request.url_for("admin_tool_review_view"))
collaborator_management_view_href = str(request.url_for("admin_collaborator_management_view"))
system_configuration_href = _build_prefixed_path(settings.admin_api_prefix, "/system/configuration")
system_configuration_view_href = str(request.url_for("admin_system_configuration_view"))
sales_revenue_reports_view_href = str(request.url_for("admin_sales_revenue_reports_view"))
rental_reports_view_href = str(request.url_for("admin_rental_reports_view"))
bot_monitoring_view_href = str(request.url_for("admin_bot_monitoring_view"))
audit_href = _build_prefixed_path(settings.admin_api_prefix, "/audit/events")
can_manage_collaborators = role_has_permission(
current_context.principal.role,
@ -155,6 +234,30 @@ def _build_home_view(
description="Fluxo humano de revisao, aprovacao e ativacao.",
badge="Operacao",
),
AdminPanelNavigationItem(
label="Configuracoes do sistema",
href=system_configuration_view_href,
description="Leitura funcional, runtime e governanca do admin em uma tela dedicada.",
badge="Fase 4",
),
AdminPanelNavigationItem(
label="Relatorios comerciais",
href=sales_revenue_reports_view_href,
description="Tela combinada para vendas e arrecadacao dentro da sessao do painel.",
badge="Relatorios",
),
AdminPanelNavigationItem(
label="Relatorios de locacao",
href=rental_reports_view_href,
description="Tela dedicada para frota, contratos e ocupacao na sessao do painel.",
badge="Locacao",
),
AdminPanelNavigationItem(
label="Monitoramento do bot",
href=bot_monitoring_view_href,
description="Fluxo operacional e telemetria conversacional em uma superficie dedicada.",
badge="Bot",
),
AdminPanelNavigationItem(
label="Areas do sistema",
href="#modules",
@ -180,10 +283,25 @@ def _build_home_view(
button_class="btn-outline-dark",
),
AdminPanelQuickAction(
label="Ver areas",
href="#modules",
label="Config. sistema",
href=system_configuration_view_href,
button_class="btn-outline-secondary",
),
AdminPanelQuickAction(
label="Relatorios",
href=sales_revenue_reports_view_href,
button_class="btn-outline-dark",
),
AdminPanelQuickAction(
label="Locacao",
href=rental_reports_view_href,
button_class="btn-outline-dark",
),
AdminPanelQuickAction(
label="Monitorar bot",
href=bot_monitoring_view_href,
button_class="btn-outline-dark",
),
]
modules = [
AdminPanelModuleCard(
@ -219,14 +337,62 @@ def _build_home_view(
AdminPanelModuleCard(
eyebrow="Acompanhamento",
title="Configuracao do sistema",
description="Snapshot do runtime administrativo, politicas de seguranca e dados de sessao do painel.",
status_label="API pronta",
status_variant="secondary",
description="Tela dedicada para consultar configuracao funcional, runtime administrativo e governanca do sistema sem sair do painel.",
status_label="Tela ativa",
status_variant="primary",
highlights=(
"Runtime e banco monitorados",
"Politicas de credencial centralizadas",
"Base pronta para futura tela dedicada",
"Catalogo funcional e governanca do bot conectados",
"Runtime e seguranca carregados por permissao",
"Separacao de modelos visivel na mesma superficie",
),
cta_label="Abrir configuracoes",
href=system_configuration_view_href,
is_available=True,
),
AdminPanelModuleCard(
eyebrow="Relatorios operacionais",
title="Relatorios de vendas e arrecadacao",
description="Tela dedicada para acompanhar o bootstrap de vendas e arrecadacao com leitura pronta para sessao web do painel.",
status_label="Tela ativa",
status_variant="info",
highlights=(
"Vendas e arrecadacao na mesma superficie",
"Leitura protegida por sessao web",
"Materializacao e proximos passos visiveis na UI",
),
cta_label="Abrir relatorios",
href=sales_revenue_reports_view_href,
is_available=True,
),
AdminPanelModuleCard(
eyebrow="Relatorios operacionais",
title="Relatorios de locacao",
description="Tela dedicada para acompanhar a estrutura inicial de frota e contratos de locacao pela sessao web do painel.",
status_label="Tela ativa",
status_variant="info",
highlights=(
"Frota e contratos lidos na mesma superficie",
"Leitura protegida por sessao web",
"Catalogo inicial e materializacao visiveis na UI",
),
cta_label="Abrir locacao",
href=rental_reports_view_href,
is_available=True,
),
AdminPanelModuleCard(
eyebrow="Monitoramento operacional",
title="Monitoramento do bot",
description="Tela dedicada para acompanhar fluxo operacional e telemetria conversacional pela sessao web do painel.",
status_label="Tela ativa",
status_variant="info",
highlights=(
"Fluxo do bot e telemetria na mesma superficie",
"Leitura protegida por sessao web",
"Materializacao e proximos passos visiveis na UI",
),
cta_label="Abrir monitoramento",
href=bot_monitoring_view_href,
is_available=True,
),
AdminPanelModuleCard(
eyebrow="Governanca",
@ -261,10 +427,28 @@ def _build_home_view(
description="Area com fila, contrato e catalogo ativo para tomada de decisao.",
),
AdminPanelSurfaceLink(
method="Runtime",
label="Configuracao do sistema",
href=system_configuration_href,
description="Snapshot tecnico do ambiente, mantido como superficie protegida enquanto a tela visual nao chega.",
method="Sistema",
label="Configuracoes do sistema",
href=system_configuration_view_href,
description="Tela dedicada para leitura funcional, runtime e governanca administrativa.",
),
AdminPanelSurfaceLink(
method="Relatorios",
label="Vendas e arrecadacao",
href=sales_revenue_reports_view_href,
description="Tela combinada para a primeira camada visual dos relatorios comerciais da fase 4.",
),
AdminPanelSurfaceLink(
method="Relatorios",
label="Locacao",
href=rental_reports_view_href,
description="Tela dedicada para a estrutura inicial de frota, contratos e ocupacao da fase 4.",
),
AdminPanelSurfaceLink(
method="Monitoramento",
label="Bot operacional",
href=bot_monitoring_view_href,
description="Tela dedicada para fluxo operacional e telemetria conversacional do bot na fase 4.",
),
AdminPanelSurfaceLink(
method="Auditoria",
@ -575,6 +759,123 @@ def _build_tool_review_view(request: Request, settings: AdminSettings) -> AdminT
)
def _build_sales_revenue_reports_view(
request: Request,
settings: AdminSettings,
) -> AdminSalesRevenueReportsPageView:
return AdminSalesRevenueReportsPageView(
app_name=settings.admin_app_name,
title="Relatorios de vendas e arrecadacao",
subtitle=(
"Visao unica para acompanhar vendas e arrecadacao no painel administrativo."
),
environment=settings.admin_environment,
version=settings.admin_version,
dashboard_href=str(request.url_for("panel_home")),
sales_overview_endpoint=_build_prefixed_path(settings.admin_api_prefix, "/panel/reports/sales/overview"),
revenue_overview_endpoint=_build_prefixed_path(settings.admin_api_prefix, "/panel/reports/arrecadacao/overview"),
access_notes=(
"A equipe interna pode consultar esta tela com a permissao de relatorios.",
"Os dados exibidos aparecem de forma consolidada para leitura segura no admin.",
"Vendas e arrecadacao ficam juntas para facilitar a rotina do time.",
),
reading_notes=(
"Comece pelos indicadores de cada bloco para ter uma leitura rapida.",
"Use os cards para entender rapidamente o foco de cada relatorio.",
"A area de proximas melhorias mostra o que entra nas etapas seguintes.",
),
)
def _build_rental_reports_view(
request: Request,
settings: AdminSettings,
) -> AdminRentalReportsPageView:
return AdminRentalReportsPageView(
app_name=settings.admin_app_name,
title="Relatorios de locacao",
subtitle=(
"Visao dedicada da locacao para acompanhar frota, contratos e receita operacional."
),
environment=settings.admin_environment,
version=settings.admin_version,
dashboard_href=str(request.url_for("panel_home")),
overview_endpoint=_build_prefixed_path(settings.admin_api_prefix, "/panel/reports/locacao/overview"),
access_notes=(
"A equipe interna pode consultar esta tela com a permissao de relatorios.",
"Os dados exibidos aparecem de forma consolidada para leitura segura no admin.",
"Locacao fica em uma tela propria para manter a leitura mais organizada.",
),
reading_notes=(
"Comece pelos indicadores principais para entender o momento da operacao.",
"Use os cards para ver rapidamente os temas cobertos nesta etapa.",
"A area de proximas melhorias indica o que ainda entra nas proximas entregas.",
),
)
def _build_bot_monitoring_view(
request: Request,
settings: AdminSettings,
) -> AdminBotMonitoringPageView:
return AdminBotMonitoringPageView(
app_name=settings.admin_app_name,
title="Monitoramento operacional do bot",
subtitle=(
"Painel unico para acompanhar o fluxo do bot e a telemetria do atendimento."
),
environment=settings.admin_environment,
version=settings.admin_version,
dashboard_href=str(request.url_for("panel_home")),
bot_flow_overview_endpoint=_build_prefixed_path(settings.admin_api_prefix, "/panel/reports/fluxo-bot/overview"),
telemetry_overview_endpoint=_build_prefixed_path(settings.admin_api_prefix, "/panel/reports/telemetria-conversacional/overview"),
access_notes=(
"A equipe interna pode consultar esta tela com a permissao de relatorios.",
"Os dados exibidos aparecem de forma consolidada para leitura segura no painel.",
"Fluxo e telemetria ficam juntos para agilizar a analise da operacao.",
),
reading_notes=(
"Comece pelo fluxo do bot para localizar status, desvios e pontos de atencao.",
"Use a telemetria para acompanhar volume, latencia e saude do atendimento.",
"A area de proximas melhorias resume o que ainda entra antes dos dashboards completos.",
),
)
def _build_system_configuration_view(
request: Request,
settings: AdminSettings,
) -> AdminSystemConfigurationPageView:
return AdminSystemConfigurationPageView(
app_name=settings.admin_app_name,
title="Configuracoes do sistema",
subtitle=(
"Visao unica do catalogo funcional, das regras do atendimento e das protecoes do painel."
),
environment=settings.admin_environment,
version=settings.admin_version,
dashboard_href=str(request.url_for("panel_home")),
overview_endpoint=_build_prefixed_path(settings.admin_api_prefix, "/system/configuration"),
runtime_endpoint=_build_prefixed_path(settings.admin_api_prefix, "/system/configuration/runtime"),
security_endpoint=_build_prefixed_path(settings.admin_api_prefix, "/system/configuration/security"),
model_runtimes_endpoint=_build_prefixed_path(settings.admin_api_prefix, "/system/configuration/model-runtimes"),
functional_endpoint=_build_prefixed_path(settings.admin_api_prefix, "/system/configuration/functional"),
functional_detail_base=_build_prefixed_path(settings.admin_api_prefix, "/system/configuration/functional"),
bot_governance_endpoint=_build_prefixed_path(settings.admin_api_prefix, "/system/configuration/functional/bot-governance"),
access_notes=(
"A equipe interna ja consegue consultar o catalogo funcional e os ajustes do atendimento nesta tela.",
"Detalhes mais sensiveis do ambiente continuam reservados para perfis com permissao elevada.",
"Toda a superficie segue somente leitura nesta etapa.",
),
governance_notes=(
"Configuracoes do atendimento e da geracao de tools aparecem separadas para evitar confusao.",
"Os ajustes do bot continuam bloqueados para escrita direta nas tabelas operacionais.",
"O foco aqui e leitura clara do estado atual antes da futura tela de edicao.",
),
)
def _build_collaborator_management_view(
request: Request,
settings: AdminSettings,

@ -4,6 +4,10 @@ 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);
@ -21,6 +25,22 @@ 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"]');
@ -554,6 +574,672 @@ function mountCollaboratorBoard(board) {
}
}
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",

@ -1,4 +1,4 @@
:root {
:root {
--admin-bg: #f6f1e8;
--admin-surface: rgba(255, 255, 255, 0.84);
--admin-surface-strong: rgba(255, 255, 255, 0.92);
@ -321,3 +321,158 @@ body.admin-view-body {
display: grid;
gap: 0.45rem;
}
.admin-system-page .admin-hero-card::after {
background: radial-gradient(circle, rgba(20, 77, 71, 0.22), transparent 72%);
}
.admin-system-grid {
display: grid;
gap: 1rem;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}
.admin-system-stack {
display: grid;
gap: 1rem;
}
.admin-system-item {
background: var(--admin-surface-strong);
border: 1px solid var(--admin-line);
border-radius: 1.35rem;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.5);
}
.admin-system-meta {
display: grid;
gap: 0.45rem;
}
.admin-system-chip-group {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.admin-commercial-reports-page .admin-hero-card::after {
background: radial-gradient(circle, rgba(193, 106, 51, 0.2), transparent 72%);
}
.admin-commercial-grid {
display: grid;
gap: 1rem;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}
.admin-commercial-stack,
.admin-commercial-list {
display: grid;
gap: 1rem;
}
.admin-commercial-item {
background: var(--admin-surface-strong);
border: 1px solid var(--admin-line);
border-radius: 1.35rem;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.5);
}
.admin-commercial-meta {
display: grid;
gap: 0.45rem;
}
.admin-commercial-chip-group {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.admin-rental-reports-page .admin-hero-card::after {
background: radial-gradient(circle, rgba(38, 88, 132, 0.2), transparent 72%);
}
.admin-rental-grid {
display: grid;
gap: 1rem;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}
.admin-rental-stack,
.admin-rental-list {
display: grid;
gap: 1rem;
}
.admin-rental-item {
background: var(--admin-surface-strong);
border: 1px solid var(--admin-line);
border-radius: 1.35rem;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.5);
}
.admin-rental-meta {
display: grid;
gap: 0.45rem;
}
.admin-rental-chip-group {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.admin-bot-monitoring-page .admin-hero-card::after {
background: radial-gradient(circle, rgba(22, 63, 58, 0.22), transparent 72%);
}
.admin-bot-monitoring-grid {
display: grid;
gap: 1rem;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}
.admin-bot-monitoring-stack,
.admin-bot-monitoring-list {
display: grid;
gap: 1rem;
}
.admin-bot-monitoring-item {
background: var(--admin-surface-strong);
border: 1px solid var(--admin-line);
border-radius: 1.35rem;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.5);
}
.admin-bot-monitoring-meta {
display: grid;
gap: 0.45rem;
}
.admin-bot-monitoring-chip-group {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.admin-system-item,
.admin-commercial-item,
.admin-rental-item,
.admin-bot-monitoring-item {
overflow-wrap: anywhere;
}
.admin-system-item h4,
.admin-commercial-item h4,
.admin-rental-item h4,
.admin-bot-monitoring-item h4 {
overflow-wrap: anywhere;
}
.admin-system-chip-group .badge,
.admin-commercial-chip-group .badge,
.admin-rental-chip-group .badge,
.admin-bot-monitoring-chip-group .badge {
white-space: normal;
text-align: left;
}

@ -145,3 +145,57 @@ class AdminCollaboratorManagementPageView(BaseModel):
password_policy_label: str
onboarding_notes: tuple[str, ...]
governance_notes: tuple[str, ...]
class AdminSystemConfigurationPageView(BaseModel):
app_name: str
title: str
subtitle: str
environment: str
version: str
dashboard_href: str
overview_endpoint: str
runtime_endpoint: str
security_endpoint: str
model_runtimes_endpoint: str
functional_endpoint: str
functional_detail_base: str
bot_governance_endpoint: str
access_notes: tuple[str, ...]
governance_notes: tuple[str, ...]
class AdminSalesRevenueReportsPageView(BaseModel):
app_name: str
title: str
subtitle: str
environment: str
version: str
dashboard_href: str
sales_overview_endpoint: str
revenue_overview_endpoint: str
access_notes: tuple[str, ...]
reading_notes: tuple[str, ...]
class AdminRentalReportsPageView(BaseModel):
app_name: str
title: str
subtitle: str
environment: str
version: str
dashboard_href: str
overview_endpoint: str
access_notes: tuple[str, ...]
reading_notes: tuple[str, ...]
class AdminBotMonitoringPageView(BaseModel):
app_name: str
title: str
subtitle: str
environment: str
version: str
dashboard_href: str
bot_flow_overview_endpoint: str
telemetry_overview_endpoint: str
access_notes: tuple[str, ...]
reading_notes: tuple[str, ...]

@ -0,0 +1,65 @@
import unittest
from fastapi.testclient import TestClient
from admin_app.api.dependencies import get_optional_panel_staff_context
from admin_app.app_factory import create_app
from admin_app.core import AdminSettings, AuthenticatedStaffContext, AuthenticatedStaffPrincipal
from shared.contracts import StaffRole
def _build_panel_context(role: StaffRole = StaffRole.COLABORADOR) -> AuthenticatedStaffContext:
return AuthenticatedStaffContext(
principal=AuthenticatedStaffPrincipal(
id=41 if role == StaffRole.COLABORADOR else 42,
email="colaborador@empresa.com" if role == StaffRole.COLABORADOR else "diretor@empresa.com",
display_name="Equipe de Monitoramento do Bot",
role=role,
is_active=True,
),
session_id=101,
)
class AdminPanelBotMonitoringViewTests(unittest.TestCase):
def test_bot_monitoring_page_redirects_to_login_without_session(self):
app = create_app(AdminSettings(admin_api_prefix="/admin"))
client = TestClient(app)
response = client.get("/admin/panel/monitoramento/bot", follow_redirects=False)
self.assertEqual(response.status_code, 302)
self.assertTrue(response.headers["location"].endswith("/admin/login"))
def test_bot_monitoring_page_renders_for_colaborador_session(self):
app = create_app(AdminSettings(admin_app_name="Admin Interno", admin_version="1.4.0", admin_api_prefix="/admin"))
app.dependency_overrides[get_optional_panel_staff_context] = lambda: _build_panel_context(StaffRole.COLABORADOR)
client = TestClient(app)
try:
response = client.get("/admin/panel/monitoramento/bot")
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 200)
self.assertIn("Monitoramento operacional do bot", response.text)
self.assertIn('data-admin-bot-monitoring="true"', response.text)
self.assertIn('data-bot-flow-overview-endpoint="/admin/panel/reports/fluxo-bot/overview"', response.text)
self.assertIn('data-telemetry-overview-endpoint="/admin/panel/reports/telemetria-conversacional/overview"', response.text)
self.assertIn("Atualizar leitura", response.text)
def test_dashboard_exposes_bot_monitoring_screen_link(self):
app = create_app(AdminSettings(admin_api_prefix="/admin"))
app.dependency_overrides[get_optional_panel_staff_context] = lambda: _build_panel_context(StaffRole.DIRETOR)
client = TestClient(app)
try:
response = client.get("/admin/panel/admin")
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 200)
self.assertIn("/admin/panel/monitoramento/bot", response.text)
self.assertIn("Abrir monitoramento", response.text)
if __name__ == "__main__":
unittest.main()

@ -0,0 +1,64 @@
import unittest
from fastapi.testclient import TestClient
from admin_app.api.dependencies import get_optional_panel_staff_context
from admin_app.app_factory import create_app
from admin_app.core import AdminSettings, AuthenticatedStaffContext, AuthenticatedStaffPrincipal
from shared.contracts import StaffRole
def _build_panel_context(role: StaffRole = StaffRole.COLABORADOR) -> AuthenticatedStaffContext:
return AuthenticatedStaffContext(
principal=AuthenticatedStaffPrincipal(
id=31 if role == StaffRole.COLABORADOR else 32,
email="colaborador@empresa.com" if role == StaffRole.COLABORADOR else "diretor@empresa.com",
display_name="Equipe de Locacao Interna",
role=role,
is_active=True,
),
session_id=97,
)
class AdminPanelRentalReportsViewTests(unittest.TestCase):
def test_rental_reports_page_redirects_to_login_without_session(self):
app = create_app(AdminSettings(admin_api_prefix="/admin"))
client = TestClient(app)
response = client.get("/admin/panel/relatorios/locacao", follow_redirects=False)
self.assertEqual(response.status_code, 302)
self.assertTrue(response.headers["location"].endswith("/admin/login"))
def test_rental_reports_page_renders_for_colaborador_session(self):
app = create_app(AdminSettings(admin_app_name="Admin Interno", admin_version="1.4.0", admin_api_prefix="/admin"))
app.dependency_overrides[get_optional_panel_staff_context] = lambda: _build_panel_context(StaffRole.COLABORADOR)
client = TestClient(app)
try:
response = client.get("/admin/panel/relatorios/locacao")
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 200)
self.assertIn("Relatorios de locacao", response.text)
self.assertIn('data-admin-rental-reports="true"', response.text)
self.assertIn('data-rental-overview-endpoint="/admin/panel/reports/locacao/overview"', response.text)
self.assertIn("Atualizar leitura", response.text)
def test_dashboard_exposes_rental_reports_screen_link(self):
app = create_app(AdminSettings(admin_api_prefix="/admin"))
app.dependency_overrides[get_optional_panel_staff_context] = lambda: _build_panel_context(StaffRole.DIRETOR)
client = TestClient(app)
try:
response = client.get("/admin/panel/admin")
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 200)
self.assertIn("/admin/panel/relatorios/locacao", response.text)
self.assertIn("Abrir locacao", response.text)
if __name__ == "__main__":
unittest.main()

@ -0,0 +1,118 @@
import unittest
from fastapi.testclient import TestClient
from admin_app.api.dependencies import get_current_panel_staff_principal
from admin_app.app_factory import create_app
from admin_app.core import AdminSettings, AuthenticatedStaffPrincipal
from shared.contracts import StaffRole
class AdminPanelReportsWebTests(unittest.TestCase):
def _build_client_with_role(
self,
role: StaffRole,
settings: AdminSettings | None = None,
) -> tuple[TestClient, object]:
app = create_app(
settings
or AdminSettings(
admin_auth_token_secret="test-secret",
admin_api_prefix="/admin",
)
)
app.dependency_overrides[get_current_panel_staff_principal] = lambda: AuthenticatedStaffPrincipal(
id=61,
email="colaborador@empresa.com" if role == StaffRole.COLABORADOR else "diretor@empresa.com",
display_name="Equipe de Relatorios Web",
role=role,
is_active=True,
)
return TestClient(app), app
def test_panel_reports_require_panel_session(self):
app = create_app(AdminSettings(admin_auth_token_secret="test-secret", admin_api_prefix="/admin"))
client = TestClient(app)
response = client.get("/admin/panel/reports/sales/overview")
self.assertEqual(response.status_code, 401)
self.assertEqual(response.json()["detail"], "Sessao administrativa web obrigatoria.")
def test_panel_sales_overview_is_available_for_colaborador(self):
client, app = self._build_client_with_role(StaffRole.COLABORADOR)
try:
response = client.get("/admin/panel/reports/sales/overview")
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 200)
payload = response.json()
self.assertEqual(payload["mode"], "sales_contract_bootstrap")
self.assertEqual(payload["source_dataset_keys"], ["sales_orders"])
self.assertEqual(len(payload["reports"]), 4)
self.assertEqual(payload["materialization"]["sync_strategy"], "etl_incremental")
def test_panel_revenue_overview_is_available_for_colaborador(self):
client, app = self._build_client_with_role(StaffRole.COLABORADOR)
try:
response = client.get("/admin/panel/reports/arrecadacao/overview")
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 200)
payload = response.json()
self.assertEqual(payload["area"], "arrecadacao")
self.assertEqual(payload["source_dataset_keys"], ["rental_payments"])
self.assertEqual(len(payload["reports"]), 3)
self.assertEqual(payload["materialization"]["storage_shape"], "snapshot_table")
def test_panel_rental_overview_is_available_for_colaborador(self):
client, app = self._build_client_with_role(StaffRole.COLABORADOR)
try:
response = client.get("/admin/panel/reports/locacao/overview")
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 200)
payload = response.json()
self.assertEqual(payload["area"], "locacao")
self.assertEqual(payload["source_domain"], "rental")
self.assertEqual(payload["source_dataset_keys"], ["rental_fleet", "rental_contracts"])
self.assertEqual(len(payload["reports"]), 5)
self.assertEqual(payload["materialization"]["sync_strategy"], "etl_incremental")
def test_panel_bot_flow_overview_is_available_for_colaborador(self):
client, app = self._build_client_with_role(StaffRole.COLABORADOR)
try:
response = client.get("/admin/panel/reports/fluxo-bot/overview")
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 200)
payload = response.json()
self.assertEqual(payload["area"], "fluxo_bot")
self.assertEqual(payload["source_domain"], "conversation")
self.assertEqual(payload["source_dataset_keys"], ["conversation_turns"])
self.assertEqual(len(payload["reports"]), 5)
self.assertEqual(payload["materialization"]["sync_strategy"], "etl_incremental")
def test_panel_conversation_telemetry_overview_is_available_for_colaborador(self):
client, app = self._build_client_with_role(StaffRole.COLABORADOR)
try:
response = client.get("/admin/panel/reports/telemetria-conversacional/overview")
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 200)
payload = response.json()
self.assertEqual(payload["area"], "telemetria_conversacional")
self.assertEqual(payload["source_domain"], "conversation")
self.assertEqual(payload["source_dataset_keys"], ["conversation_turns"])
self.assertEqual(len(payload["reports"]), 5)
self.assertEqual(payload["materialization"]["sync_strategy"], "etl_incremental")
if __name__ == "__main__":
unittest.main()

@ -0,0 +1,65 @@
import unittest
from fastapi.testclient import TestClient
from admin_app.api.dependencies import get_optional_panel_staff_context
from admin_app.app_factory import create_app
from admin_app.core import AdminSettings, AuthenticatedStaffContext, AuthenticatedStaffPrincipal
from shared.contracts import StaffRole
def _build_panel_context(role: StaffRole = StaffRole.COLABORADOR) -> AuthenticatedStaffContext:
return AuthenticatedStaffContext(
principal=AuthenticatedStaffPrincipal(
id=27 if role == StaffRole.COLABORADOR else 28,
email="colaborador@empresa.com" if role == StaffRole.COLABORADOR else "diretor@empresa.com",
display_name="Equipe Comercial Interna",
role=role,
is_active=True,
),
session_id=92,
)
class AdminPanelSalesRevenueReportsViewTests(unittest.TestCase):
def test_sales_revenue_reports_page_redirects_to_login_without_session(self):
app = create_app(AdminSettings(admin_api_prefix="/admin"))
client = TestClient(app)
response = client.get("/admin/panel/relatorios/vendas-arrecadacao", follow_redirects=False)
self.assertEqual(response.status_code, 302)
self.assertTrue(response.headers["location"].endswith("/admin/login"))
def test_sales_revenue_reports_page_renders_for_colaborador_session(self):
app = create_app(AdminSettings(admin_app_name="Admin Interno", admin_version="1.4.0", admin_api_prefix="/admin"))
app.dependency_overrides[get_optional_panel_staff_context] = lambda: _build_panel_context(StaffRole.COLABORADOR)
client = TestClient(app)
try:
response = client.get("/admin/panel/relatorios/vendas-arrecadacao")
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 200)
self.assertIn("Relatorios de vendas e arrecadacao", response.text)
self.assertIn('data-admin-sales-revenue-reports="true"', response.text)
self.assertIn('data-sales-overview-endpoint="/admin/panel/reports/sales/overview"', response.text)
self.assertIn('data-revenue-overview-endpoint="/admin/panel/reports/arrecadacao/overview"', response.text)
self.assertIn("Atualizar leitura", response.text)
def test_dashboard_exposes_sales_revenue_reports_screen_link(self):
app = create_app(AdminSettings(admin_api_prefix="/admin"))
app.dependency_overrides[get_optional_panel_staff_context] = lambda: _build_panel_context(StaffRole.DIRETOR)
client = TestClient(app)
try:
response = client.get("/admin/panel/admin")
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 200)
self.assertIn("/admin/panel/relatorios/vendas-arrecadacao", response.text)
self.assertIn("Abrir relatorios", response.text)
if __name__ == "__main__":
unittest.main()

@ -0,0 +1,82 @@
import unittest
from fastapi.testclient import TestClient
from admin_app.api.dependencies import get_optional_panel_staff_context
from admin_app.app_factory import create_app
from admin_app.core import AdminSettings, AuthenticatedStaffContext, AuthenticatedStaffPrincipal
from shared.contracts import StaffRole
def _build_panel_context(role: StaffRole = StaffRole.COLABORADOR) -> AuthenticatedStaffContext:
return AuthenticatedStaffContext(
principal=AuthenticatedStaffPrincipal(
id=17 if role == StaffRole.COLABORADOR else 18,
email="colaborador@empresa.com" if role == StaffRole.COLABORADOR else "diretor@empresa.com",
display_name="Equipe de Configuracao",
role=role,
is_active=True,
),
session_id=91,
)
class AdminPanelSystemConfigurationWebTests(unittest.TestCase):
def test_system_configuration_page_redirects_to_login_without_session(self):
app = create_app(AdminSettings(admin_api_prefix="/admin"))
client = TestClient(app)
response = client.get("/admin/panel/sistema/configuracoes", follow_redirects=False)
self.assertEqual(response.status_code, 302)
self.assertTrue(response.headers["location"].endswith("/admin/login"))
def test_system_configuration_page_renders_for_colaborador_session(self):
app = create_app(AdminSettings(admin_app_name="Admin Interno", admin_version="1.4.0", admin_api_prefix="/admin"))
app.dependency_overrides[get_optional_panel_staff_context] = lambda: _build_panel_context(StaffRole.COLABORADOR)
client = TestClient(app)
try:
response = client.get("/admin/panel/sistema/configuracoes")
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 200)
self.assertIn("Configuracoes do sistema", response.text)
self.assertIn('data-admin-system-configuration="true"', response.text)
self.assertIn('data-functional-endpoint="/admin/system/configuration/functional"', response.text)
self.assertIn('data-bot-governance-endpoint="/admin/system/configuration/functional/bot-governance"', response.text)
self.assertIn('data-runtime-endpoint="/admin/system/configuration/runtime"', response.text)
self.assertIn('data-security-endpoint="/admin/system/configuration/security"', response.text)
self.assertIn('data-model-runtimes-endpoint="/admin/system/configuration/model-runtimes"', response.text)
self.assertIn("Atualizar leitura", response.text)
def test_dashboard_exposes_system_configuration_screen_link(self):
app = create_app(AdminSettings(admin_api_prefix="/admin"))
app.dependency_overrides[get_optional_panel_staff_context] = lambda: _build_panel_context(StaffRole.DIRETOR)
client = TestClient(app)
try:
response = client.get("/admin/panel/admin")
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 200)
self.assertIn("/admin/panel/sistema/configuracoes", response.text)
self.assertIn("Abrir configuracoes", response.text)
def test_system_configuration_page_renders_for_director_session(self):
app = create_app(AdminSettings(admin_api_prefix="/admin"))
app.dependency_overrides[get_optional_panel_staff_context] = lambda: _build_panel_context(StaffRole.DIRETOR)
client = TestClient(app)
try:
response = client.get("/admin/panel/sistema/configuracoes")
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 200)
self.assertIn('data-overview-endpoint="/admin/system/configuration"', response.text)
self.assertIn('data-functional-detail-base="/admin/system/configuration/functional"', response.text)
self.assertIn("Somente leitura", response.text)
if __name__ == "__main__":
unittest.main()
Loading…
Cancel
Save