Compare commits

...

2 Commits

Author SHA1 Message Date
Vitor Hugo Belorio Simão d6e765ce3c 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.
2 weeks ago
Vitor Hugo Belorio Simão 9a31b0c5ae feat(admin): estruturar configuracao e relatorios da fase 4
Entrega a camada de backend da fase 4 com rotas administrativas para configuracao funcional do sistema, separacao explicita dos runtimes do atendimento e da geracao de tools, e as estruturas iniciais de relatorios de vendas, arrecadacao, locacao, fluxo do bot e telemetria conversacional.

Tambem adiciona a protecao de escrita no runtime administrativo para bloquear writes diretos nas tabelas operacionais do product, expoe esse snapshot no sistema e amplia a cobertura com testes web para configuracao, relatorios e governanca de escrita.
2 weeks ago

@ -5,7 +5,9 @@ from admin_app.api.routes.auth import router as auth_router
from admin_app.api.routes.collaborators import router as collaborators_router
from admin_app.api.routes.panel_auth import router as panel_auth_router
from admin_app.api.routes.panel_collaborators import router as panel_collaborators_router
from admin_app.api.routes.panel_reports import router as panel_reports_router
from admin_app.api.routes.panel_tools import router as panel_tools_router
from admin_app.api.routes.reports import router as reports_router
from admin_app.api.routes.system import router as system_router
from admin_app.api.routes.tools import router as tools_router
@ -13,8 +15,10 @@ api_router = APIRouter()
api_router.include_router(auth_router)
api_router.include_router(panel_auth_router)
api_router.include_router(panel_collaborators_router)
api_router.include_router(panel_reports_router)
api_router.include_router(panel_tools_router)
api_router.include_router(system_router)
api_router.include_router(reports_router)
api_router.include_router(collaborators_router)
api_router.include_router(tools_router)
api_router.include_router(audit_router)
api_router.include_router(audit_router)

@ -0,0 +1,143 @@
from fastapi import APIRouter, Depends
from admin_app.api.dependencies import get_settings, require_panel_admin_permission
from admin_app.api.schemas import (
AdminBotFlowReportOverviewResponse,
AdminConversationTelemetryReportOverviewResponse,
AdminRentalReportOverviewResponse,
AdminRevenueReportOverviewResponse,
AdminSalesReportOverviewResponse,
)
from admin_app.core import AdminSettings, AuthenticatedStaffPrincipal
from admin_app.services import ReportService
from shared.contracts import AdminPermission
router = APIRouter(prefix="/panel/reports", tags=["panel-reports"])
def _build_service(settings: AdminSettings) -> ReportService:
return ReportService(settings)
@router.get(
"/sales/overview",
response_model=AdminSalesReportOverviewResponse,
)
def panel_sales_reports_overview(
settings: AdminSettings = Depends(get_settings),
_: AuthenticatedStaffPrincipal = Depends(
require_panel_admin_permission(AdminPermission.VIEW_REPORTS)
),
):
service = _build_service(settings)
payload = service.build_sales_overview_payload()
return AdminSalesReportOverviewResponse(
service="orquestrador-admin",
domain=payload["domain"],
mode=payload["mode"],
source_dataset_keys=payload["source_dataset_keys"],
metrics=payload["metrics"],
materialization=payload["materialization"],
reports=payload["reports"],
next_steps=payload["next_steps"],
)
@router.get(
"/arrecadacao/overview",
response_model=AdminRevenueReportOverviewResponse,
)
def panel_revenue_reports_overview(
settings: AdminSettings = Depends(get_settings),
_: AuthenticatedStaffPrincipal = Depends(
require_panel_admin_permission(AdminPermission.VIEW_REPORTS)
),
):
service = _build_service(settings)
payload = service.build_revenue_overview_payload()
return AdminRevenueReportOverviewResponse(
service="orquestrador-admin",
area=payload["area"],
source_domain=payload["source_domain"],
mode=payload["mode"],
source_dataset_keys=payload["source_dataset_keys"],
metrics=payload["metrics"],
materialization=payload["materialization"],
reports=payload["reports"],
next_steps=payload["next_steps"],
)
@router.get(
"/locacao/overview",
response_model=AdminRentalReportOverviewResponse,
)
def panel_rental_reports_overview(
settings: AdminSettings = Depends(get_settings),
_: AuthenticatedStaffPrincipal = Depends(
require_panel_admin_permission(AdminPermission.VIEW_REPORTS)
),
):
service = _build_service(settings)
payload = service.build_rental_overview_payload()
return AdminRentalReportOverviewResponse(
service="orquestrador-admin",
area=payload["area"],
source_domain=payload["source_domain"],
mode=payload["mode"],
source_dataset_keys=payload["source_dataset_keys"],
metrics=payload["metrics"],
materialization=payload["materialization"],
reports=payload["reports"],
next_steps=payload["next_steps"],
)
@router.get(
"/fluxo-bot/overview",
response_model=AdminBotFlowReportOverviewResponse,
)
def panel_bot_flow_reports_overview(
settings: AdminSettings = Depends(get_settings),
_: AuthenticatedStaffPrincipal = Depends(
require_panel_admin_permission(AdminPermission.VIEW_REPORTS)
),
):
service = _build_service(settings)
payload = service.build_bot_flow_overview_payload()
return AdminBotFlowReportOverviewResponse(
service="orquestrador-admin",
area=payload["area"],
source_domain=payload["source_domain"],
mode=payload["mode"],
source_dataset_keys=payload["source_dataset_keys"],
metrics=payload["metrics"],
materialization=payload["materialization"],
reports=payload["reports"],
next_steps=payload["next_steps"],
)
@router.get(
"/telemetria-conversacional/overview",
response_model=AdminConversationTelemetryReportOverviewResponse,
)
def panel_conversation_telemetry_reports_overview(
settings: AdminSettings = Depends(get_settings),
_: AuthenticatedStaffPrincipal = Depends(
require_panel_admin_permission(AdminPermission.VIEW_REPORTS)
),
):
service = _build_service(settings)
payload = service.build_conversation_telemetry_overview_payload()
return AdminConversationTelemetryReportOverviewResponse(
service="orquestrador-admin",
area=payload["area"],
source_domain=payload["source_domain"],
mode=payload["mode"],
source_dataset_keys=payload["source_dataset_keys"],
metrics=payload["metrics"],
materialization=payload["materialization"],
reports=payload["reports"],
next_steps=payload["next_steps"],
)

@ -0,0 +1,477 @@
from fastapi import APIRouter, Depends, HTTPException, status
from admin_app.api.dependencies import get_settings, require_admin_permission
from admin_app.api.schemas import (
AdminBotFlowReportCatalogResponse,
AdminBotFlowReportOverviewResponse,
AdminBotFlowReportResponse,
AdminConversationTelemetryReportCatalogResponse,
AdminConversationTelemetryReportOverviewResponse,
AdminConversationTelemetryReportResponse,
AdminRentalReportCatalogResponse,
AdminRentalReportOverviewResponse,
AdminRentalReportResponse,
AdminReportDatasetListResponse,
AdminReportDatasetResponse,
AdminReportOverviewResponse,
AdminRevenueReportCatalogResponse,
AdminRevenueReportOverviewResponse,
AdminRevenueReportResponse,
AdminSalesReportCatalogResponse,
AdminSalesReportOverviewResponse,
AdminSalesReportResponse,
)
from admin_app.core import AdminSettings, AuthenticatedStaffPrincipal
from admin_app.services import ReportService
from shared.contracts import AdminPermission
router = APIRouter(prefix="/reports", tags=["reports"])
def _build_service(settings: AdminSettings) -> ReportService:
return ReportService(settings)
@router.get(
"/overview",
response_model=AdminReportOverviewResponse,
)
def reports_overview(
settings: AdminSettings = Depends(get_settings),
_: AuthenticatedStaffPrincipal = Depends(
require_admin_permission(AdminPermission.VIEW_REPORTS)
),
):
service = _build_service(settings)
payload = service.build_overview_payload()
return AdminReportOverviewResponse(
service="orquestrador-admin",
mode=payload["mode"],
metrics=payload["metrics"],
materialization=payload["materialization"],
report_families=payload["report_families"],
next_steps=payload["next_steps"],
)
@router.get(
"/datasets",
response_model=AdminReportDatasetListResponse,
)
def report_datasets(
settings: AdminSettings = Depends(get_settings),
_: AuthenticatedStaffPrincipal = Depends(
require_admin_permission(AdminPermission.VIEW_REPORTS)
),
):
service = _build_service(settings)
payload = service.list_datasets_payload()
return AdminReportDatasetListResponse(
service="orquestrador-admin",
source=payload["source"],
materialization=payload["materialization"],
datasets=payload["datasets"],
)
@router.get(
"/datasets/{dataset_key}",
response_model=AdminReportDatasetResponse,
)
def report_dataset_detail(
dataset_key: str,
settings: AdminSettings = Depends(get_settings),
_: AuthenticatedStaffPrincipal = Depends(
require_admin_permission(AdminPermission.VIEW_REPORTS)
),
):
service = _build_service(settings)
payload = service.get_dataset_payload(dataset_key)
if payload is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Dataset operacional nao encontrado para relatorio.",
)
return AdminReportDatasetResponse(
service="orquestrador-admin",
source=payload["source"],
materialization=payload["materialization"],
dataset=payload["dataset"],
)
@router.get(
"/sales/overview",
response_model=AdminSalesReportOverviewResponse,
)
def sales_reports_overview(
settings: AdminSettings = Depends(get_settings),
_: AuthenticatedStaffPrincipal = Depends(
require_admin_permission(AdminPermission.VIEW_REPORTS)
),
):
service = _build_service(settings)
payload = service.build_sales_overview_payload()
return AdminSalesReportOverviewResponse(
service="orquestrador-admin",
domain=payload["domain"],
mode=payload["mode"],
source_dataset_keys=payload["source_dataset_keys"],
metrics=payload["metrics"],
materialization=payload["materialization"],
reports=payload["reports"],
next_steps=payload["next_steps"],
)
@router.get(
"/sales/reports",
response_model=AdminSalesReportCatalogResponse,
)
def sales_reports_catalog(
settings: AdminSettings = Depends(get_settings),
_: AuthenticatedStaffPrincipal = Depends(
require_admin_permission(AdminPermission.VIEW_REPORTS)
),
):
service = _build_service(settings)
payload = service.list_sales_reports_payload()
return AdminSalesReportCatalogResponse(
service="orquestrador-admin",
domain=payload["domain"],
source=payload["source"],
materialization=payload["materialization"],
reports=payload["reports"],
)
@router.get(
"/sales/reports/{report_key}",
response_model=AdminSalesReportResponse,
)
def sales_report_detail(
report_key: str,
settings: AdminSettings = Depends(get_settings),
_: AuthenticatedStaffPrincipal = Depends(
require_admin_permission(AdminPermission.VIEW_REPORTS)
),
):
service = _build_service(settings)
payload = service.get_sales_report_payload(report_key)
if payload is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Relatorio de vendas nao encontrado.",
)
return AdminSalesReportResponse(
service="orquestrador-admin",
domain=payload["domain"],
source=payload["source"],
materialization=payload["materialization"],
report=payload["report"],
)
@router.get(
"/arrecadacao/overview",
response_model=AdminRevenueReportOverviewResponse,
)
def revenue_reports_overview(
settings: AdminSettings = Depends(get_settings),
_: AuthenticatedStaffPrincipal = Depends(
require_admin_permission(AdminPermission.VIEW_REPORTS)
),
):
service = _build_service(settings)
payload = service.build_revenue_overview_payload()
return AdminRevenueReportOverviewResponse(
service="orquestrador-admin",
area=payload["area"],
source_domain=payload["source_domain"],
mode=payload["mode"],
source_dataset_keys=payload["source_dataset_keys"],
metrics=payload["metrics"],
materialization=payload["materialization"],
reports=payload["reports"],
next_steps=payload["next_steps"],
)
@router.get(
"/arrecadacao/reports",
response_model=AdminRevenueReportCatalogResponse,
)
def revenue_reports_catalog(
settings: AdminSettings = Depends(get_settings),
_: AuthenticatedStaffPrincipal = Depends(
require_admin_permission(AdminPermission.VIEW_REPORTS)
),
):
service = _build_service(settings)
payload = service.list_revenue_reports_payload()
return AdminRevenueReportCatalogResponse(
service="orquestrador-admin",
area=payload["area"],
source_domain=payload["source_domain"],
source=payload["source"],
materialization=payload["materialization"],
reports=payload["reports"],
)
@router.get(
"/arrecadacao/reports/{report_key}",
response_model=AdminRevenueReportResponse,
)
def revenue_report_detail(
report_key: str,
settings: AdminSettings = Depends(get_settings),
_: AuthenticatedStaffPrincipal = Depends(
require_admin_permission(AdminPermission.VIEW_REPORTS)
),
):
service = _build_service(settings)
payload = service.get_revenue_report_payload(report_key)
if payload is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Relatorio de arrecadacao nao encontrado.",
)
return AdminRevenueReportResponse(
service="orquestrador-admin",
area=payload["area"],
source_domain=payload["source_domain"],
source=payload["source"],
materialization=payload["materialization"],
report=payload["report"],
)
@router.get(
"/locacao/overview",
response_model=AdminRentalReportOverviewResponse,
)
def rental_reports_overview(
settings: AdminSettings = Depends(get_settings),
_: AuthenticatedStaffPrincipal = Depends(
require_admin_permission(AdminPermission.VIEW_REPORTS)
),
):
service = _build_service(settings)
payload = service.build_rental_overview_payload()
return AdminRentalReportOverviewResponse(
service="orquestrador-admin",
area=payload["area"],
source_domain=payload["source_domain"],
mode=payload["mode"],
source_dataset_keys=payload["source_dataset_keys"],
metrics=payload["metrics"],
materialization=payload["materialization"],
reports=payload["reports"],
next_steps=payload["next_steps"],
)
@router.get(
"/locacao/reports",
response_model=AdminRentalReportCatalogResponse,
)
def rental_reports_catalog(
settings: AdminSettings = Depends(get_settings),
_: AuthenticatedStaffPrincipal = Depends(
require_admin_permission(AdminPermission.VIEW_REPORTS)
),
):
service = _build_service(settings)
payload = service.list_rental_reports_payload()
return AdminRentalReportCatalogResponse(
service="orquestrador-admin",
area=payload["area"],
source_domain=payload["source_domain"],
source=payload["source"],
materialization=payload["materialization"],
reports=payload["reports"],
)
@router.get(
"/locacao/reports/{report_key}",
response_model=AdminRentalReportResponse,
)
def rental_report_detail(
report_key: str,
settings: AdminSettings = Depends(get_settings),
_: AuthenticatedStaffPrincipal = Depends(
require_admin_permission(AdminPermission.VIEW_REPORTS)
),
):
service = _build_service(settings)
payload = service.get_rental_report_payload(report_key)
if payload is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Relatorio de locacao nao encontrado.",
)
return AdminRentalReportResponse(
service="orquestrador-admin",
area=payload["area"],
source_domain=payload["source_domain"],
source=payload["source"],
materialization=payload["materialization"],
report=payload["report"],
)
@router.get(
"/fluxo-bot/overview",
response_model=AdminBotFlowReportOverviewResponse,
)
def bot_flow_reports_overview(
settings: AdminSettings = Depends(get_settings),
_: AuthenticatedStaffPrincipal = Depends(
require_admin_permission(AdminPermission.VIEW_REPORTS)
),
):
service = _build_service(settings)
payload = service.build_bot_flow_overview_payload()
return AdminBotFlowReportOverviewResponse(
service="orquestrador-admin",
area=payload["area"],
source_domain=payload["source_domain"],
mode=payload["mode"],
source_dataset_keys=payload["source_dataset_keys"],
metrics=payload["metrics"],
materialization=payload["materialization"],
reports=payload["reports"],
next_steps=payload["next_steps"],
)
@router.get(
"/fluxo-bot/reports",
response_model=AdminBotFlowReportCatalogResponse,
)
def bot_flow_reports_catalog(
settings: AdminSettings = Depends(get_settings),
_: AuthenticatedStaffPrincipal = Depends(
require_admin_permission(AdminPermission.VIEW_REPORTS)
),
):
service = _build_service(settings)
payload = service.list_bot_flow_reports_payload()
return AdminBotFlowReportCatalogResponse(
service="orquestrador-admin",
area=payload["area"],
source_domain=payload["source_domain"],
source=payload["source"],
materialization=payload["materialization"],
reports=payload["reports"],
)
@router.get(
"/fluxo-bot/reports/{report_key}",
response_model=AdminBotFlowReportResponse,
)
def bot_flow_report_detail(
report_key: str,
settings: AdminSettings = Depends(get_settings),
_: AuthenticatedStaffPrincipal = Depends(
require_admin_permission(AdminPermission.VIEW_REPORTS)
),
):
service = _build_service(settings)
payload = service.get_bot_flow_report_payload(report_key)
if payload is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Relatorio operacional do fluxo do bot nao encontrado.",
)
return AdminBotFlowReportResponse(
service="orquestrador-admin",
area=payload["area"],
source_domain=payload["source_domain"],
source=payload["source"],
materialization=payload["materialization"],
report=payload["report"],
)
@router.get(
"/telemetria-conversacional/overview",
response_model=AdminConversationTelemetryReportOverviewResponse,
)
def conversation_telemetry_reports_overview(
settings: AdminSettings = Depends(get_settings),
_: AuthenticatedStaffPrincipal = Depends(
require_admin_permission(AdminPermission.VIEW_REPORTS)
),
):
service = _build_service(settings)
payload = service.build_conversation_telemetry_overview_payload()
return AdminConversationTelemetryReportOverviewResponse(
service="orquestrador-admin",
area=payload["area"],
source_domain=payload["source_domain"],
mode=payload["mode"],
source_dataset_keys=payload["source_dataset_keys"],
metrics=payload["metrics"],
materialization=payload["materialization"],
reports=payload["reports"],
next_steps=payload["next_steps"],
)
@router.get(
"/telemetria-conversacional/reports",
response_model=AdminConversationTelemetryReportCatalogResponse,
)
def conversation_telemetry_reports_catalog(
settings: AdminSettings = Depends(get_settings),
_: AuthenticatedStaffPrincipal = Depends(
require_admin_permission(AdminPermission.VIEW_REPORTS)
),
):
service = _build_service(settings)
payload = service.list_conversation_telemetry_reports_payload()
return AdminConversationTelemetryReportCatalogResponse(
service="orquestrador-admin",
area=payload["area"],
source_domain=payload["source_domain"],
source=payload["source"],
materialization=payload["materialization"],
reports=payload["reports"],
)
@router.get(
"/telemetria-conversacional/reports/{report_key}",
response_model=AdminConversationTelemetryReportResponse,
)
def conversation_telemetry_report_detail(
report_key: str,
settings: AdminSettings = Depends(get_settings),
_: AuthenticatedStaffPrincipal = Depends(
require_admin_permission(AdminPermission.VIEW_REPORTS)
),
):
service = _build_service(settings)
payload = service.get_conversation_telemetry_report_payload(report_key)
if payload is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Relatorio de telemetria conversacional nao encontrado.",
)
return AdminConversationTelemetryReportResponse(
service="orquestrador-admin",
area=payload["area"],
source_domain=payload["source_domain"],
source=payload["source"],
materialization=payload["materialization"],
report=payload["report"],
)

@ -1,4 +1,4 @@
from fastapi import APIRouter, Depends, Request
from fastapi import APIRouter, Depends, HTTPException, Request, status
from fastapi.responses import RedirectResponse, Response
from admin_app.api.dependencies import (
@ -18,10 +18,15 @@ from admin_app.api.schemas import (
AdminCapabilityResponse,
AdminCurrentAccessResponse,
AdminHealthResponse,
AdminSystemBotGovernedConfigurationResponse,
AdminSystemConfigurationResponse,
AdminSystemFunctionalConfigurationCatalogResponse,
AdminSystemFunctionalConfigurationDetailResponse,
AdminSystemInfoResponse,
AdminSystemModelRuntimeSeparationResponse,
AdminSystemRuntimeConfigurationResponse,
AdminSystemSecurityConfigurationResponse,
AdminSystemWriteGovernanceResponse,
)
from admin_app.core import AdminSecurityService, AuthenticatedStaffPrincipal
from admin_app.core.settings import AdminSettings
@ -127,6 +132,8 @@ def system_configuration(
service="orquestrador-admin",
runtime=runtime_payload,
security=service.build_security_configuration_payload(),
model_runtimes=service.build_model_runtime_separation_payload(),
write_governance=service.build_write_governance_payload(),
sources=service.build_configuration_sources_payload(),
)
@ -167,6 +174,113 @@ def system_security_configuration(
)
@router.get(
"/system/configuration/model-runtimes",
response_model=AdminSystemModelRuntimeSeparationResponse,
)
def system_model_runtime_separation(
settings: AdminSettings = Depends(get_settings),
security_service: AdminSecurityService = Depends(get_security_service),
_: AuthenticatedStaffPrincipal = Depends(
require_admin_permission(AdminPermission.MANAGE_SETTINGS)
),
):
service = _build_service(settings, security_service)
return AdminSystemModelRuntimeSeparationResponse(
service="orquestrador-admin",
model_runtimes=service.build_model_runtime_separation_payload(),
)
@router.get(
"/system/configuration/write-governance",
response_model=AdminSystemWriteGovernanceResponse,
)
def system_write_governance_configuration(
settings: AdminSettings = Depends(get_settings),
security_service: AdminSecurityService = Depends(get_security_service),
_: AuthenticatedStaffPrincipal = Depends(
require_admin_permission(AdminPermission.MANAGE_SETTINGS)
),
):
service = _build_service(settings, security_service)
return AdminSystemWriteGovernanceResponse(
service="orquestrador-admin",
write_governance=service.build_write_governance_payload(),
)
@router.get(
"/system/configuration/functional",
response_model=AdminSystemFunctionalConfigurationCatalogResponse,
)
def system_functional_configuration_catalog(
settings: AdminSettings = Depends(get_settings),
security_service: AdminSecurityService = Depends(get_security_service),
_: AuthenticatedStaffPrincipal = Depends(
require_admin_permission(AdminPermission.VIEW_SYSTEM)
),
):
service = _build_service(settings, security_service)
payload = service.build_functional_configuration_catalog_payload()
return AdminSystemFunctionalConfigurationCatalogResponse(
service="orquestrador-admin",
mode=payload["mode"],
configurations=payload["configurations"],
bot_governed_parent_config_keys=payload["bot_governed_parent_config_keys"],
next_steps=payload["next_steps"],
)
@router.get(
"/system/configuration/functional/bot-governance",
response_model=AdminSystemBotGovernedConfigurationResponse,
)
def system_bot_governed_configuration(
settings: AdminSettings = Depends(get_settings),
security_service: AdminSecurityService = Depends(get_security_service),
_: AuthenticatedStaffPrincipal = Depends(
require_admin_permission(AdminPermission.VIEW_SYSTEM)
),
):
service = _build_service(settings, security_service)
payload = service.build_bot_governed_configuration_payload()
return AdminSystemBotGovernedConfigurationResponse(
service="orquestrador-admin",
parent_config_keys=payload["parent_config_keys"],
settings=payload["settings"],
)
@router.get(
"/system/configuration/functional/{config_key}",
response_model=AdminSystemFunctionalConfigurationDetailResponse,
)
def system_functional_configuration_detail(
config_key: str,
settings: AdminSettings = Depends(get_settings),
security_service: AdminSecurityService = Depends(get_security_service),
_: AuthenticatedStaffPrincipal = Depends(
require_admin_permission(AdminPermission.VIEW_SYSTEM)
),
):
service = _build_service(settings, security_service)
payload = service.get_functional_configuration_payload(config_key)
if payload is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Configuracao funcional do sistema nao encontrada.",
)
return AdminSystemFunctionalConfigurationDetailResponse(
service="orquestrador-admin",
configuration=payload["configuration"],
linked_bot_settings=payload["linked_bot_settings"],
related_runtime_profile=payload["related_runtime_profile"],
managed_by_bot_governance=payload["managed_by_bot_governance"],
)
def _build_runtime_configuration_payload(
service: SystemService,
settings: AdminSettings,

@ -3,7 +3,22 @@ from datetime import datetime
from pydantic import BaseModel, Field, field_validator
from admin_app.core import AdminCredentialStrategy
from shared.contracts import AdminPermission, ServiceName, StaffRole, ToolLifecycleStatus, ToolParameterType
from shared.contracts import (
AdminPermission,
OperationalConsistencyModel,
OperationalDataDomain,
OperationalDataSensitivity,
OperationalFreshnessTarget,
OperationalQuerySurface,
OperationalReadGranularity,
OperationalReadModel,
OperationalStorageShape,
OperationalSyncStrategy,
ServiceName,
StaffRole,
ToolLifecycleStatus,
ToolParameterType,
)
class AdminRootResponse(BaseModel):
@ -104,6 +119,51 @@ class AdminConfigurationSourceResponse(BaseModel):
description: str
class AdminFunctionalConfigurationFieldResponse(BaseModel):
name: str
description: str
writable: bool
secret: bool
class AdminFunctionalConfigurationContractResponse(BaseModel):
config_key: str
domain: str
description: str
source: str
read_permission: str
write_permission: str | None = None
mutability: str
propagation: str
affects_product_runtime: bool
direct_product_write_allowed: bool
fields: list[AdminFunctionalConfigurationFieldResponse]
class AdminModelRuntimeProfileResponse(BaseModel):
runtime_target: str
config_key: str
catalog_runtime_target: str
purpose: str
consumed_by_service: str
description: str
read_permission: str
write_permission: str
published_independently: bool
rollback_independently: bool
cross_target_propagation_allowed: bool
affects_customer_response: bool
can_generate_code: bool
class AdminSystemModelRuntimeSeparationPayload(BaseModel):
runtime_profiles: list[AdminModelRuntimeProfileResponse]
separation_rules: list[str]
atendimento_runtime_configuration: AdminFunctionalConfigurationContractResponse
tool_generation_runtime_configuration: AdminFunctionalConfigurationContractResponse
bot_governed_parent_config_keys: list[str]
class AdminSystemRuntimeConfigurationResponse(BaseModel):
service: str
runtime: AdminSystemRuntimeConfigurationPayload
@ -114,13 +174,466 @@ class AdminSystemSecurityConfigurationResponse(BaseModel):
security: AdminCredentialStrategy
class AdminSystemWriteGovernancePayload(BaseModel):
mode: str
allowed_direct_write_tables: list[str]
blocked_operational_dataset_keys: list[str]
blocked_product_source_tables: list[str]
governed_configuration_keys: list[str]
enforcement_points: list[str]
governance_rules: list[str]
class AdminSystemConfigurationResponse(BaseModel):
service: str
runtime: AdminSystemRuntimeConfigurationPayload
security: AdminCredentialStrategy
model_runtimes: AdminSystemModelRuntimeSeparationPayload
write_governance: AdminSystemWriteGovernancePayload
sources: list[AdminConfigurationSourceResponse]
class AdminSystemModelRuntimeSeparationResponse(BaseModel):
service: str
model_runtimes: AdminSystemModelRuntimeSeparationPayload
class AdminSystemWriteGovernanceResponse(BaseModel):
service: str
write_governance: AdminSystemWriteGovernancePayload
class AdminBotGovernedSettingResponse(BaseModel):
setting_key: str
parent_config_key: str
field_name: str
area: str
description: str
read_permission: str
write_permission: str
mutability: str
versioned_publication_required: bool
direct_product_write_allowed: bool
class AdminSystemFunctionalConfigurationCatalogResponse(BaseModel):
service: str
mode: str
configurations: list[AdminFunctionalConfigurationContractResponse]
bot_governed_parent_config_keys: list[str]
next_steps: list[str]
class AdminSystemFunctionalConfigurationDetailResponse(BaseModel):
service: str
configuration: AdminFunctionalConfigurationContractResponse
linked_bot_settings: list[AdminBotGovernedSettingResponse]
related_runtime_profile: AdminModelRuntimeProfileResponse | None = None
managed_by_bot_governance: bool
class AdminSystemBotGovernedConfigurationResponse(BaseModel):
service: str
parent_config_keys: list[str]
settings: list[AdminBotGovernedSettingResponse]
class AdminReportMetricResponse(BaseModel):
key: str
label: str
value: str
description: str
class AdminReportFamilyResponse(BaseModel):
key: str
label: str
description: str
dataset_keys: list[str]
class AdminReportMaterializationResponse(BaseModel):
report_read_model: OperationalReadModel
consistency_model: OperationalConsistencyModel
sync_strategy: OperationalSyncStrategy
storage_shape: OperationalStorageShape
query_surface: OperationalQuerySurface
uses_product_replica: bool
direct_product_query_allowed: bool
refresh_behavior: str
class AdminReportFieldResponse(BaseModel):
name: str
description: str
sensitivity: OperationalDataSensitivity
class AdminReportDatasetSummaryResponse(BaseModel):
dataset_key: str
domain: OperationalDataDomain
description: str
source_table: str
freshness_target: OperationalFreshnessTarget
allowed_granularities: list[OperationalReadGranularity]
allowed_field_count: int
blocked_field_count: int
write_allowed: bool
materialization_status: str
last_consolidated_at: datetime | None = None
source_watermark: str | None = None
class AdminReportDatasetDetailResponse(BaseModel):
dataset_key: str
domain: OperationalDataDomain
description: str
source_table: str
read_permission: AdminPermission
report_read_model: OperationalReadModel
consistency_model: OperationalConsistencyModel
sync_strategy: OperationalSyncStrategy
storage_shape: OperationalStorageShape
query_surface: OperationalQuerySurface
uses_product_replica: bool
direct_product_query_allowed: bool
freshness_target: OperationalFreshnessTarget
allowed_granularities: list[OperationalReadGranularity]
write_allowed: bool
materialization_status: str
last_consolidated_at: datetime | None = None
source_watermark: str | None = None
allowed_fields: list[AdminReportFieldResponse]
blocked_fields: list[AdminReportFieldResponse]
class AdminReportOverviewResponse(BaseModel):
service: str
mode: str
metrics: list[AdminReportMetricResponse]
materialization: AdminReportMaterializationResponse
report_families: list[AdminReportFamilyResponse]
next_steps: list[str]
class AdminReportDatasetListResponse(BaseModel):
service: str
source: str
materialization: AdminReportMaterializationResponse
datasets: list[AdminReportDatasetSummaryResponse]
class AdminReportDatasetResponse(BaseModel):
service: str
source: str
materialization: AdminReportMaterializationResponse
dataset: AdminReportDatasetDetailResponse
class AdminSalesReportMetricDefinitionResponse(BaseModel):
key: str
label: str
aggregation: str
description: str
class AdminSalesReportDimensionResponse(BaseModel):
field_name: str
label: str
description: str
default_group_by: bool = False
class AdminSalesReportFilterResponse(BaseModel):
field_name: str
label: str
filter_type: str
description: str
required: bool = False
class AdminSalesReportDefinitionSummaryResponse(BaseModel):
report_key: str
label: str
description: str
dataset_key: str
default_time_field: str
default_granularity: OperationalReadGranularity
supported_metric_keys: list[str]
supported_dimension_fields: list[str]
materialization_status: str
last_consolidated_at: datetime | None = None
source_watermark: str | None = None
class AdminSalesReportDefinitionDetailResponse(BaseModel):
report_key: str
label: str
description: str
dataset_key: str
default_time_field: str
default_granularity: OperationalReadGranularity
metrics: list[AdminSalesReportMetricDefinitionResponse]
dimensions: list[AdminSalesReportDimensionResponse]
filters: list[AdminSalesReportFilterResponse]
dataset: AdminReportDatasetDetailResponse
class AdminSalesReportOverviewResponse(BaseModel):
service: str
domain: OperationalDataDomain
mode: str
source_dataset_keys: list[str]
metrics: list[AdminReportMetricResponse]
materialization: AdminReportMaterializationResponse
reports: list[AdminSalesReportDefinitionSummaryResponse]
next_steps: list[str]
class AdminSalesReportCatalogResponse(BaseModel):
service: str
domain: OperationalDataDomain
source: str
materialization: AdminReportMaterializationResponse
reports: list[AdminSalesReportDefinitionSummaryResponse]
class AdminSalesReportResponse(BaseModel):
service: str
domain: OperationalDataDomain
source: str
materialization: AdminReportMaterializationResponse
report: AdminSalesReportDefinitionDetailResponse
class AdminRevenueReportDefinitionSummaryResponse(BaseModel):
report_key: str
label: str
description: str
dataset_key: str
default_time_field: str
default_granularity: OperationalReadGranularity
supported_metric_keys: list[str]
supported_dimension_fields: list[str]
materialization_status: str
last_consolidated_at: datetime | None = None
source_watermark: str | None = None
class AdminRevenueReportDefinitionDetailResponse(BaseModel):
report_key: str
label: str
description: str
dataset_key: str
default_time_field: str
default_granularity: OperationalReadGranularity
metrics: list[AdminSalesReportMetricDefinitionResponse]
dimensions: list[AdminSalesReportDimensionResponse]
filters: list[AdminSalesReportFilterResponse]
dataset: AdminReportDatasetDetailResponse
class AdminRevenueReportOverviewResponse(BaseModel):
service: str
area: str
source_domain: OperationalDataDomain
mode: str
source_dataset_keys: list[str]
metrics: list[AdminReportMetricResponse]
materialization: AdminReportMaterializationResponse
reports: list[AdminRevenueReportDefinitionSummaryResponse]
next_steps: list[str]
class AdminRevenueReportCatalogResponse(BaseModel):
service: str
area: str
source_domain: OperationalDataDomain
source: str
materialization: AdminReportMaterializationResponse
reports: list[AdminRevenueReportDefinitionSummaryResponse]
class AdminRevenueReportResponse(BaseModel):
service: str
area: str
source_domain: OperationalDataDomain
source: str
materialization: AdminReportMaterializationResponse
report: AdminRevenueReportDefinitionDetailResponse
class AdminRentalReportDefinitionSummaryResponse(BaseModel):
report_key: str
label: str
description: str
dataset_key: str
default_time_field: str
default_granularity: OperationalReadGranularity
supported_metric_keys: list[str]
supported_dimension_fields: list[str]
materialization_status: str
last_consolidated_at: datetime | None = None
source_watermark: str | None = None
class AdminRentalReportDefinitionDetailResponse(BaseModel):
report_key: str
label: str
description: str
dataset_key: str
default_time_field: str
default_granularity: OperationalReadGranularity
metrics: list[AdminSalesReportMetricDefinitionResponse]
dimensions: list[AdminSalesReportDimensionResponse]
filters: list[AdminSalesReportFilterResponse]
dataset: AdminReportDatasetDetailResponse
class AdminRentalReportOverviewResponse(BaseModel):
service: str
area: str
source_domain: OperationalDataDomain
mode: str
source_dataset_keys: list[str]
metrics: list[AdminReportMetricResponse]
materialization: AdminReportMaterializationResponse
reports: list[AdminRentalReportDefinitionSummaryResponse]
next_steps: list[str]
class AdminRentalReportCatalogResponse(BaseModel):
service: str
area: str
source_domain: OperationalDataDomain
source: str
materialization: AdminReportMaterializationResponse
reports: list[AdminRentalReportDefinitionSummaryResponse]
class AdminRentalReportResponse(BaseModel):
service: str
area: str
source_domain: OperationalDataDomain
source: str
materialization: AdminReportMaterializationResponse
report: AdminRentalReportDefinitionDetailResponse
class AdminBotFlowReportDefinitionSummaryResponse(BaseModel):
report_key: str
label: str
description: str
dataset_key: str
default_time_field: str
default_granularity: OperationalReadGranularity
supported_metric_keys: list[str]
supported_dimension_fields: list[str]
materialization_status: str
last_consolidated_at: datetime | None = None
source_watermark: str | None = None
class AdminBotFlowReportDefinitionDetailResponse(BaseModel):
report_key: str
label: str
description: str
dataset_key: str
default_time_field: str
default_granularity: OperationalReadGranularity
metrics: list[AdminSalesReportMetricDefinitionResponse]
dimensions: list[AdminSalesReportDimensionResponse]
filters: list[AdminSalesReportFilterResponse]
dataset: AdminReportDatasetDetailResponse
class AdminBotFlowReportOverviewResponse(BaseModel):
service: str
area: str
source_domain: OperationalDataDomain
mode: str
source_dataset_keys: list[str]
metrics: list[AdminReportMetricResponse]
materialization: AdminReportMaterializationResponse
reports: list[AdminBotFlowReportDefinitionSummaryResponse]
next_steps: list[str]
class AdminBotFlowReportCatalogResponse(BaseModel):
service: str
area: str
source_domain: OperationalDataDomain
source: str
materialization: AdminReportMaterializationResponse
reports: list[AdminBotFlowReportDefinitionSummaryResponse]
class AdminBotFlowReportResponse(BaseModel):
service: str
area: str
source_domain: OperationalDataDomain
source: str
materialization: AdminReportMaterializationResponse
report: AdminBotFlowReportDefinitionDetailResponse
class AdminConversationTelemetryReportDefinitionSummaryResponse(BaseModel):
report_key: str
label: str
description: str
dataset_key: str
default_time_field: str
default_granularity: OperationalReadGranularity
supported_metric_keys: list[str]
supported_dimension_fields: list[str]
materialization_status: str
last_consolidated_at: datetime | None = None
source_watermark: str | None = None
class AdminConversationTelemetryReportDefinitionDetailResponse(BaseModel):
report_key: str
label: str
description: str
dataset_key: str
default_time_field: str
default_granularity: OperationalReadGranularity
metrics: list[AdminSalesReportMetricDefinitionResponse]
dimensions: list[AdminSalesReportDimensionResponse]
filters: list[AdminSalesReportFilterResponse]
dataset: AdminReportDatasetDetailResponse
class AdminConversationTelemetryReportOverviewResponse(BaseModel):
service: str
area: str
source_domain: OperationalDataDomain
mode: str
source_dataset_keys: list[str]
metrics: list[AdminReportMetricResponse]
materialization: AdminReportMaterializationResponse
reports: list[AdminConversationTelemetryReportDefinitionSummaryResponse]
next_steps: list[str]
class AdminConversationTelemetryReportCatalogResponse(BaseModel):
service: str
area: str
source_domain: OperationalDataDomain
source: str
materialization: AdminReportMaterializationResponse
reports: list[AdminConversationTelemetryReportDefinitionSummaryResponse]
class AdminConversationTelemetryReportResponse(BaseModel):
service: str
area: str
source_domain: OperationalDataDomain
source: str
materialization: AdminReportMaterializationResponse
report: AdminConversationTelemetryReportDefinitionDetailResponse
class AdminLoginRequest(BaseModel):
email: str
password: str = Field(min_length=1)

@ -1,9 +1,10 @@
from collections.abc import Generator
from sqlalchemy import create_engine
from sqlalchemy import create_engine, event
from sqlalchemy.orm import Session, declarative_base, sessionmaker
from admin_app.core.settings import get_admin_settings
from admin_app.db.write_governance import enforce_admin_session_write_governance
# monta a conexão do banco administrativo e expõe get_admin_db_session(). Esse generator é o que alimenta as dependências FastAPI para repositórios e serviços.
@ -33,6 +34,16 @@ AdminSessionLocal = sessionmaker(
bind=admin_engine,
)
@event.listens_for(AdminSessionLocal, "before_flush")
def _block_unguarded_admin_writes(session, flush_context, instances):
enforce_admin_session_write_governance(
new=session.new,
dirty=session.dirty,
deleted=session.deleted,
)
AdminBase = declarative_base()

@ -0,0 +1,104 @@
from __future__ import annotations
from collections.abc import Iterable
from shared.contracts import (
PRODUCT_OPERATIONAL_DATASETS,
SYSTEM_FUNCTIONAL_CONFIGURATIONS,
FunctionalConfigurationPropagation,
)
ALLOWED_ADMIN_WRITE_TABLES: tuple[str, ...] = (
"admin_audit_logs",
"staff_accounts",
"staff_sessions",
)
class AdminWriteGovernanceViolation(RuntimeError):
"""Raised when the admin runtime attempts an ungoverned direct write."""
def ensure_direct_admin_write_allowed(table_name: str) -> None:
normalized_table_name = str(table_name or "").strip().lower()
if normalized_table_name in ALLOWED_ADMIN_WRITE_TABLES:
return
raise AdminWriteGovernanceViolation(
"Escrita direta do admin bloqueada para a tabela "
f"'{normalized_table_name or 'desconhecida'}'. "
"Use um fluxo governado, versionado e auditavel antes de publicar qualquer efeito no product."
)
def enforce_admin_session_write_governance(
*,
new: Iterable[object] = (),
dirty: Iterable[object] = (),
deleted: Iterable[object] = (),
) -> None:
seen_tables: set[str] = set()
for instance in (*tuple(new), *tuple(dirty), *tuple(deleted)):
table_name = _resolve_table_name(instance)
if table_name is None or table_name in seen_tables:
continue
ensure_direct_admin_write_allowed(table_name)
seen_tables.add(table_name)
def build_admin_write_governance_payload() -> dict:
governed_configuration_keys = sorted(
configuration.config_key
for configuration in SYSTEM_FUNCTIONAL_CONFIGURATIONS
if configuration.propagation == FunctionalConfigurationPropagation.VERSIONED_PUBLICATION
)
return {
"mode": "admin_internal_tables_only",
"allowed_direct_write_tables": list(ALLOWED_ADMIN_WRITE_TABLES),
"blocked_operational_dataset_keys": sorted(
dataset.dataset_key for dataset in PRODUCT_OPERATIONAL_DATASETS
),
"blocked_product_source_tables": sorted(
{dataset.source_table for dataset in PRODUCT_OPERATIONAL_DATASETS}
),
"governed_configuration_keys": governed_configuration_keys,
"enforcement_points": [
"AdminSession.before_flush bloqueia escrita ORM fora do allowlist interno do admin.",
"Contratos compartilhados mantem datasets operacionais com write_allowed=false.",
"Configuracoes que afetam o runtime do product seguem versioned_publication antes de qualquer efeito operacional.",
],
"governance_rules": [
"O admin nao escreve diretamente nas tabelas operacionais do product.",
"Toda alteracao com efeito no product nasce como estado administrativo versionado.",
"O product consome apenas configuracao publicada e aprovada.",
],
}
def build_admin_write_governance_source_payload() -> dict:
return {
"key": "admin_write_governance",
"source": "runtime_guard",
"mutable": False,
"description": (
"Guard no AdminSession bloqueia escrita ORM fora das tabelas internas do admin e preserva a governanca versionada antes de qualquer efeito no product."
),
}
def _resolve_table_name(instance: object) -> str | None:
table = getattr(instance, "__table__", None)
if table is not None:
table_name = getattr(table, "name", None)
if table_name:
return str(table_name).strip().lower()
class_table_name = getattr(type(instance), "__tablename__", None)
if class_table_name:
return str(class_table_name).strip().lower()
instance_table_name = getattr(instance, "__tablename__", None)
if instance_table_name:
return str(instance_table_name).strip().lower()
return None

@ -5,6 +5,7 @@ from admin_app.services.audit_service import (
)
from admin_app.services.auth_service import AuthService
from admin_app.services.collaborator_management_service import CollaboratorManagementService
from admin_app.services.report_service import ReportService
from admin_app.services.system_service import SystemService
from admin_app.services.tool_management_service import ToolManagementService
@ -14,6 +15,7 @@ __all__ = [
"AuditService",
"AuthService",
"CollaboratorManagementService",
"ReportService",
"SystemService",
"ToolManagementService",
]
]

@ -0,0 +1,963 @@
from admin_app.core.settings import AdminSettings
from shared.contracts import (
PRODUCT_OPERATIONAL_DATASETS,
OperationalDatasetContract,
OperationalReadGranularity,
get_operational_dataset,
)
_MATERIALIZATION_STATUS = "contract_defined_pending_snapshot_view"
_REFRESH_BEHAVIOR = "manual_refresh_triggers_sync_boundary"
_REPORT_SOURCE = "shared_contract_catalog"
_SALES_DATASET_KEY = "sales_orders"
_SALES_REPORT_METRICS = {
"total_orders": {"key": "total_orders", "label": "Pedidos totais", "aggregation": "count", "description": "Quantidade total de pedidos consolidados no periodo."},
"gross_order_value": {"key": "gross_order_value", "label": "Valor bruto negociado", "aggregation": "sum", "description": "Soma do valor negociado dos pedidos incluidos no recorte."},
"active_orders": {"key": "active_orders", "label": "Pedidos ativos", "aggregation": "count_where_status_active", "description": "Quantidade de pedidos ainda em fluxo operacional ativo."},
"cancelled_orders": {"key": "cancelled_orders", "label": "Pedidos cancelados", "aggregation": "count_where_status_cancelled", "description": "Quantidade de pedidos cancelados no recorte selecionado."},
"cancellation_rate": {"key": "cancellation_rate", "label": "Taxa de cancelamento", "aggregation": "ratio", "description": "Relacao entre pedidos cancelados e total de pedidos consolidados."},
"average_ticket": {"key": "average_ticket", "label": "Ticket medio", "aggregation": "avg", "description": "Media do valor negociado por pedido dentro do recorte."},
}
_SALES_DIMENSIONS = {
"created_at": {"field_name": "created_at", "label": "Periodo de criacao", "description": "Agrupamento temporal da criacao do pedido.", "default_group_by": True},
"updated_at": {"field_name": "updated_at", "label": "Periodo de atualizacao", "description": "Agrupamento temporal da ultima atualizacao do pedido.", "default_group_by": True},
"data_cancelamento": {"field_name": "data_cancelamento", "label": "Periodo de cancelamento", "description": "Agrupamento temporal do cancelamento registrado.", "default_group_by": True},
"status": {"field_name": "status", "label": "Status do pedido", "description": "Separa os pedidos por etapa operacional."},
"modelo_veiculo": {"field_name": "modelo_veiculo", "label": "Modelo do veiculo", "description": "Recorte por modelo comercial negociado."},
"motivo_cancelamento": {"field_name": "motivo_cancelamento", "label": "Motivo do cancelamento", "description": "Separa cancelamentos pelo motivo operacional registrado."},
}
_SALES_FILTERS = {
"created_at": {"field_name": "created_at", "label": "Periodo", "filter_type": "date_range", "description": "Intervalo de criacao do pedido consolidado.", "required": True},
"updated_at": {"field_name": "updated_at", "label": "Periodo", "filter_type": "date_range", "description": "Intervalo da ultima atualizacao do pedido.", "required": True},
"data_cancelamento": {"field_name": "data_cancelamento", "label": "Periodo", "filter_type": "date_range", "description": "Intervalo em que o cancelamento foi registrado.", "required": True},
"status": {"field_name": "status", "label": "Status", "filter_type": "enum", "description": "Restringe o consolidado para um ou mais status operacionais."},
"modelo_veiculo": {"field_name": "modelo_veiculo", "label": "Modelo do veiculo", "filter_type": "enum", "description": "Filtra pedidos por modelo comercial reservado."},
"motivo_cancelamento": {"field_name": "motivo_cancelamento", "label": "Motivo do cancelamento", "filter_type": "enum", "description": "Restringe o consolidado para um ou mais motivos operacionais."},
}
_SALES_REPORTS = (
{"report_key": "orders_volume", "label": "Volume de pedidos", "description": "Acompanha o volume bruto de pedidos por periodo e status operacional.", "default_time_field": "created_at", "default_granularity": OperationalReadGranularity.AGGREGATE, "metric_keys": ("total_orders", "gross_order_value"), "dimension_fields": ("created_at", "status", "modelo_veiculo"), "filter_fields": ("created_at", "status", "modelo_veiculo")},
{"report_key": "active_vs_cancelled", "label": "Pedidos ativos e cancelados", "description": "Compara pedidos em andamento com pedidos cancelados para leitura operacional da conversao.", "default_time_field": "updated_at", "default_granularity": OperationalReadGranularity.AGGREGATE, "metric_keys": ("active_orders", "cancelled_orders", "cancellation_rate"), "dimension_fields": ("updated_at", "status", "modelo_veiculo"), "filter_fields": ("updated_at", "status", "modelo_veiculo")},
{"report_key": "average_ticket", "label": "Ticket medio", "description": "Consolida a evolucao do valor medio negociado por periodo e por modelo.", "default_time_field": "created_at", "default_granularity": OperationalReadGranularity.AGGREGATE, "metric_keys": ("average_ticket", "gross_order_value", "total_orders"), "dimension_fields": ("created_at", "modelo_veiculo", "status"), "filter_fields": ("created_at", "status", "modelo_veiculo")},
{"report_key": "cancellations_by_period", "label": "Cancelamentos por periodo", "description": "Organiza o volume de cancelamentos e seus motivos ao longo do tempo.", "default_time_field": "data_cancelamento", "default_granularity": OperationalReadGranularity.AGGREGATE, "metric_keys": ("cancelled_orders", "cancellation_rate"), "dimension_fields": ("data_cancelamento", "motivo_cancelamento", "modelo_veiculo"), "filter_fields": ("data_cancelamento", "motivo_cancelamento", "modelo_veiculo")},
)
_REVENUE_DATASET_KEY = "rental_payments"
_REVENUE_REPORT_METRICS = {
"total_payments": {"key": "total_payments", "label": "Pagamentos totais", "aggregation": "count", "description": "Quantidade total de pagamentos liquidados no periodo."},
"collected_amount": {"key": "collected_amount", "label": "Valor arrecadado", "aggregation": "sum", "description": "Soma do valor liquidado dos pagamentos incluidos no recorte."},
"average_payment_amount": {"key": "average_payment_amount", "label": "Valor medio por pagamento", "aggregation": "avg", "description": "Media do valor liquidado por pagamento no recorte selecionado."},
"distinct_contracts": {"key": "distinct_contracts", "label": "Contratos conciliados", "aggregation": "count_distinct", "description": "Quantidade de contratos distintos com pagamento consolidado no periodo."},
}
_REVENUE_DIMENSIONS = {
"data_pagamento": {"field_name": "data_pagamento", "label": "Periodo do pagamento", "description": "Agrupamento temporal do pagamento liquidado.", "default_group_by": True},
"created_at": {"field_name": "created_at", "label": "Periodo de registro", "description": "Agrupamento temporal do registro do pagamento no read model administrativo.", "default_group_by": True},
"contrato_numero": {"field_name": "contrato_numero", "label": "Contrato", "description": "Recorte por contrato associado ao pagamento."},
"placa": {"field_name": "placa", "label": "Placa", "description": "Recorte por veiculo vinculado ao contrato pago."},
"protocolo": {"field_name": "protocolo", "label": "Protocolo", "description": "Rastreio por protocolo publico do pagamento."},
}
_REVENUE_FILTERS = {
"data_pagamento": {"field_name": "data_pagamento", "label": "Periodo do pagamento", "filter_type": "date_range", "description": "Intervalo em que o pagamento foi liquidado.", "required": True},
"created_at": {"field_name": "created_at", "label": "Periodo de registro", "filter_type": "date_range", "description": "Intervalo em que o pagamento foi registrado no dataset administrativo.", "required": True},
"contrato_numero": {"field_name": "contrato_numero", "label": "Contrato", "filter_type": "exact_match", "description": "Filtra pagamentos por contrato associado."},
"placa": {"field_name": "placa", "label": "Placa", "filter_type": "exact_match", "description": "Filtra pagamentos pela placa vinculada ao contrato."},
"protocolo": {"field_name": "protocolo", "label": "Protocolo", "filter_type": "exact_match", "description": "Filtra o consolidado por protocolo publico do pagamento."},
}
_REVENUE_REPORTS = (
{"report_key": "payments_volume", "label": "Volume de pagamentos", "description": "Acompanha a quantidade de pagamentos liquidados por periodo, contrato e veiculo.", "default_time_field": "data_pagamento", "default_granularity": OperationalReadGranularity.AGGREGATE, "metric_keys": ("total_payments", "distinct_contracts"), "dimension_fields": ("data_pagamento", "contrato_numero", "placa"), "filter_fields": ("data_pagamento", "contrato_numero", "placa")},
{"report_key": "collected_amount", "label": "Arrecadacao por periodo", "description": "Consolida o valor arrecadado por periodo com apoio de contrato e placa para leitura operacional.", "default_time_field": "data_pagamento", "default_granularity": OperationalReadGranularity.AGGREGATE, "metric_keys": ("collected_amount", "average_payment_amount", "total_payments"), "dimension_fields": ("data_pagamento", "contrato_numero", "placa"), "filter_fields": ("data_pagamento", "contrato_numero", "placa")},
{"report_key": "contract_reconciliation", "label": "Pagamentos por contrato", "description": "Organiza pagamentos conciliados por contrato com rastreio por placa e protocolo publico.", "default_time_field": "data_pagamento", "default_granularity": OperationalReadGranularity.AGGREGATE, "metric_keys": ("collected_amount", "total_payments"), "dimension_fields": ("contrato_numero", "placa", "protocolo"), "filter_fields": ("data_pagamento", "contrato_numero", "placa", "protocolo")},
)
_RENTAL_FLEET_DATASET_KEY = "rental_fleet"
_RENTAL_CONTRACTS_DATASET_KEY = "rental_contracts"
_RENTAL_REPORT_METRICS = {
"total_fleet_vehicles": {"key": "total_fleet_vehicles", "label": "Veiculos da frota", "aggregation": "count", "description": "Quantidade total de veiculos consolidados na frota administrativa."},
"available_fleet_vehicles": {"key": "available_fleet_vehicles", "label": "Veiculos disponiveis", "aggregation": "count_where_status_available", "description": "Quantidade de veiculos em status operacional disponivel para locacao."},
"average_daily_rate": {"key": "average_daily_rate", "label": "Diaria media", "aggregation": "avg", "description": "Media do valor de diaria vigente dos veiculos incluidos no recorte."},
"total_contracts": {"key": "total_contracts", "label": "Contratos totais", "aggregation": "count", "description": "Quantidade total de contratos consolidados no periodo selecionado."},
"active_contracts": {"key": "active_contracts", "label": "Contratos ativos", "aggregation": "count_where_status_active", "description": "Quantidade de contratos ainda em curso no recorte operacional."},
"closed_contracts": {"key": "closed_contracts", "label": "Contratos encerrados", "aggregation": "count_where_status_closed", "description": "Quantidade de contratos concluidos ou encerrados no recorte selecionado."},
"overdue_contracts": {"key": "overdue_contracts", "label": "Devolucoes em atraso", "aggregation": "count_overdue", "description": "Quantidade de contratos com fim previsto vencido e sem devolucao consolidada."},
"occupied_vehicles": {"key": "occupied_vehicles", "label": "Veiculos ocupados", "aggregation": "count_distinct_active_vehicles", "description": "Quantidade de veiculos distintos associados a contratos ativos no periodo."},
"projected_revenue": {"key": "projected_revenue", "label": "Receita prevista", "aggregation": "sum", "description": "Soma do valor previsto dos contratos incluidos no recorte."},
"final_revenue": {"key": "final_revenue", "label": "Receita final", "aggregation": "sum", "description": "Soma do valor final consolidado dos contratos no recorte selecionado."},
"revenue_delta": {"key": "revenue_delta", "label": "Desvio entre previsto e final", "aggregation": "difference", "description": "Diferenca consolidada entre receita prevista e receita final dos contratos."},
}
_RENTAL_DIMENSIONS = {
"created_at": {"field_name": "created_at", "label": "Periodo de cadastro", "description": "Agrupamento temporal do cadastro no read model administrativo.", "default_group_by": True},
"categoria": {"field_name": "categoria", "label": "Categoria", "description": "Recorte por categoria comercial da locacao."},
"status": {"field_name": "status", "label": "Status", "description": "Separa frota ou contratos por status operacional."},
"modelo": {"field_name": "modelo", "label": "Modelo", "description": "Recorte por modelo do veiculo de locacao."},
"placa": {"field_name": "placa", "label": "Placa", "description": "Rastreio por placa do veiculo locado."},
"data_inicio": {"field_name": "data_inicio", "label": "Inicio da locacao", "description": "Agrupamento temporal da abertura do contrato.", "default_group_by": True},
"data_fim_prevista": {"field_name": "data_fim_prevista", "label": "Fim previsto", "description": "Agrupamento temporal do fim previsto da locacao.", "default_group_by": True},
"data_devolucao": {"field_name": "data_devolucao", "label": "Data de devolucao", "description": "Agrupamento temporal da devolucao consolidada do contrato.", "default_group_by": True},
"updated_at": {"field_name": "updated_at", "label": "Ultima atualizacao", "description": "Agrupamento temporal da ultima atualizacao do contrato.", "default_group_by": True},
"modelo_veiculo": {"field_name": "modelo_veiculo", "label": "Modelo do veiculo", "description": "Recorte por modelo do veiculo vinculado ao contrato."},
"contrato_numero": {"field_name": "contrato_numero", "label": "Contrato", "description": "Rastreio por numero publico do contrato."},
}
_RENTAL_FILTERS = {
"created_at": {"field_name": "created_at", "label": "Periodo de cadastro", "filter_type": "date_range", "description": "Intervalo de cadastro no dataset administrativo.", "required": True},
"categoria": {"field_name": "categoria", "label": "Categoria", "filter_type": "enum", "description": "Filtra frota ou contratos por categoria comercial."},
"status": {"field_name": "status", "label": "Status", "filter_type": "enum", "description": "Restringe o consolidado para um ou mais status operacionais."},
"modelo": {"field_name": "modelo", "label": "Modelo", "filter_type": "enum", "description": "Filtra o consolidado por modelo da frota."},
"placa": {"field_name": "placa", "label": "Placa", "filter_type": "exact_match", "description": "Filtra o consolidado pela placa do veiculo."},
"data_inicio": {"field_name": "data_inicio", "label": "Inicio da locacao", "filter_type": "date_range", "description": "Intervalo de abertura dos contratos de locacao.", "required": True},
"data_fim_prevista": {"field_name": "data_fim_prevista", "label": "Fim previsto", "filter_type": "date_range", "description": "Intervalo do fim previsto dos contratos de locacao.", "required": True},
"updated_at": {"field_name": "updated_at", "label": "Ultima atualizacao", "filter_type": "date_range", "description": "Intervalo da ultima atualizacao operacional do contrato.", "required": True},
"modelo_veiculo": {"field_name": "modelo_veiculo", "label": "Modelo do veiculo", "filter_type": "enum", "description": "Filtra contratos pelo modelo do veiculo locado."},
"contrato_numero": {"field_name": "contrato_numero", "label": "Contrato", "filter_type": "exact_match", "description": "Filtra o consolidado por numero publico do contrato."},
}
_RENTAL_REPORTS = (
{"report_key": "fleet_availability", "label": "Disponibilidade da frota", "description": "Resume disponibilidade, status e diaria vigente da frota de locacao.", "dataset_key": _RENTAL_FLEET_DATASET_KEY, "default_time_field": "created_at", "default_granularity": OperationalReadGranularity.AGGREGATE, "metric_keys": ("total_fleet_vehicles", "available_fleet_vehicles", "average_daily_rate"), "dimension_fields": ("created_at", "categoria", "status", "modelo"), "filter_fields": ("created_at", "categoria", "status", "modelo", "placa")},
{"report_key": "contracts_lifecycle", "label": "Contratos ativos e encerrados", "description": "Organiza o ciclo operacional dos contratos de locacao entre abertos, ativos e encerrados.", "dataset_key": _RENTAL_CONTRACTS_DATASET_KEY, "default_time_field": "data_inicio", "default_granularity": OperationalReadGranularity.AGGREGATE, "metric_keys": ("total_contracts", "active_contracts", "closed_contracts"), "dimension_fields": ("data_inicio", "categoria", "status", "modelo_veiculo"), "filter_fields": ("data_inicio", "categoria", "status", "placa", "contrato_numero")},
{"report_key": "overdue_returns", "label": "Devolucoes em atraso", "description": "Acompanha contratos com fim previsto vencido e sem devolucao consolidada.", "dataset_key": _RENTAL_CONTRACTS_DATASET_KEY, "default_time_field": "data_fim_prevista", "default_granularity": OperationalReadGranularity.AGGREGATE, "metric_keys": ("overdue_contracts", "active_contracts"), "dimension_fields": ("data_fim_prevista", "categoria", "status", "placa"), "filter_fields": ("data_fim_prevista", "categoria", "status", "placa", "contrato_numero")},
{"report_key": "fleet_occupancy", "label": "Ocupacao da frota", "description": "Consolida o uso da frota por contratos ativos ao longo do tempo e por categoria.", "dataset_key": _RENTAL_CONTRACTS_DATASET_KEY, "default_time_field": "data_inicio", "default_granularity": OperationalReadGranularity.AGGREGATE, "metric_keys": ("occupied_vehicles", "active_contracts", "projected_revenue"), "dimension_fields": ("data_inicio", "categoria", "modelo_veiculo", "status"), "filter_fields": ("data_inicio", "categoria", "status", "modelo_veiculo", "placa")},
{"report_key": "projected_vs_final_revenue", "label": "Receita prevista versus final", "description": "Compara o valor previsto na abertura do contrato com o valor final consolidado da locacao.", "dataset_key": _RENTAL_CONTRACTS_DATASET_KEY, "default_time_field": "updated_at", "default_granularity": OperationalReadGranularity.AGGREGATE, "metric_keys": ("projected_revenue", "final_revenue", "revenue_delta"), "dimension_fields": ("updated_at", "categoria", "status", "modelo_veiculo"), "filter_fields": ("updated_at", "categoria", "status", "placa", "contrato_numero")},
)
_BOT_FLOW_DATASET_KEY = "conversation_turns"
_BOT_FLOW_REPORT_METRICS = {
"total_turns": {"key": "total_turns", "label": "Turnos totais", "aggregation": "count", "description": "Quantidade total de turnos processados no recorte operacional."},
"completed_turns": {"key": "completed_turns", "label": "Turnos concluidos", "aggregation": "count_where_status_completed", "description": "Quantidade de turnos concluidos pelo fluxo operacional do bot."},
"errored_turns": {"key": "errored_turns", "label": "Turnos com falha", "aggregation": "count_where_status_error", "description": "Quantidade de turnos com falha operacional no processamento."},
"tool_routed_turns": {"key": "tool_routed_turns", "label": "Turnos com tool", "aggregation": "count_where_tool_called", "description": "Quantidade de turnos que acionaram pelo menos uma tool no fluxo."},
"fallback_turns": {"key": "fallback_turns", "label": "Turnos em fallback", "aggregation": "count_where_action_fallback", "description": "Quantidade de turnos encaminhados para fallback funcional do bot."},
"handoff_turns": {"key": "handoff_turns", "label": "Turnos em handoff", "aggregation": "count_where_action_handoff", "description": "Quantidade de turnos que escalaram para handoff humano."},
}
_BOT_FLOW_DIMENSIONS = {
"started_at": {"field_name": "started_at", "label": "Inicio do turno", "description": "Agrupamento temporal do inicio do processamento do turno.", "default_group_by": True},
"completed_at": {"field_name": "completed_at", "label": "Fim do turno", "description": "Agrupamento temporal da finalizacao do turno processado.", "default_group_by": True},
"channel": {"field_name": "channel", "label": "Canal", "description": "Recorte por canal operacional do atendimento."},
"turn_status": {"field_name": "turn_status", "label": "Status do turno", "description": "Separa o fluxo pelos estados operacionais do turno."},
"action": {"field_name": "action", "label": "Acao do fluxo", "description": "Recorte pela acao tomada pelo orquestrador durante o turno."},
"tool_name": {"field_name": "tool_name", "label": "Tool acionada", "description": "Rastreio da tool utilizada durante o turno do bot."},
"domain": {"field_name": "domain", "label": "Dominio operacional", "description": "Recorte pelo dominio operacional associado ao turno."},
"intent": {"field_name": "intent", "label": "Intencao", "description": "Recorte pela intencao classificada para o turno."},
}
_BOT_FLOW_FILTERS = {
"started_at": {"field_name": "started_at", "label": "Inicio do turno", "filter_type": "date_range", "description": "Intervalo de inicio do processamento do turno.", "required": True},
"completed_at": {"field_name": "completed_at", "label": "Fim do turno", "filter_type": "date_range", "description": "Intervalo de finalizacao do turno processado."},
"channel": {"field_name": "channel", "label": "Canal", "filter_type": "enum", "description": "Filtra o fluxo por canal operacional."},
"turn_status": {"field_name": "turn_status", "label": "Status do turno", "filter_type": "enum", "description": "Restringe o consolidado para um ou mais status do turno."},
"action": {"field_name": "action", "label": "Acao do fluxo", "filter_type": "enum", "description": "Restringe o consolidado para uma ou mais acoes do fluxo do bot."},
"tool_name": {"field_name": "tool_name", "label": "Tool acionada", "filter_type": "enum", "description": "Filtra os turnos pela tool utilizada no atendimento."},
"domain": {"field_name": "domain", "label": "Dominio operacional", "filter_type": "enum", "description": "Filtra o fluxo pelo dominio operacional associado ao turno."},
"intent": {"field_name": "intent", "label": "Intencao", "filter_type": "enum", "description": "Filtra o consolidado pela intencao classificada para o turno."},
}
_BOT_FLOW_REPORTS = (
{"report_key": "turn_status_overview", "label": "Status dos turnos", "description": "Acompanha o andamento operacional dos turnos por status, canal e dominio.", "default_time_field": "started_at", "default_granularity": OperationalReadGranularity.AGGREGATE, "metric_keys": ("total_turns", "completed_turns", "errored_turns"), "dimension_fields": ("started_at", "turn_status", "channel", "domain"), "filter_fields": ("started_at", "turn_status", "channel", "domain")},
{"report_key": "action_routing_flow", "label": "Roteamento do fluxo", "description": "Organiza as acoes do orquestrador entre resposta, fallback, handoff e outros caminhos operacionais.", "default_time_field": "started_at", "default_granularity": OperationalReadGranularity.AGGREGATE, "metric_keys": ("total_turns", "fallback_turns", "handoff_turns"), "dimension_fields": ("started_at", "action", "channel", "domain"), "filter_fields": ("started_at", "action", "channel", "domain", "intent")},
{"report_key": "tool_activation_flow", "label": "Uso operacional de tools", "description": "Mostra quais turnos acionaram tools e como isso se distribui no fluxo do bot.", "default_time_field": "started_at", "default_granularity": OperationalReadGranularity.AGGREGATE, "metric_keys": ("tool_routed_turns", "completed_turns", "errored_turns"), "dimension_fields": ("started_at", "tool_name", "action", "domain"), "filter_fields": ("started_at", "tool_name", "action", "domain", "intent")},
{"report_key": "fallback_and_handoff", "label": "Fallback e handoff", "description": "Destaca turnos que saem do fluxo padrao para fallback funcional ou handoff humano.", "default_time_field": "started_at", "default_granularity": OperationalReadGranularity.AGGREGATE, "metric_keys": ("fallback_turns", "handoff_turns", "errored_turns"), "dimension_fields": ("started_at", "action", "channel", "intent"), "filter_fields": ("started_at", "action", "channel", "intent", "domain")},
{"report_key": "operational_failures", "label": "Falhas operacionais do fluxo", "description": "Ajuda a triar turnos com falha por status, acao e canal operacional.", "default_time_field": "started_at", "default_granularity": OperationalReadGranularity.AGGREGATE, "metric_keys": ("errored_turns", "total_turns", "tool_routed_turns"), "dimension_fields": ("started_at", "turn_status", "action", "channel"), "filter_fields": ("started_at", "turn_status", "action", "channel", "tool_name")},
)
_CONVERSATION_TELEMETRY_DATASET_KEY = "conversation_turns"
_CONVERSATION_TELEMETRY_REPORT_METRICS = {
"total_turns": {"key": "total_turns", "label": "Turnos totais", "aggregation": "count", "description": "Quantidade total de turnos observados no recorte de telemetria."},
"distinct_conversations": {"key": "distinct_conversations", "label": "Conversas distintas", "aggregation": "count_distinct", "description": "Quantidade de conversas distintas observadas no recorte selecionado."},
"average_latency_ms": {"key": "average_latency_ms", "label": "Latencia media", "aggregation": "avg", "description": "Media do tempo de processamento do turno em milissegundos."},
"p95_latency_ms": {"key": "p95_latency_ms", "label": "Latencia p95", "aggregation": "percentile_p95", "description": "Percentil 95 do tempo de processamento dos turnos observados."},
"tool_routed_turns": {"key": "tool_routed_turns", "label": "Turnos com tool", "aggregation": "count_where_tool_called", "description": "Quantidade de turnos que acionaram pelo menos uma tool no atendimento."},
"errored_turns": {"key": "errored_turns", "label": "Turnos com falha", "aggregation": "count_where_status_error", "description": "Quantidade de turnos com falha no recorte de telemetria."},
}
_CONVERSATION_TELEMETRY_DIMENSIONS = {
"started_at": {"field_name": "started_at", "label": "Inicio do turno", "description": "Agrupamento temporal do inicio do processamento do turno.", "default_group_by": True},
"completed_at": {"field_name": "completed_at", "label": "Fim do turno", "description": "Agrupamento temporal da finalizacao do turno.", "default_group_by": True},
"channel": {"field_name": "channel", "label": "Canal", "description": "Recorte por canal operacional do atendimento."},
"domain": {"field_name": "domain", "label": "Dominio operacional", "description": "Recorte pelo dominio operacional associado ao turno."},
"intent": {"field_name": "intent", "label": "Intencao", "description": "Recorte pela intencao classificada para o turno."},
"tool_name": {"field_name": "tool_name", "label": "Tool acionada", "description": "Rastreio da tool utilizada durante o turno."},
"turn_status": {"field_name": "turn_status", "label": "Status do turno", "description": "Separa a telemetria pelos estados observados do turno."},
"action": {"field_name": "action", "label": "Acao do turno", "description": "Recorte pela acao operacional tomada pelo orquestrador."},
}
_CONVERSATION_TELEMETRY_FILTERS = {
"started_at": {"field_name": "started_at", "label": "Inicio do turno", "filter_type": "date_range", "description": "Intervalo de inicio do processamento do turno.", "required": True},
"completed_at": {"field_name": "completed_at", "label": "Fim do turno", "filter_type": "date_range", "description": "Intervalo de finalizacao do turno processado."},
"channel": {"field_name": "channel", "label": "Canal", "filter_type": "enum", "description": "Filtra a telemetria por canal operacional."},
"domain": {"field_name": "domain", "label": "Dominio operacional", "filter_type": "enum", "description": "Filtra a telemetria pelo dominio associado ao turno."},
"intent": {"field_name": "intent", "label": "Intencao", "filter_type": "enum", "description": "Filtra o recorte pela intencao classificada."},
"tool_name": {"field_name": "tool_name", "label": "Tool acionada", "filter_type": "enum", "description": "Filtra os turnos pela tool utilizada durante o atendimento."},
"turn_status": {"field_name": "turn_status", "label": "Status do turno", "filter_type": "enum", "description": "Restringe o consolidado para um ou mais status observados."},
"action": {"field_name": "action", "label": "Acao do turno", "filter_type": "enum", "description": "Restringe o consolidado para uma ou mais acoes do orquestrador."},
}
_CONVERSATION_TELEMETRY_REPORTS = (
{"report_key": "conversation_volume", "label": "Volume de atendimento", "description": "Consolida o volume de turnos e conversas por periodo, canal e dominio.", "default_time_field": "started_at", "default_granularity": OperationalReadGranularity.AGGREGATE, "metric_keys": ("total_turns", "distinct_conversations"), "dimension_fields": ("started_at", "channel", "domain", "intent"), "filter_fields": ("started_at", "channel", "domain", "intent")},
{"report_key": "latency_profile", "label": "Perfil de latencia", "description": "Organiza sinais de latencia media e p95 por canal, dominio e intencao.", "default_time_field": "completed_at", "default_granularity": OperationalReadGranularity.AGGREGATE, "metric_keys": ("average_latency_ms", "p95_latency_ms", "total_turns"), "dimension_fields": ("completed_at", "channel", "domain", "intent"), "filter_fields": ("started_at", "completed_at", "channel", "domain", "intent")},
{"report_key": "domain_distribution", "label": "Distribuicao por dominio", "description": "Mostra como o atendimento se distribui entre dominios, intencoes e canais.", "default_time_field": "started_at", "default_granularity": OperationalReadGranularity.AGGREGATE, "metric_keys": ("total_turns", "distinct_conversations"), "dimension_fields": ("started_at", "domain", "intent", "channel"), "filter_fields": ("started_at", "domain", "intent", "channel")},
{"report_key": "tool_usage_telemetry", "label": "Uso de tools", "description": "Expoe quais tools aparecem com mais frequencia no atendimento e em quais contextos.", "default_time_field": "started_at", "default_granularity": OperationalReadGranularity.AGGREGATE, "metric_keys": ("tool_routed_turns", "total_turns", "errored_turns"), "dimension_fields": ("started_at", "tool_name", "domain", "channel"), "filter_fields": ("started_at", "tool_name", "domain", "channel", "intent")},
{"report_key": "turn_health_status", "label": "Saude por status", "description": "Acompanha estados de saude do atendimento por status observado e acao tomada.", "default_time_field": "started_at", "default_granularity": OperationalReadGranularity.AGGREGATE, "metric_keys": ("errored_turns", "total_turns", "average_latency_ms"), "dimension_fields": ("started_at", "turn_status", "action", "channel"), "filter_fields": ("started_at", "turn_status", "action", "channel", "domain")},
)
_REPORT_FAMILIES = (
{
"key": "sales",
"label": "Vendas",
"description": "Pedidos, conversao comercial e cancelamentos usados pela operacao interna.",
"dataset_keys": ["sales_orders"],
},
{
"key": "arrecadacao",
"label": "Arrecadacao",
"description": "Recebimentos de locacao e conciliacao operacional do faturamento.",
"dataset_keys": ["rental_payments"],
},
{
"key": "operacao",
"label": "Operacao",
"description": "Estoque, revisoes, frota e contratos que suportam o acompanhamento do dia a dia.",
"dataset_keys": [
"vehicle_inventory",
"review_schedules",
"rental_fleet",
"rental_contracts",
],
},
{
"key": "telemetria_atendimento",
"label": "Telemetria de atendimento",
"description": "Turnos conversacionais, uso de tools e sinais de eficiencia do bot.",
"dataset_keys": ["conversation_turns"],
},
{
"key": "integration_deliveries",
"label": "Entregas de integracao",
"description": "Rastreio operacional das entregas para provedores e falhas de despacho.",
"dataset_keys": ["integration_deliveries"],
},
)
class ReportService:
def __init__(self, settings: AdminSettings):
self.settings = settings
def build_overview_payload(self) -> dict:
datasets = PRODUCT_OPERATIONAL_DATASETS
near_real_time_count = sum(1 for dataset in datasets if dataset.freshness_target.value == "near_real_time")
intra_hour_count = sum(1 for dataset in datasets if dataset.freshness_target.value == "intra_hour")
return {
"mode": "shared_contract_bootstrap",
"metrics": [
{
"key": "datasets",
"label": "Datasets liberados",
"value": str(len(datasets)),
"description": "Datasets operacionais explicitamente liberados para relatorios administrativos.",
},
{
"key": "domains",
"label": "Dominios operacionais",
"value": str(len({dataset.domain for dataset in datasets})),
"description": "Dominios cobertos pelo catalogo inicial de leitura administrativa.",
},
{
"key": "near_real_time_targets",
"label": "Metas near real time",
"value": str(near_real_time_count),
"description": "Datasets cuja UX espera consolidacao mais frequente sem leitura live do produto.",
},
{
"key": "intra_hour_targets",
"label": "Metas intra-hour",
"value": str(intra_hour_count),
"description": "Datasets de apoio operacional e telemetria servidos por consolidacao eventual intra-horaria.",
},
],
"materialization": self._build_materialization_payload(),
"report_families": list(_REPORT_FAMILIES),
"next_steps": [
"Criar snapshots sanitizados no admin para vendas, arrecadacao e operacao.",
"Servir views dedicadas por caso de uso em vez de espelhar o schema operacional do produto.",
"Exibir carimbo de atualizacao e watermark quando a camada de sincronizacao entrar em producao.",
],
}
def list_datasets_payload(self) -> dict:
return {
"source": _REPORT_SOURCE,
"materialization": self._build_materialization_payload(),
"datasets": [
self._serialize_dataset_summary(dataset)
for dataset in sorted(
PRODUCT_OPERATIONAL_DATASETS,
key=lambda item: (item.domain.value, item.dataset_key),
)
],
}
def get_dataset_payload(self, dataset_key: str) -> dict | None:
dataset = get_operational_dataset(dataset_key)
if dataset is None:
return None
return {
"source": _REPORT_SOURCE,
"materialization": self._build_materialization_payload(dataset),
"dataset": self._serialize_dataset_detail(dataset),
}
def build_sales_overview_payload(self) -> dict:
dataset = self._get_sales_dataset()
return {
"domain": dataset.domain,
"mode": "sales_contract_bootstrap",
"source_dataset_keys": [dataset.dataset_key],
"metrics": [
{
"key": "source_datasets",
"label": "Datasets fonte",
"value": "1",
"description": "A estrutura inicial de vendas nasce apoiada em um dataset sanitizado de pedidos.",
},
{
"key": "initial_reports",
"label": "Relatorios iniciais",
"value": str(len(_SALES_REPORTS)),
"description": "Casos de uso de vendas previstos para a primeira superficie administrativa do dominio.",
},
{
"key": "allowed_fields",
"label": "Campos liberados",
"value": str(len(dataset.allowed_fields)),
"description": "Campos operacionais expostos para agregacao e filtros de vendas.",
},
{
"key": "blocked_fields",
"label": "Campos bloqueados",
"value": str(len(dataset.blocked_fields)),
"description": "Campos sensiveis que permanecem fora do read model administrativo.",
},
{
"key": "freshness_target",
"label": "Meta de frescor",
"value": dataset.freshness_target.value,
"description": "Objetivo inicial de consolidacao para a UX dos relatorios de vendas.",
},
],
"materialization": self._build_materialization_payload(dataset),
"reports": [self._serialize_sales_report_summary(report) for report in _SALES_REPORTS],
"next_steps": [
"Materializar snapshot sanitizado de sales_orders no banco administrativo.",
"Criar dedicated views separadas para volume, ticket medio e cancelamentos.",
"Exibir watermark e timestamp da ultima consolidacao quando o ETL incremental entrar em producao.",
],
}
def list_sales_reports_payload(self) -> dict:
dataset = self._get_sales_dataset()
return {
"domain": dataset.domain,
"source": _REPORT_SOURCE,
"materialization": self._build_materialization_payload(dataset),
"reports": [self._serialize_sales_report_summary(report) for report in _SALES_REPORTS],
}
def get_sales_report_payload(self, report_key: str) -> dict | None:
normalized_report_key = self._normalize_key(report_key)
dataset = self._get_sales_dataset()
for report in _SALES_REPORTS:
if report["report_key"] == normalized_report_key:
return {
"domain": dataset.domain,
"source": _REPORT_SOURCE,
"materialization": self._build_materialization_payload(dataset),
"report": self._serialize_sales_report_detail(report, dataset),
}
return None
def build_revenue_overview_payload(self) -> dict:
dataset = self._get_revenue_dataset()
return {
"area": "arrecadacao",
"source_domain": dataset.domain,
"mode": "revenue_contract_bootstrap",
"source_dataset_keys": [dataset.dataset_key],
"metrics": [
{
"key": "source_datasets",
"label": "Datasets fonte",
"value": "1",
"description": "A estrutura inicial de arrecadacao nasce apoiada em um dataset sanitizado de pagamentos.",
},
{
"key": "initial_reports",
"label": "Relatorios iniciais",
"value": str(len(_REVENUE_REPORTS)),
"description": "Casos de uso iniciais de arrecadacao previstos para a primeira superficie administrativa.",
},
{
"key": "allowed_fields",
"label": "Campos liberados",
"value": str(len(dataset.allowed_fields)),
"description": "Campos operacionais expostos para agregacao e conciliacao de pagamentos.",
},
{
"key": "blocked_fields",
"label": "Campos bloqueados",
"value": str(len(dataset.blocked_fields)),
"description": "Campos sensiveis que permanecem fora do read model administrativo.",
},
{
"key": "freshness_target",
"label": "Meta de frescor",
"value": dataset.freshness_target.value,
"description": "Objetivo inicial de consolidacao para a UX dos relatorios de arrecadacao.",
},
],
"materialization": self._build_materialization_payload(dataset),
"reports": [self._serialize_revenue_report_summary(report) for report in _REVENUE_REPORTS],
"next_steps": [
"Materializar snapshot sanitizado de rental_payments no banco administrativo.",
"Criar dedicated views separadas para arrecadacao por periodo e conciliacao por contrato.",
"Cruzar contratos e pagamentos em uma etapa futura para abrir inadimplencia operacional sem leitura live do produto.",
],
}
def list_revenue_reports_payload(self) -> dict:
dataset = self._get_revenue_dataset()
return {
"area": "arrecadacao",
"source_domain": dataset.domain,
"source": _REPORT_SOURCE,
"materialization": self._build_materialization_payload(dataset),
"reports": [self._serialize_revenue_report_summary(report) for report in _REVENUE_REPORTS],
}
def get_revenue_report_payload(self, report_key: str) -> dict | None:
normalized_report_key = self._normalize_key(report_key)
dataset = self._get_revenue_dataset()
for report in _REVENUE_REPORTS:
if report["report_key"] == normalized_report_key:
return {
"area": "arrecadacao",
"source_domain": dataset.domain,
"source": _REPORT_SOURCE,
"materialization": self._build_materialization_payload(dataset),
"report": self._serialize_revenue_report_detail(report, dataset),
}
return None
def build_rental_overview_payload(self) -> dict:
fleet_dataset = self._get_rental_fleet_dataset()
contracts_dataset = self._get_rental_contracts_dataset()
return {
"area": "locacao",
"source_domain": contracts_dataset.domain,
"mode": "rental_contract_bootstrap",
"source_dataset_keys": [fleet_dataset.dataset_key, contracts_dataset.dataset_key],
"metrics": [
{
"key": "source_datasets",
"label": "Datasets fonte",
"value": "2",
"description": "A estrutura inicial de locacao nasce sobre snapshots sanitizados de frota e contratos.",
},
{
"key": "initial_reports",
"label": "Relatorios iniciais",
"value": str(len(_RENTAL_REPORTS)),
"description": "Casos de uso operacionais de locacao previstos para a primeira superficie administrativa.",
},
{
"key": "fleet_allowed_fields",
"label": "Campos liberados da frota",
"value": str(len(fleet_dataset.allowed_fields)),
"description": "Campos expostos para disponibilidade, categoria e diaria vigente da frota.",
},
{
"key": "contracts_allowed_fields",
"label": "Campos liberados dos contratos",
"value": str(len(contracts_dataset.allowed_fields)),
"description": "Campos expostos para ciclo do contrato, ocupacao e devolucao operacional.",
},
{
"key": "freshness_target",
"label": "Meta de frescor",
"value": contracts_dataset.freshness_target.value,
"description": "Objetivo inicial de consolidacao para a UX dos relatorios de locacao.",
},
],
"materialization": self._build_materialization_payload(contracts_dataset),
"reports": [self._serialize_rental_report_summary(report) for report in _RENTAL_REPORTS],
"next_steps": [
"Materializar snapshots sanitizados de rental_fleet e rental_contracts no banco administrativo.",
"Criar dedicated views separadas para disponibilidade da frota, contratos em curso e devolucoes em atraso.",
"Combinar frota e contratos em uma camada futura de ocupacao sem consultar tabelas live do produto.",
],
}
def list_rental_reports_payload(self) -> dict:
contracts_dataset = self._get_rental_contracts_dataset()
return {
"area": "locacao",
"source_domain": contracts_dataset.domain,
"source": _REPORT_SOURCE,
"materialization": self._build_materialization_payload(contracts_dataset),
"reports": [self._serialize_rental_report_summary(report) for report in _RENTAL_REPORTS],
}
def get_rental_report_payload(self, report_key: str) -> dict | None:
normalized_report_key = self._normalize_key(report_key)
for report in _RENTAL_REPORTS:
if report["report_key"] == normalized_report_key:
dataset = self._get_rental_dataset(report["dataset_key"])
return {
"area": "locacao",
"source_domain": dataset.domain,
"source": _REPORT_SOURCE,
"materialization": self._build_materialization_payload(dataset),
"report": self._serialize_rental_report_detail(report, dataset),
}
return None
def build_bot_flow_overview_payload(self) -> dict:
dataset = self._get_bot_flow_dataset()
return {
"area": "fluxo_bot",
"source_domain": dataset.domain,
"mode": "bot_flow_contract_bootstrap",
"source_dataset_keys": [dataset.dataset_key],
"metrics": [
{
"key": "source_datasets",
"label": "Datasets fonte",
"value": "1",
"description": "A estrutura inicial do fluxo do bot nasce apoiada em um dataset sanitizado de turnos operacionais.",
},
{
"key": "initial_reports",
"label": "Relatorios iniciais",
"value": str(len(_BOT_FLOW_REPORTS)),
"description": "Casos de uso de operacao do fluxo do bot previstos para a primeira superficie administrativa.",
},
{
"key": "allowed_fields",
"label": "Campos liberados",
"value": str(len(dataset.allowed_fields)),
"description": "Campos operacionais expostos para triagem de status, acao e uso de tools.",
},
{
"key": "blocked_fields",
"label": "Campos bloqueados",
"value": str(len(dataset.blocked_fields)),
"description": "Campos sensiveis e mensagens livres que permanecem fora da operacao administrativa.",
},
{
"key": "freshness_target",
"label": "Meta de frescor",
"value": dataset.freshness_target.value,
"description": "Objetivo inicial de consolidacao para a UX dos relatorios operacionais do fluxo do bot.",
},
],
"materialization": self._build_materialization_payload(dataset),
"reports": [self._serialize_bot_flow_report_summary(report) for report in _BOT_FLOW_REPORTS],
"next_steps": [
"Materializar snapshot sanitizado de conversation_turns no banco administrativo.",
"Criar dedicated views separadas para status do turno, roteamento operacional e falhas do fluxo.",
"Reservar latencia e eficiencia detalhada para a etapa seguinte de telemetria conversacional.",
],
}
def list_bot_flow_reports_payload(self) -> dict:
dataset = self._get_bot_flow_dataset()
return {
"area": "fluxo_bot",
"source_domain": dataset.domain,
"source": _REPORT_SOURCE,
"materialization": self._build_materialization_payload(dataset),
"reports": [self._serialize_bot_flow_report_summary(report) for report in _BOT_FLOW_REPORTS],
}
def get_bot_flow_report_payload(self, report_key: str) -> dict | None:
normalized_report_key = self._normalize_key(report_key)
dataset = self._get_bot_flow_dataset()
for report in _BOT_FLOW_REPORTS:
if report["report_key"] == normalized_report_key:
return {
"area": "fluxo_bot",
"source_domain": dataset.domain,
"source": _REPORT_SOURCE,
"materialization": self._build_materialization_payload(dataset),
"report": self._serialize_bot_flow_report_detail(report, dataset),
}
return None
def build_conversation_telemetry_overview_payload(self) -> dict:
dataset = self._get_conversation_telemetry_dataset()
return {
"area": "telemetria_conversacional",
"source_domain": dataset.domain,
"mode": "conversation_telemetry_contract_bootstrap",
"source_dataset_keys": [dataset.dataset_key],
"metrics": [
{
"key": "source_datasets",
"label": "Datasets fonte",
"value": "1",
"description": "A estrutura inicial de telemetria conversacional nasce apoiada em um dataset sanitizado de turnos.",
},
{
"key": "initial_reports",
"label": "Relatorios iniciais",
"value": str(len(_CONVERSATION_TELEMETRY_REPORTS)),
"description": "Casos de uso iniciais de observabilidade conversacional previstos para a primeira superficie administrativa.",
},
{
"key": "allowed_fields",
"label": "Campos liberados",
"value": str(len(dataset.allowed_fields)),
"description": "Campos expostos para volume, latencia, distribuicao por dominio e uso de tools.",
},
{
"key": "blocked_fields",
"label": "Campos bloqueados",
"value": str(len(dataset.blocked_fields)),
"description": "Campos sensiveis e texto livre que permanecem fora da telemetria administrativa.",
},
{
"key": "freshness_target",
"label": "Meta de frescor",
"value": dataset.freshness_target.value,
"description": "Objetivo inicial de consolidacao para a UX dos relatorios de telemetria conversacional.",
},
],
"materialization": self._build_materialization_payload(dataset),
"reports": [self._serialize_conversation_telemetry_report_summary(report) for report in _CONVERSATION_TELEMETRY_REPORTS],
"next_steps": [
"Materializar snapshot sanitizado de conversation_turns no banco administrativo.",
"Criar dedicated views separadas para volume, latencia e distribuicao por dominio do atendimento.",
"Preparar buckets e watermark de consolidacao para comparativos historicos da telemetria.",
],
}
def list_conversation_telemetry_reports_payload(self) -> dict:
dataset = self._get_conversation_telemetry_dataset()
return {
"area": "telemetria_conversacional",
"source_domain": dataset.domain,
"source": _REPORT_SOURCE,
"materialization": self._build_materialization_payload(dataset),
"reports": [self._serialize_conversation_telemetry_report_summary(report) for report in _CONVERSATION_TELEMETRY_REPORTS],
}
def get_conversation_telemetry_report_payload(self, report_key: str) -> dict | None:
normalized_report_key = self._normalize_key(report_key)
dataset = self._get_conversation_telemetry_dataset()
for report in _CONVERSATION_TELEMETRY_REPORTS:
if report["report_key"] == normalized_report_key:
return {
"area": "telemetria_conversacional",
"source_domain": dataset.domain,
"source": _REPORT_SOURCE,
"materialization": self._build_materialization_payload(dataset),
"report": self._serialize_conversation_telemetry_report_detail(report, dataset),
}
return None
@staticmethod
def _build_materialization_payload(dataset: OperationalDatasetContract | None = None) -> dict:
reference_dataset = dataset or PRODUCT_OPERATIONAL_DATASETS[0]
return {
"report_read_model": reference_dataset.report_read_model,
"consistency_model": reference_dataset.consistency_model,
"sync_strategy": reference_dataset.sync_strategy,
"storage_shape": reference_dataset.storage_shape,
"query_surface": reference_dataset.query_surface,
"uses_product_replica": reference_dataset.uses_product_replica,
"direct_product_query_allowed": reference_dataset.direct_product_query_allowed,
"refresh_behavior": _REFRESH_BEHAVIOR,
}
@staticmethod
def _serialize_dataset_summary(dataset) -> dict:
return {
"dataset_key": dataset.dataset_key,
"domain": dataset.domain,
"description": dataset.description,
"source_table": dataset.source_table,
"freshness_target": dataset.freshness_target,
"allowed_granularities": list(dataset.allowed_granularities),
"allowed_field_count": len(dataset.allowed_fields),
"blocked_field_count": len(dataset.blocked_fields),
"write_allowed": dataset.write_allowed,
"materialization_status": _MATERIALIZATION_STATUS,
"last_consolidated_at": None,
"source_watermark": None,
}
@classmethod
def _serialize_dataset_detail(cls, dataset) -> dict:
return {
"dataset_key": dataset.dataset_key,
"domain": dataset.domain,
"description": dataset.description,
"source_table": dataset.source_table,
"read_permission": dataset.read_permission,
"report_read_model": dataset.report_read_model,
"consistency_model": dataset.consistency_model,
"sync_strategy": dataset.sync_strategy,
"storage_shape": dataset.storage_shape,
"query_surface": dataset.query_surface,
"uses_product_replica": dataset.uses_product_replica,
"direct_product_query_allowed": dataset.direct_product_query_allowed,
"freshness_target": dataset.freshness_target,
"allowed_granularities": list(dataset.allowed_granularities),
"write_allowed": dataset.write_allowed,
"materialization_status": _MATERIALIZATION_STATUS,
"last_consolidated_at": None,
"source_watermark": None,
"allowed_fields": [cls._serialize_field(field) for field in dataset.allowed_fields],
"blocked_fields": [cls._serialize_field(field) for field in dataset.blocked_fields],
}
@classmethod
def _serialize_sales_report_summary(cls, report_definition: dict) -> dict:
return {
"report_key": report_definition["report_key"],
"label": report_definition["label"],
"description": report_definition["description"],
"dataset_key": _SALES_DATASET_KEY,
"default_time_field": report_definition["default_time_field"],
"default_granularity": report_definition["default_granularity"],
"supported_metric_keys": list(report_definition["metric_keys"]),
"supported_dimension_fields": list(report_definition["dimension_fields"]),
"materialization_status": _MATERIALIZATION_STATUS,
"last_consolidated_at": None,
"source_watermark": None,
}
@classmethod
def _serialize_sales_report_detail(
cls,
report_definition: dict,
dataset: OperationalDatasetContract,
) -> dict:
return {
"report_key": report_definition["report_key"],
"label": report_definition["label"],
"description": report_definition["description"],
"dataset_key": _SALES_DATASET_KEY,
"default_time_field": report_definition["default_time_field"],
"default_granularity": report_definition["default_granularity"],
"metrics": [dict(_SALES_REPORT_METRICS[key]) for key in report_definition["metric_keys"]],
"dimensions": [dict(_SALES_DIMENSIONS[field_name]) for field_name in report_definition["dimension_fields"]],
"filters": [dict(_SALES_FILTERS[field_name]) for field_name in report_definition["filter_fields"]],
"dataset": cls._serialize_dataset_detail(dataset),
}
@classmethod
def _serialize_revenue_report_summary(cls, report_definition: dict) -> dict:
return {
"report_key": report_definition["report_key"],
"label": report_definition["label"],
"description": report_definition["description"],
"dataset_key": _REVENUE_DATASET_KEY,
"default_time_field": report_definition["default_time_field"],
"default_granularity": report_definition["default_granularity"],
"supported_metric_keys": list(report_definition["metric_keys"]),
"supported_dimension_fields": list(report_definition["dimension_fields"]),
"materialization_status": _MATERIALIZATION_STATUS,
"last_consolidated_at": None,
"source_watermark": None,
}
@classmethod
def _serialize_revenue_report_detail(
cls,
report_definition: dict,
dataset: OperationalDatasetContract,
) -> dict:
return {
"report_key": report_definition["report_key"],
"label": report_definition["label"],
"description": report_definition["description"],
"dataset_key": _REVENUE_DATASET_KEY,
"default_time_field": report_definition["default_time_field"],
"default_granularity": report_definition["default_granularity"],
"metrics": [dict(_REVENUE_REPORT_METRICS[key]) for key in report_definition["metric_keys"]],
"dimensions": [dict(_REVENUE_DIMENSIONS[field_name]) for field_name in report_definition["dimension_fields"]],
"filters": [dict(_REVENUE_FILTERS[field_name]) for field_name in report_definition["filter_fields"]],
"dataset": cls._serialize_dataset_detail(dataset),
}
@classmethod
def _serialize_rental_report_summary(cls, report_definition: dict) -> dict:
return {
"report_key": report_definition["report_key"],
"label": report_definition["label"],
"description": report_definition["description"],
"dataset_key": report_definition["dataset_key"],
"default_time_field": report_definition["default_time_field"],
"default_granularity": report_definition["default_granularity"],
"supported_metric_keys": list(report_definition["metric_keys"]),
"supported_dimension_fields": list(report_definition["dimension_fields"]),
"materialization_status": _MATERIALIZATION_STATUS,
"last_consolidated_at": None,
"source_watermark": None,
}
@classmethod
def _serialize_rental_report_detail(
cls,
report_definition: dict,
dataset: OperationalDatasetContract,
) -> dict:
return {
"report_key": report_definition["report_key"],
"label": report_definition["label"],
"description": report_definition["description"],
"dataset_key": report_definition["dataset_key"],
"default_time_field": report_definition["default_time_field"],
"default_granularity": report_definition["default_granularity"],
"metrics": [dict(_RENTAL_REPORT_METRICS[key]) for key in report_definition["metric_keys"]],
"dimensions": [dict(_RENTAL_DIMENSIONS[field_name]) for field_name in report_definition["dimension_fields"]],
"filters": [dict(_RENTAL_FILTERS[field_name]) for field_name in report_definition["filter_fields"]],
"dataset": cls._serialize_dataset_detail(dataset),
}
@classmethod
def _serialize_bot_flow_report_summary(cls, report_definition: dict) -> dict:
return {
"report_key": report_definition["report_key"],
"label": report_definition["label"],
"description": report_definition["description"],
"dataset_key": _BOT_FLOW_DATASET_KEY,
"default_time_field": report_definition["default_time_field"],
"default_granularity": report_definition["default_granularity"],
"supported_metric_keys": list(report_definition["metric_keys"]),
"supported_dimension_fields": list(report_definition["dimension_fields"]),
"materialization_status": _MATERIALIZATION_STATUS,
"last_consolidated_at": None,
"source_watermark": None,
}
@classmethod
def _serialize_bot_flow_report_detail(
cls,
report_definition: dict,
dataset: OperationalDatasetContract,
) -> dict:
return {
"report_key": report_definition["report_key"],
"label": report_definition["label"],
"description": report_definition["description"],
"dataset_key": _BOT_FLOW_DATASET_KEY,
"default_time_field": report_definition["default_time_field"],
"default_granularity": report_definition["default_granularity"],
"metrics": [dict(_BOT_FLOW_REPORT_METRICS[key]) for key in report_definition["metric_keys"]],
"dimensions": [dict(_BOT_FLOW_DIMENSIONS[field_name]) for field_name in report_definition["dimension_fields"]],
"filters": [dict(_BOT_FLOW_FILTERS[field_name]) for field_name in report_definition["filter_fields"]],
"dataset": cls._serialize_dataset_detail(dataset),
}
@classmethod
def _serialize_conversation_telemetry_report_summary(cls, report_definition: dict) -> dict:
return {
"report_key": report_definition["report_key"],
"label": report_definition["label"],
"description": report_definition["description"],
"dataset_key": _CONVERSATION_TELEMETRY_DATASET_KEY,
"default_time_field": report_definition["default_time_field"],
"default_granularity": report_definition["default_granularity"],
"supported_metric_keys": list(report_definition["metric_keys"]),
"supported_dimension_fields": list(report_definition["dimension_fields"]),
"materialization_status": _MATERIALIZATION_STATUS,
"last_consolidated_at": None,
"source_watermark": None,
}
@classmethod
def _serialize_conversation_telemetry_report_detail(
cls,
report_definition: dict,
dataset: OperationalDatasetContract,
) -> dict:
return {
"report_key": report_definition["report_key"],
"label": report_definition["label"],
"description": report_definition["description"],
"dataset_key": _CONVERSATION_TELEMETRY_DATASET_KEY,
"default_time_field": report_definition["default_time_field"],
"default_granularity": report_definition["default_granularity"],
"metrics": [dict(_CONVERSATION_TELEMETRY_REPORT_METRICS[key]) for key in report_definition["metric_keys"]],
"dimensions": [dict(_CONVERSATION_TELEMETRY_DIMENSIONS[field_name]) for field_name in report_definition["dimension_fields"]],
"filters": [dict(_CONVERSATION_TELEMETRY_FILTERS[field_name]) for field_name in report_definition["filter_fields"]],
"dataset": cls._serialize_dataset_detail(dataset),
}
@staticmethod
def _serialize_field(field) -> dict:
return {
"name": field.name,
"description": field.description,
"sensitivity": field.sensitivity,
}
@staticmethod
def _normalize_key(value: str) -> str:
return str(value or "").strip().lower()
@staticmethod
def _get_sales_dataset() -> OperationalDatasetContract:
dataset = get_operational_dataset(_SALES_DATASET_KEY)
if dataset is None:
raise RuntimeError("sales_orders contract is required to build sales reports")
return dataset
@staticmethod
def _get_revenue_dataset() -> OperationalDatasetContract:
dataset = get_operational_dataset(_REVENUE_DATASET_KEY)
if dataset is None:
raise RuntimeError("rental_payments contract is required to build revenue reports")
return dataset
@staticmethod
def _get_rental_dataset(dataset_key: str) -> OperationalDatasetContract:
dataset = get_operational_dataset(dataset_key)
if dataset is None:
raise RuntimeError(f"{dataset_key} contract is required to build rental reports")
return dataset
@classmethod
def _get_rental_fleet_dataset(cls) -> OperationalDatasetContract:
return cls._get_rental_dataset(_RENTAL_FLEET_DATASET_KEY)
@classmethod
def _get_rental_contracts_dataset(cls) -> OperationalDatasetContract:
return cls._get_rental_dataset(_RENTAL_CONTRACTS_DATASET_KEY)
@staticmethod
def _get_bot_flow_dataset() -> OperationalDatasetContract:
dataset = get_operational_dataset(_BOT_FLOW_DATASET_KEY)
if dataset is None:
raise RuntimeError("conversation_turns contract is required to build bot flow reports")
return dataset
@staticmethod
def _get_conversation_telemetry_dataset() -> OperationalDatasetContract:
dataset = get_operational_dataset(_CONVERSATION_TELEMETRY_DATASET_KEY)
if dataset is None:
raise RuntimeError("conversation_turns contract is required to build conversation telemetry reports")
return dataset

@ -1,5 +1,17 @@
from admin_app.core import AdminCredentialStrategy, AdminSecurityService
from admin_app.core import AdminCredentialStrategy, AdminSecurityService
from admin_app.db.write_governance import (
build_admin_write_governance_payload,
build_admin_write_governance_source_payload,
)
from admin_app.core.settings import AdminSettings
from shared.contracts import (
BOT_GOVERNED_SETTINGS,
FunctionalConfigurationPropagation,
MODEL_RUNTIME_PROFILES,
MODEL_RUNTIME_SEPARATION_RULES,
SYSTEM_FUNCTIONAL_CONFIGURATIONS,
get_functional_configuration,
)
class SystemService:
@ -56,6 +68,95 @@ class SystemService:
def build_security_configuration_payload(self) -> AdminCredentialStrategy:
return self.security_service.build_credential_strategy()
def build_model_runtime_separation_payload(self) -> dict:
atendimento_configuration = get_functional_configuration("atendimento_runtime_profile")
tool_generation_configuration = get_functional_configuration("tool_generation_runtime_profile")
if atendimento_configuration is None or tool_generation_configuration is None:
raise RuntimeError("Shared functional configuration contracts are not available.")
return {
"runtime_profiles": [
self._serialize_model_runtime_profile(runtime_profile)
for runtime_profile in MODEL_RUNTIME_PROFILES
],
"separation_rules": list(MODEL_RUNTIME_SEPARATION_RULES),
"atendimento_runtime_configuration": self._serialize_functional_configuration(
atendimento_configuration
),
"tool_generation_runtime_configuration": self._serialize_functional_configuration(
tool_generation_configuration
),
"bot_governed_parent_config_keys": sorted(
{setting.parent_config_key for setting in BOT_GOVERNED_SETTINGS}
),
}
def build_functional_configuration_catalog_payload(self) -> dict:
return {
"mode": "shared_contract_bootstrap",
"configurations": [
self._serialize_functional_configuration(configuration)
for configuration in SYSTEM_FUNCTIONAL_CONFIGURATIONS
],
"bot_governed_parent_config_keys": sorted(
{setting.parent_config_key for setting in BOT_GOVERNED_SETTINGS}
),
"next_steps": [
"Persistir estado funcional governado no admin antes da publicacao para o produto.",
"Adicionar versionamento, auditoria e aprovacao humana para configuracoes alteraveis.",
"Conectar o estado desejado do admin ao estado efetivo publicado no product.",
],
}
def get_functional_configuration_payload(self, config_key: str) -> dict | None:
configuration = get_functional_configuration(config_key)
if configuration is None:
return None
linked_bot_settings = [
self._serialize_bot_governed_setting(setting)
for setting in BOT_GOVERNED_SETTINGS
if setting.parent_config_key == configuration.config_key
]
related_runtime_profile = next(
(
self._serialize_model_runtime_profile(runtime_profile)
for runtime_profile in MODEL_RUNTIME_PROFILES
if runtime_profile.config_key == configuration.config_key
),
None,
)
return {
"configuration": self._serialize_functional_configuration(configuration),
"linked_bot_settings": linked_bot_settings,
"related_runtime_profile": related_runtime_profile,
"managed_by_bot_governance": bool(linked_bot_settings),
}
def build_bot_governed_configuration_payload(self) -> dict:
ordered_settings = sorted(
BOT_GOVERNED_SETTINGS,
key=lambda setting: (setting.parent_config_key, setting.area.value, setting.setting_key),
)
return {
"parent_config_keys": sorted(
{setting.parent_config_key for setting in BOT_GOVERNED_SETTINGS}
),
"settings": [
self._serialize_bot_governed_setting(setting)
for setting in ordered_settings
],
}
def build_write_governance_payload(self) -> dict:
payload = build_admin_write_governance_payload()
payload["governed_configuration_keys"] = sorted(
configuration.config_key
for configuration in SYSTEM_FUNCTIONAL_CONFIGURATIONS
if configuration.propagation == FunctionalConfigurationPropagation.VERSIONED_PUBLICATION
)
return payload
def build_configuration_sources_payload(self) -> list[dict]:
return [
{
@ -82,4 +183,80 @@ class SystemService:
"mutable": False,
"description": "Cookies e sessao web do painel derivam da configuracao ativa do admin.",
},
{
"key": "functional_configuration_contracts",
"source": "shared_contract",
"mutable": False,
"description": "Catalogo compartilhado das configuracoes funcionais governadas entre admin e product.",
},
{
"key": "bot_governed_configuration_contracts",
"source": "shared_contract",
"mutable": False,
"description": "Regras compartilhadas dos campos do bot que ficam sob governanca administrativa.",
},
{
"key": "model_runtime_separation",
"source": "shared_contract",
"mutable": False,
"description": "Contratos compartilhados que separam o runtime do atendimento do runtime de geracao de tools.",
},
build_admin_write_governance_source_payload(),
]
@staticmethod
def _serialize_model_runtime_profile(runtime_profile) -> dict:
return {
"runtime_target": runtime_profile.runtime_target,
"config_key": runtime_profile.config_key,
"catalog_runtime_target": runtime_profile.catalog_runtime_target,
"purpose": runtime_profile.purpose,
"consumed_by_service": runtime_profile.consumed_by_service,
"description": runtime_profile.description,
"read_permission": runtime_profile.read_permission,
"write_permission": runtime_profile.write_permission,
"published_independently": runtime_profile.published_independently,
"rollback_independently": runtime_profile.rollback_independently,
"cross_target_propagation_allowed": runtime_profile.cross_target_propagation_allowed,
"affects_customer_response": runtime_profile.affects_customer_response,
"can_generate_code": runtime_profile.can_generate_code,
}
@staticmethod
def _serialize_functional_configuration(configuration) -> dict:
return {
"config_key": configuration.config_key,
"domain": configuration.domain,
"description": configuration.description,
"source": configuration.source,
"read_permission": configuration.read_permission,
"write_permission": configuration.write_permission,
"mutability": configuration.mutability,
"propagation": configuration.propagation,
"affects_product_runtime": configuration.affects_product_runtime,
"direct_product_write_allowed": configuration.direct_product_write_allowed,
"fields": [
{
"name": field.name,
"description": field.description,
"writable": field.writable,
"secret": field.secret,
}
for field in configuration.fields
],
}
@staticmethod
def _serialize_bot_governed_setting(setting) -> dict:
return {
"setting_key": setting.setting_key,
"parent_config_key": setting.parent_config_key,
"field_name": setting.field_name,
"area": setting.area,
"description": setting.description,
"read_permission": setting.read_permission,
"write_permission": setting.write_permission,
"mutability": setting.mutability,
"versioned_publication_required": setting.versioned_publication_required,
"direct_product_write_allowed": setting.direct_product_write_allowed,
}

@ -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,116 @@
import unittest
from fastapi.testclient import TestClient
from admin_app.api.dependencies import get_current_staff_principal
from admin_app.app_factory import create_app
from admin_app.core import AdminSettings, AuthenticatedStaffPrincipal
from shared.contracts import StaffRole
class AdminBotFlowReportsWebTests(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_staff_principal] = lambda: AuthenticatedStaffPrincipal(
id=71,
email="colaborador@empresa.com" if role == StaffRole.COLABORADOR else "diretor@empresa.com",
display_name="Equipe de Operacao do Bot",
role=role,
is_active=True,
)
return TestClient(app), app
def test_bot_flow_reports_overview_requires_authentication(self):
app = create_app(AdminSettings(admin_auth_token_secret="test-secret", admin_api_prefix="/admin"))
client = TestClient(app)
response = client.get("/admin/reports/fluxo-bot/overview")
self.assertEqual(response.status_code, 401)
self.assertEqual(response.json()["detail"], "Autenticacao administrativa obrigatoria.")
def test_bot_flow_reports_overview_returns_bootstrap_structure_for_colaborador(self):
client, app = self._build_client_with_role(StaffRole.COLABORADOR)
try:
response = client.get("/admin/reports/fluxo-bot/overview", headers={"Authorization": "Bearer token"})
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 200)
payload = response.json()
self.assertEqual(payload["service"], "orquestrador-admin")
self.assertEqual(payload["area"], "fluxo_bot")
self.assertEqual(payload["source_domain"], "conversation")
self.assertEqual(payload["mode"], "bot_flow_contract_bootstrap")
self.assertEqual(payload["source_dataset_keys"], ["conversation_turns"])
self.assertEqual(payload["materialization"]["sync_strategy"], "etl_incremental")
self.assertEqual(len(payload["reports"]), 5)
self.assertIn("fallback_and_handoff", [item["report_key"] for item in payload["reports"]])
self.assertEqual(payload["metrics"][1]["value"], "5")
def test_bot_flow_reports_catalog_returns_initial_report_definitions(self):
client, app = self._build_client_with_role(StaffRole.COLABORADOR)
try:
response = client.get("/admin/reports/fluxo-bot/reports", headers={"Authorization": "Bearer token"})
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"], "shared_contract_catalog")
self.assertEqual(len(payload["reports"]), 5)
routing = next(item for item in payload["reports"] if item["report_key"] == "action_routing_flow")
self.assertEqual(routing["dataset_key"], "conversation_turns")
self.assertEqual(routing["default_granularity"], "aggregate")
self.assertEqual(routing["materialization_status"], "contract_defined_pending_snapshot_view")
self.assertIn("action", routing["supported_dimension_fields"])
def test_bot_flow_report_detail_returns_metrics_filters_and_dataset_contract(self):
client, app = self._build_client_with_role(StaffRole.COLABORADOR)
try:
response = client.get(
"/admin/reports/fluxo-bot/reports/operational_failures",
headers={"Authorization": "Bearer token"},
)
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 200)
payload = response.json()["report"]
self.assertEqual(payload["report_key"], "operational_failures")
self.assertEqual(payload["dataset_key"], "conversation_turns")
self.assertEqual(payload["default_time_field"], "started_at")
self.assertIn("errored_turns", [metric["key"] for metric in payload["metrics"]])
self.assertIn("action", [item["field_name"] for item in payload["dimensions"]])
self.assertIn("tool_name", [item["field_name"] for item in payload["filters"]])
self.assertFalse(payload["dataset"]["write_allowed"])
self.assertIn("assistant_response", [field["name"] for field in payload["dataset"]["blocked_fields"]])
def test_bot_flow_report_detail_returns_404_for_unknown_report(self):
client, app = self._build_client_with_role(StaffRole.COLABORADOR)
try:
response = client.get(
"/admin/reports/fluxo-bot/reports/channel_heatmap",
headers={"Authorization": "Bearer token"},
)
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 404)
self.assertEqual(response.json()["detail"], "Relatorio operacional do fluxo do bot nao encontrado.")
if __name__ == "__main__":
unittest.main()

@ -0,0 +1,126 @@
import unittest
from fastapi.testclient import TestClient
from admin_app.api.dependencies import get_current_staff_principal
from admin_app.app_factory import create_app
from admin_app.core import AdminSettings, AuthenticatedStaffPrincipal
from shared.contracts import StaffRole
class AdminConversationTelemetryReportsWebTests(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_staff_principal] = lambda: AuthenticatedStaffPrincipal(
id=81,
email="colaborador@empresa.com" if role == StaffRole.COLABORADOR else "diretor@empresa.com",
display_name="Equipe de Telemetria Conversacional",
role=role,
is_active=True,
)
return TestClient(app), app
def test_conversation_telemetry_reports_overview_requires_authentication(self):
app = create_app(AdminSettings(admin_auth_token_secret="test-secret", admin_api_prefix="/admin"))
client = TestClient(app)
response = client.get("/admin/reports/telemetria-conversacional/overview")
self.assertEqual(response.status_code, 401)
self.assertEqual(response.json()["detail"], "Autenticacao administrativa obrigatoria.")
def test_conversation_telemetry_reports_overview_returns_bootstrap_structure_for_colaborador(self):
client, app = self._build_client_with_role(StaffRole.COLABORADOR)
try:
response = client.get(
"/admin/reports/telemetria-conversacional/overview",
headers={"Authorization": "Bearer token"},
)
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 200)
payload = response.json()
self.assertEqual(payload["service"], "orquestrador-admin")
self.assertEqual(payload["area"], "telemetria_conversacional")
self.assertEqual(payload["source_domain"], "conversation")
self.assertEqual(payload["mode"], "conversation_telemetry_contract_bootstrap")
self.assertEqual(payload["source_dataset_keys"], ["conversation_turns"])
self.assertEqual(payload["materialization"]["sync_strategy"], "etl_incremental")
self.assertEqual(len(payload["reports"]), 5)
self.assertIn("latency_profile", [item["report_key"] for item in payload["reports"]])
self.assertEqual(payload["metrics"][1]["value"], "5")
def test_conversation_telemetry_reports_catalog_returns_initial_report_definitions(self):
client, app = self._build_client_with_role(StaffRole.COLABORADOR)
try:
response = client.get(
"/admin/reports/telemetria-conversacional/reports",
headers={"Authorization": "Bearer token"},
)
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"], "shared_contract_catalog")
self.assertEqual(len(payload["reports"]), 5)
volume = next(item for item in payload["reports"] if item["report_key"] == "conversation_volume")
self.assertEqual(volume["dataset_key"], "conversation_turns")
self.assertEqual(volume["default_granularity"], "aggregate")
self.assertEqual(volume["materialization_status"], "contract_defined_pending_snapshot_view")
self.assertIn("domain", volume["supported_dimension_fields"])
def test_conversation_telemetry_report_detail_returns_metrics_filters_and_dataset_contract(self):
client, app = self._build_client_with_role(StaffRole.COLABORADOR)
try:
response = client.get(
"/admin/reports/telemetria-conversacional/reports/latency_profile",
headers={"Authorization": "Bearer token"},
)
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 200)
payload = response.json()["report"]
self.assertEqual(payload["report_key"], "latency_profile")
self.assertEqual(payload["dataset_key"], "conversation_turns")
self.assertEqual(payload["default_time_field"], "completed_at")
self.assertIn("average_latency_ms", [metric["key"] for metric in payload["metrics"]])
self.assertIn("p95_latency_ms", [metric["key"] for metric in payload["metrics"]])
self.assertIn("intent", [item["field_name"] for item in payload["dimensions"]])
self.assertIn("completed_at", [item["field_name"] for item in payload["filters"]])
self.assertFalse(payload["dataset"]["write_allowed"])
self.assertIn("assistant_response", [field["name"] for field in payload["dataset"]["blocked_fields"]])
def test_conversation_telemetry_report_detail_returns_404_for_unknown_report(self):
client, app = self._build_client_with_role(StaffRole.COLABORADOR)
try:
response = client.get(
"/admin/reports/telemetria-conversacional/reports/sentiment_breakdown",
headers={"Authorization": "Bearer token"},
)
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 404)
self.assertEqual(
response.json()["detail"],
"Relatorio de telemetria conversacional nao encontrado.",
)
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=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()

@ -0,0 +1,117 @@
import unittest
from fastapi.testclient import TestClient
from admin_app.api.dependencies import get_current_staff_principal
from admin_app.app_factory import create_app
from admin_app.core import AdminSettings, AuthenticatedStaffPrincipal
from shared.contracts import StaffRole
class AdminRentalReportsWebTests(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_staff_principal] = lambda: AuthenticatedStaffPrincipal(
id=61,
email="colaborador@empresa.com" if role == StaffRole.COLABORADOR else "diretor@empresa.com",
display_name="Equipe de Locacao",
role=role,
is_active=True,
)
return TestClient(app), app
def test_rental_reports_overview_requires_authentication(self):
app = create_app(AdminSettings(admin_auth_token_secret="test-secret", admin_api_prefix="/admin"))
client = TestClient(app)
response = client.get("/admin/reports/locacao/overview")
self.assertEqual(response.status_code, 401)
self.assertEqual(response.json()["detail"], "Autenticacao administrativa obrigatoria.")
def test_rental_reports_overview_returns_bootstrap_structure_for_colaborador(self):
client, app = self._build_client_with_role(StaffRole.COLABORADOR)
try:
response = client.get("/admin/reports/locacao/overview", headers={"Authorization": "Bearer token"})
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 200)
payload = response.json()
self.assertEqual(payload["service"], "orquestrador-admin")
self.assertEqual(payload["area"], "locacao")
self.assertEqual(payload["source_domain"], "rental")
self.assertEqual(payload["mode"], "rental_contract_bootstrap")
self.assertEqual(payload["source_dataset_keys"], ["rental_fleet", "rental_contracts"])
self.assertEqual(payload["materialization"]["sync_strategy"], "etl_incremental")
self.assertEqual(len(payload["reports"]), 5)
self.assertIn("fleet_occupancy", [item["report_key"] for item in payload["reports"]])
self.assertEqual(payload["metrics"][1]["value"], "5")
def test_rental_reports_catalog_returns_initial_report_definitions(self):
client, app = self._build_client_with_role(StaffRole.COLABORADOR)
try:
response = client.get("/admin/reports/locacao/reports", headers={"Authorization": "Bearer token"})
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"], "shared_contract_catalog")
self.assertEqual(len(payload["reports"]), 5)
fleet = next(item for item in payload["reports"] if item["report_key"] == "fleet_availability")
self.assertEqual(fleet["dataset_key"], "rental_fleet")
self.assertEqual(fleet["default_granularity"], "aggregate")
self.assertEqual(fleet["materialization_status"], "contract_defined_pending_snapshot_view")
self.assertIn("categoria", fleet["supported_dimension_fields"])
def test_rental_report_detail_returns_metrics_filters_and_dataset_contract(self):
client, app = self._build_client_with_role(StaffRole.COLABORADOR)
try:
response = client.get(
"/admin/reports/locacao/reports/projected_vs_final_revenue",
headers={"Authorization": "Bearer token"},
)
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 200)
payload = response.json()["report"]
self.assertEqual(payload["report_key"], "projected_vs_final_revenue")
self.assertEqual(payload["dataset_key"], "rental_contracts")
self.assertEqual(payload["default_time_field"], "updated_at")
self.assertIn("projected_revenue", [metric["key"] for metric in payload["metrics"]])
self.assertIn("revenue_delta", [metric["key"] for metric in payload["metrics"]])
self.assertIn("status", [item["field_name"] for item in payload["dimensions"]])
self.assertIn("contrato_numero", [item["field_name"] for item in payload["filters"]])
self.assertFalse(payload["dataset"]["write_allowed"])
self.assertIn("cpf", [field["name"] for field in payload["dataset"]["blocked_fields"]])
def test_rental_report_detail_returns_404_for_unknown_report(self):
client, app = self._build_client_with_role(StaffRole.COLABORADOR)
try:
response = client.get(
"/admin/reports/locacao/reports/fleet_idle_heatmap",
headers={"Authorization": "Bearer token"},
)
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 404)
self.assertEqual(response.json()["detail"], "Relatorio de locacao nao encontrado.")
if __name__ == "__main__":
unittest.main()

@ -0,0 +1,114 @@
import unittest
from fastapi.testclient import TestClient
from admin_app.api.dependencies import get_current_staff_principal
from admin_app.app_factory import create_app
from admin_app.core import AdminSettings, AuthenticatedStaffPrincipal
from shared.contracts import StaffRole
class AdminReportsWebTests(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_staff_principal] = lambda: AuthenticatedStaffPrincipal(
id=31,
email="colaborador@empresa.com" if role == StaffRole.COLABORADOR else "diretor@empresa.com",
display_name="Equipe de Relatorios",
role=role,
is_active=True,
)
return TestClient(app), app
def test_reports_overview_requires_authentication(self):
app = create_app(AdminSettings(admin_auth_token_secret="test-secret", admin_api_prefix="/admin"))
client = TestClient(app)
response = client.get("/admin/reports/overview")
self.assertEqual(response.status_code, 401)
self.assertEqual(response.json()["detail"], "Autenticacao administrativa obrigatoria.")
def test_reports_overview_returns_contract_snapshot_for_colaborador(self):
client, app = self._build_client_with_role(StaffRole.COLABORADOR)
try:
response = client.get("/admin/reports/overview", headers={"Authorization": "Bearer token"})
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 200)
payload = response.json()
self.assertEqual(payload["service"], "orquestrador-admin")
self.assertEqual(payload["mode"], "shared_contract_bootstrap")
self.assertEqual(payload["materialization"]["sync_strategy"], "etl_incremental")
self.assertEqual(payload["materialization"]["storage_shape"], "snapshot_table")
self.assertEqual(payload["materialization"]["query_surface"], "dedicated_view")
self.assertIn("sales", [item["key"] for item in payload["report_families"]])
self.assertEqual(payload["metrics"][0]["value"], "8")
def test_report_datasets_return_catalog_for_colaborador(self):
client, app = self._build_client_with_role(StaffRole.COLABORADOR)
try:
response = client.get("/admin/reports/datasets", headers={"Authorization": "Bearer token"})
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 200)
payload = response.json()
self.assertEqual(payload["source"], "shared_contract_catalog")
self.assertEqual(len(payload["datasets"]), 8)
self.assertIn("sales_orders", [item["dataset_key"] for item in payload["datasets"]])
self.assertIn("conversation_turns", [item["dataset_key"] for item in payload["datasets"]])
sales = next(item for item in payload["datasets"] if item["dataset_key"] == "sales_orders")
self.assertEqual(sales["freshness_target"], "near_real_time")
self.assertEqual(sales["materialization_status"], "contract_defined_pending_snapshot_view")
def test_report_dataset_detail_returns_allowed_and_blocked_fields(self):
client, app = self._build_client_with_role(StaffRole.COLABORADOR)
try:
response = client.get(
"/admin/reports/datasets/sales_orders",
headers={"Authorization": "Bearer token"},
)
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 200)
payload = response.json()["dataset"]
self.assertEqual(payload["dataset_key"], "sales_orders")
self.assertEqual(payload["domain"], "sales")
self.assertEqual(payload["read_permission"], "view_reports")
self.assertFalse(payload["direct_product_query_allowed"])
self.assertFalse(payload["write_allowed"])
self.assertIn("numero_pedido", [field["name"] for field in payload["allowed_fields"]])
self.assertIn("cpf", [field["name"] for field in payload["blocked_fields"]])
def test_report_dataset_detail_returns_404_for_unknown_dataset(self):
client, app = self._build_client_with_role(StaffRole.COLABORADOR)
try:
response = client.get(
"/admin/reports/datasets/customers",
headers={"Authorization": "Bearer token"},
)
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 404)
self.assertEqual(
response.json()["detail"],
"Dataset operacional nao encontrado para relatorio.",
)
if __name__ == "__main__":
unittest.main()

@ -0,0 +1,116 @@
import unittest
from fastapi.testclient import TestClient
from admin_app.api.dependencies import get_current_staff_principal
from admin_app.app_factory import create_app
from admin_app.core import AdminSettings, AuthenticatedStaffPrincipal
from shared.contracts import StaffRole
class AdminRevenueReportsWebTests(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_staff_principal] = lambda: AuthenticatedStaffPrincipal(
id=51,
email="colaborador@empresa.com" if role == StaffRole.COLABORADOR else "diretor@empresa.com",
display_name="Equipe Financeira",
role=role,
is_active=True,
)
return TestClient(app), app
def test_revenue_reports_overview_requires_authentication(self):
app = create_app(AdminSettings(admin_auth_token_secret="test-secret", admin_api_prefix="/admin"))
client = TestClient(app)
response = client.get("/admin/reports/arrecadacao/overview")
self.assertEqual(response.status_code, 401)
self.assertEqual(response.json()["detail"], "Autenticacao administrativa obrigatoria.")
def test_revenue_reports_overview_returns_bootstrap_structure_for_colaborador(self):
client, app = self._build_client_with_role(StaffRole.COLABORADOR)
try:
response = client.get("/admin/reports/arrecadacao/overview", headers={"Authorization": "Bearer token"})
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 200)
payload = response.json()
self.assertEqual(payload["service"], "orquestrador-admin")
self.assertEqual(payload["area"], "arrecadacao")
self.assertEqual(payload["source_domain"], "rental")
self.assertEqual(payload["mode"], "revenue_contract_bootstrap")
self.assertEqual(payload["source_dataset_keys"], ["rental_payments"])
self.assertEqual(payload["materialization"]["sync_strategy"], "etl_incremental")
self.assertEqual(len(payload["reports"]), 3)
self.assertIn("collected_amount", [item["report_key"] for item in payload["reports"]])
self.assertEqual(payload["metrics"][1]["value"], "3")
def test_revenue_reports_catalog_returns_initial_report_definitions(self):
client, app = self._build_client_with_role(StaffRole.COLABORADOR)
try:
response = client.get("/admin/reports/arrecadacao/reports", headers={"Authorization": "Bearer token"})
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 200)
payload = response.json()
self.assertEqual(payload["area"], "arrecadacao")
self.assertEqual(payload["source_domain"], "rental")
self.assertEqual(payload["source"], "shared_contract_catalog")
self.assertEqual(len(payload["reports"]), 3)
collected = next(item for item in payload["reports"] if item["report_key"] == "collected_amount")
self.assertEqual(collected["dataset_key"], "rental_payments")
self.assertEqual(collected["default_granularity"], "aggregate")
self.assertEqual(collected["materialization_status"], "contract_defined_pending_snapshot_view")
self.assertIn("contrato_numero", collected["supported_dimension_fields"])
def test_revenue_report_detail_returns_metrics_filters_and_dataset_contract(self):
client, app = self._build_client_with_role(StaffRole.COLABORADOR)
try:
response = client.get(
"/admin/reports/arrecadacao/reports/contract_reconciliation",
headers={"Authorization": "Bearer token"},
)
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 200)
payload = response.json()["report"]
self.assertEqual(payload["report_key"], "contract_reconciliation")
self.assertEqual(payload["dataset_key"], "rental_payments")
self.assertEqual(payload["default_time_field"], "data_pagamento")
self.assertIn("collected_amount", [metric["key"] for metric in payload["metrics"]])
self.assertIn("contrato_numero", [item["field_name"] for item in payload["dimensions"]])
self.assertIn("protocolo", [item["field_name"] for item in payload["filters"]])
self.assertFalse(payload["dataset"]["write_allowed"])
self.assertIn("identificador_comprovante", [field["name"] for field in payload["dataset"]["blocked_fields"]])
def test_revenue_report_detail_returns_404_for_unknown_report(self):
client, app = self._build_client_with_role(StaffRole.COLABORADOR)
try:
response = client.get(
"/admin/reports/arrecadacao/reports/cash_flow_projection",
headers={"Authorization": "Bearer token"},
)
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 404)
self.assertEqual(response.json()["detail"], "Relatorio de arrecadacao nao encontrado.")
if __name__ == "__main__":
unittest.main()

@ -0,0 +1,114 @@
import unittest
from fastapi.testclient import TestClient
from admin_app.api.dependencies import get_current_staff_principal
from admin_app.app_factory import create_app
from admin_app.core import AdminSettings, AuthenticatedStaffPrincipal
from shared.contracts import StaffRole
class AdminSalesReportsWebTests(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_staff_principal] = lambda: AuthenticatedStaffPrincipal(
id=41,
email="colaborador@empresa.com" if role == StaffRole.COLABORADOR else "diretor@empresa.com",
display_name="Equipe Comercial",
role=role,
is_active=True,
)
return TestClient(app), app
def test_sales_reports_overview_requires_authentication(self):
app = create_app(AdminSettings(admin_auth_token_secret="test-secret", admin_api_prefix="/admin"))
client = TestClient(app)
response = client.get("/admin/reports/sales/overview")
self.assertEqual(response.status_code, 401)
self.assertEqual(response.json()["detail"], "Autenticacao administrativa obrigatoria.")
def test_sales_reports_overview_returns_bootstrap_structure_for_colaborador(self):
client, app = self._build_client_with_role(StaffRole.COLABORADOR)
try:
response = client.get("/admin/reports/sales/overview", headers={"Authorization": "Bearer token"})
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 200)
payload = response.json()
self.assertEqual(payload["service"], "orquestrador-admin")
self.assertEqual(payload["domain"], "sales")
self.assertEqual(payload["mode"], "sales_contract_bootstrap")
self.assertEqual(payload["source_dataset_keys"], ["sales_orders"])
self.assertEqual(payload["materialization"]["sync_strategy"], "etl_incremental")
self.assertEqual(len(payload["reports"]), 4)
self.assertIn("average_ticket", [item["report_key"] for item in payload["reports"]])
self.assertEqual(payload["metrics"][1]["value"], "4")
def test_sales_reports_catalog_returns_initial_report_definitions(self):
client, app = self._build_client_with_role(StaffRole.COLABORADOR)
try:
response = client.get("/admin/reports/sales/reports", headers={"Authorization": "Bearer token"})
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 200)
payload = response.json()
self.assertEqual(payload["domain"], "sales")
self.assertEqual(payload["source"], "shared_contract_catalog")
self.assertEqual(len(payload["reports"]), 4)
volume = next(item for item in payload["reports"] if item["report_key"] == "orders_volume")
self.assertEqual(volume["dataset_key"], "sales_orders")
self.assertEqual(volume["default_granularity"], "aggregate")
self.assertEqual(volume["materialization_status"], "contract_defined_pending_snapshot_view")
self.assertIn("status", volume["supported_dimension_fields"])
def test_sales_report_detail_returns_metrics_filters_and_dataset_contract(self):
client, app = self._build_client_with_role(StaffRole.COLABORADOR)
try:
response = client.get(
"/admin/reports/sales/reports/cancellations_by_period",
headers={"Authorization": "Bearer token"},
)
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 200)
payload = response.json()["report"]
self.assertEqual(payload["report_key"], "cancellations_by_period")
self.assertEqual(payload["dataset_key"], "sales_orders")
self.assertEqual(payload["default_time_field"], "data_cancelamento")
self.assertIn("cancelled_orders", [metric["key"] for metric in payload["metrics"]])
self.assertIn("motivo_cancelamento", [item["field_name"] for item in payload["dimensions"]])
self.assertIn("data_cancelamento", [item["field_name"] for item in payload["filters"]])
self.assertFalse(payload["dataset"]["write_allowed"])
self.assertIn("cpf", [field["name"] for field in payload["dataset"]["blocked_fields"]])
def test_sales_report_detail_returns_404_for_unknown_report(self):
client, app = self._build_client_with_role(StaffRole.COLABORADOR)
try:
response = client.get(
"/admin/reports/sales/reports/orders_by_store",
headers={"Authorization": "Bearer token"},
)
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 404)
self.assertEqual(response.json()["detail"], "Relatorio de vendas nao encontrado.")
if __name__ == "__main__":
unittest.main()

@ -84,7 +84,11 @@ class AdminSystemConfigurationWebTests(unittest.TestCase):
self.assertTrue(payload["security"]["password"]["pepper_configured"])
self.assertTrue(payload["security"]["bootstrap"]["enabled"])
self.assertTrue(payload["security"]["bootstrap"]["password_configured"])
self.assertEqual(payload["write_governance"]["mode"], "admin_internal_tables_only")
self.assertIn("staff_accounts", payload["write_governance"]["allowed_direct_write_tables"])
self.assertIn("orders", payload["write_governance"]["blocked_product_source_tables"])
self.assertIn("panel_session", [item["key"] for item in payload["sources"]])
self.assertIn("admin_write_governance", [item["key"] for item in payload["sources"]])
def test_runtime_configuration_route_exposes_panel_cookie_metadata(self):
settings = AdminSettings(
@ -130,6 +134,25 @@ class AdminSystemConfigurationWebTests(unittest.TestCase):
self.assertEqual(security["bootstrap"]["role"], "diretor")
def test_write_governance_route_exposes_internal_allowlist_and_product_blocks(self):
settings = AdminSettings(
admin_auth_token_secret="test-secret",
admin_api_prefix="/admin",
)
client, app = self._build_client_with_role(StaffRole.DIRETOR, settings)
try:
response = client.get("/admin/system/configuration/write-governance", headers={"Authorization": "Bearer token"})
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 200)
payload = response.json()["write_governance"]
self.assertEqual(payload["mode"], "admin_internal_tables_only")
self.assertIn("staff_sessions", payload["allowed_direct_write_tables"])
self.assertIn("conversation_turns", payload["blocked_product_source_tables"])
self.assertIn("channel_operation_policy", payload["governed_configuration_keys"])
if __name__ == "__main__":
unittest.main()

@ -0,0 +1,122 @@
import unittest
from fastapi.testclient import TestClient
from admin_app.api.dependencies import get_current_staff_principal
from admin_app.app_factory import create_app
from admin_app.core import AdminSettings, AuthenticatedStaffPrincipal
from shared.contracts import StaffRole
class AdminSystemFunctionalConfigurationWebTests(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_staff_principal] = lambda: AuthenticatedStaffPrincipal(
id=41,
email="colaborador@empresa.com" if role == StaffRole.COLABORADOR else "diretor@empresa.com",
display_name="Equipe de Configuracao",
role=role,
is_active=True,
)
return TestClient(app), app
def test_functional_configuration_catalog_requires_authentication(self):
app = create_app(AdminSettings(admin_auth_token_secret="test-secret", admin_api_prefix="/admin"))
client = TestClient(app)
response = client.get("/admin/system/configuration/functional")
self.assertEqual(response.status_code, 401)
self.assertEqual(response.json()["detail"], "Autenticacao administrativa obrigatoria.")
def test_colaborador_can_consult_functional_configuration_catalog(self):
client, app = self._build_client_with_role(StaffRole.COLABORADOR)
try:
response = client.get(
"/admin/system/configuration/functional",
headers={"Authorization": "Bearer token"},
)
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 200)
payload = response.json()
self.assertEqual(payload["mode"], "shared_contract_bootstrap")
self.assertEqual(len(payload["configurations"]), 6)
self.assertIn(
"allowed_model_catalog",
[item["config_key"] for item in payload["configurations"]],
)
self.assertIn(
"published_runtime_state",
[item["config_key"] for item in payload["configurations"]],
)
self.assertIn("atendimento_runtime_profile", payload["bot_governed_parent_config_keys"])
self.assertNotIn("tool_generation_runtime_profile", payload["bot_governed_parent_config_keys"])
def test_colaborador_can_consult_bot_governed_configuration_route(self):
client, app = self._build_client_with_role(StaffRole.COLABORADOR)
try:
response = client.get(
"/admin/system/configuration/functional/bot-governance",
headers={"Authorization": "Bearer token"},
)
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 200)
payload = response.json()
self.assertEqual(len(payload["settings"]), 15)
self.assertIn("bot_behavior_policy", payload["parent_config_keys"])
self.assertIn("channel_operation_policy", payload["parent_config_keys"])
self.assertNotIn("tool_generation_runtime_profile", payload["parent_config_keys"])
def test_functional_configuration_detail_links_runtime_and_bot_governance(self):
client, app = self._build_client_with_role(StaffRole.COLABORADOR)
try:
response = client.get(
"/admin/system/configuration/functional/atendimento_runtime_profile",
headers={"Authorization": "Bearer token"},
)
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 200)
payload = response.json()
self.assertEqual(payload["configuration"]["config_key"], "atendimento_runtime_profile")
self.assertTrue(payload["managed_by_bot_governance"])
self.assertEqual(payload["related_runtime_profile"]["runtime_target"], "atendimento")
self.assertIn(
"bot_tool_policy_ref",
[item["setting_key"] for item in payload["linked_bot_settings"]],
)
def test_functional_configuration_detail_returns_404_for_unknown_key(self):
client, app = self._build_client_with_role(StaffRole.COLABORADOR)
try:
response = client.get(
"/admin/system/configuration/functional/segredos_infra",
headers={"Authorization": "Bearer token"},
)
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 404)
self.assertEqual(
response.json()["detail"],
"Configuracao funcional do sistema nao encontrada.",
)
if __name__ == "__main__":
unittest.main()

@ -0,0 +1,117 @@
import unittest
from fastapi.testclient import TestClient
from admin_app.api.dependencies import get_current_staff_principal
from admin_app.app_factory import create_app
from admin_app.core import AdminSettings, AuthenticatedStaffPrincipal
from shared.contracts import StaffRole
class AdminSystemModelRuntimeConfigurationWebTests(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",
admin_environment="development",
admin_debug=True,
)
)
app.dependency_overrides[get_current_staff_principal] = lambda: AuthenticatedStaffPrincipal(
id=99,
email="diretor@empresa.com" if role == StaffRole.DIRETOR else "colaborador@empresa.com",
display_name="Equipe Interna",
role=role,
is_active=True,
)
return TestClient(app), app
def test_model_runtime_route_requires_manage_settings_permission(self):
client, app = self._build_client_with_role(StaffRole.COLABORADOR)
try:
response = client.get(
"/admin/system/configuration/model-runtimes",
headers={"Authorization": "Bearer token"},
)
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 403)
self.assertEqual(
response.json()["detail"],
"Permissao administrativa insuficiente: 'manage_settings'.",
)
def test_model_runtime_route_exposes_separated_atendimento_and_generation_profiles(self):
client, app = self._build_client_with_role(StaffRole.DIRETOR)
try:
response = client.get(
"/admin/system/configuration/model-runtimes",
headers={"Authorization": "Bearer token"},
)
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 200)
payload = response.json()["model_runtimes"]
self.assertEqual(len(payload["runtime_profiles"]), 2)
self.assertEqual(
{profile["config_key"] for profile in payload["runtime_profiles"]},
{"atendimento_runtime_profile", "tool_generation_runtime_profile"},
)
self.assertEqual(
{profile["consumed_by_service"] for profile in payload["runtime_profiles"]},
{"product", "admin"},
)
self.assertIn("no_implicit_propagation", payload["separation_rules"])
self.assertEqual(
payload["atendimento_runtime_configuration"]["config_key"],
"atendimento_runtime_profile",
)
self.assertEqual(
payload["tool_generation_runtime_configuration"]["config_key"],
"tool_generation_runtime_profile",
)
self.assertIn("prompt_profile_ref", [
field["name"] for field in payload["atendimento_runtime_configuration"]["fields"]
])
self.assertIn("reasoning_profile", [
field["name"] for field in payload["tool_generation_runtime_configuration"]["fields"]
])
self.assertIn("atendimento_runtime_profile", payload["bot_governed_parent_config_keys"])
self.assertNotIn("tool_generation_runtime_profile", payload["bot_governed_parent_config_keys"])
def test_configuration_overview_includes_model_runtime_separation_snapshot(self):
client, app = self._build_client_with_role(StaffRole.DIRETOR)
try:
response = client.get(
"/admin/system/configuration",
headers={"Authorization": "Bearer token"},
)
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 200)
payload = response.json()
self.assertEqual(
payload["model_runtimes"]["atendimento_runtime_configuration"]["config_key"],
"atendimento_runtime_profile",
)
self.assertEqual(
payload["model_runtimes"]["tool_generation_runtime_configuration"]["config_key"],
"tool_generation_runtime_profile",
)
self.assertIn(
"model_runtime_separation",
[item["key"] for item in payload["sources"]],
)
if __name__ == "__main__":
unittest.main()

@ -0,0 +1,60 @@
import unittest
from admin_app.db.write_governance import (
AdminWriteGovernanceViolation,
build_admin_write_governance_payload,
ensure_direct_admin_write_allowed,
enforce_admin_session_write_governance,
)
class _FakeTabledObject:
def __init__(self, table_name: str):
self.__tablename__ = table_name
class AdminWriteGovernanceTests(unittest.TestCase):
def test_payload_exposes_internal_allowlist_and_governed_targets(self):
payload = build_admin_write_governance_payload()
self.assertEqual(payload["mode"], "admin_internal_tables_only")
self.assertEqual(
payload["allowed_direct_write_tables"],
["admin_audit_logs", "staff_accounts", "staff_sessions"],
)
self.assertIn("sales_orders", payload["blocked_operational_dataset_keys"])
self.assertIn("orders", payload["blocked_product_source_tables"])
self.assertIn("conversation_turns", payload["blocked_product_source_tables"])
self.assertIn("atendimento_runtime_profile", payload["governed_configuration_keys"])
self.assertIn("bot_behavior_policy", payload["governed_configuration_keys"])
def test_internal_admin_tables_are_allowed_for_direct_write(self):
ensure_direct_admin_write_allowed("staff_accounts")
ensure_direct_admin_write_allowed("staff_sessions")
ensure_direct_admin_write_allowed("admin_audit_logs")
def test_unknown_or_product_tables_raise_governance_violation(self):
with self.assertRaises(AdminWriteGovernanceViolation):
ensure_direct_admin_write_allowed("orders")
with self.assertRaises(AdminWriteGovernanceViolation):
ensure_direct_admin_write_allowed("conversation_turns")
def test_session_guard_accepts_only_internal_admin_tables(self):
enforce_admin_session_write_governance(
new=(_FakeTabledObject("staff_accounts"),),
dirty=(_FakeTabledObject("staff_sessions"),),
deleted=(_FakeTabledObject("admin_audit_logs"),),
)
def test_session_guard_blocks_direct_operational_write_attempt(self):
with self.assertRaises(AdminWriteGovernanceViolation) as context:
enforce_admin_session_write_governance(
new=(_FakeTabledObject("orders"),),
)
self.assertIn("fluxo governado, versionado e auditavel", str(context.exception))
if __name__ == "__main__":
unittest.main()
Loading…
Cancel
Save