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

641 lines
27 KiB
Python

from fastapi import APIRouter, Depends, Request
from fastapi.responses import HTMLResponse, RedirectResponse, Response
from admin_app.api.dependencies import get_optional_panel_staff_context
from admin_app.core import AdminSettings, AuthenticatedStaffContext, get_admin_settings
from admin_app.services import ToolManagementService
from admin_app.view.assets import PANEL_STATIC_MOUNT_NAME
from admin_app.view.rendering import (
render_collaborator_management_page,
render_login_page,
render_panel_home,
render_tool_intake_page,
render_tool_review_page,
)
from admin_app.view.view_models import (
AdminCollaboratorManagementPageView,
AdminLoginPageView,
AdminPanelHomeView,
AdminPanelMetric,
AdminPanelModuleCard,
AdminPanelNavigationItem,
AdminPanelQuickAction,
AdminPanelRoadmapItem,
AdminPanelSurfaceLink,
AdminToolIntakeDomainOption,
AdminToolIntakePageView,
AdminToolIntakeParameterTypeOption,
AdminToolReviewPageView,
AdminToolReviewWorkflowStep,
)
from shared.contracts import AdminPermission, StaffRole, role_has_permission
panel_router = APIRouter(tags=["panel"])
@panel_router.get("/panel", name="panel_entry")
def panel_entry(
request: Request,
current_context: AuthenticatedStaffContext | None = Depends(get_optional_panel_staff_context),
) -> RedirectResponse:
target_route_name = "panel_home" if current_context is not None else "admin_login_view"
return _redirect_to_route(request, target_route_name)
@panel_router.get("/panel/admin", response_class=HTMLResponse, name="panel_home")
def panel_home(
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")
settings = _resolve_settings(request)
view = _build_home_view(request, settings, current_context)
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_panel_home(view, css_href=css_href, js_href=js_href))
@panel_router.get("/login", response_class=HTMLResponse, name="admin_login_view")
def login_page(
request: Request,
current_context: AuthenticatedStaffContext | None = Depends(get_optional_panel_staff_context),
) -> Response:
if current_context is not None:
return _redirect_to_route(request, "panel_home")
settings = _resolve_settings(request)
view = _build_login_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_login_page(view, css_href=css_href, js_href=js_href))
@panel_router.get("/panel/tools/new", response_class=HTMLResponse, name="admin_tool_intake_view")
def tool_intake_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")
settings = _resolve_settings(request)
view = _build_tool_intake_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_tool_intake_page(view, css_href=css_href, js_href=js_href))
@panel_router.get("/panel/tools/review", response_class=HTMLResponse, name="admin_tool_review_view")
def tool_review_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")
settings = _resolve_settings(request)
view = _build_tool_review_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_tool_review_page(view, css_href=css_href, js_href=js_href))
@panel_router.get("/panel/colaboradores/gestao", response_class=HTMLResponse, name="admin_collaborator_management_view")
def collaborator_management_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.MANAGE_STAFF_ACCOUNTS):
return _redirect_to_route(request, "panel_home")
settings = _resolve_settings(request)
view = _build_collaborator_management_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_collaborator_management_page(view, css_href=css_href, js_href=js_href))
def _build_home_view(
request: Request,
settings: AdminSettings,
current_context: AuthenticatedStaffContext,
) -> AdminPanelHomeView:
panel_href = str(request.url_for("panel_home"))
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")
audit_href = _build_prefixed_path(settings.admin_api_prefix, "/audit/events")
can_manage_collaborators = role_has_permission(
current_context.principal.role,
AdminPermission.MANAGE_STAFF_ACCOUNTS,
)
navigation = [
AdminPanelNavigationItem(
label="Dashboard",
href=panel_href,
description="Entrada principal do ambiente interno.",
badge="Ativo",
is_active=True,
),
AdminPanelNavigationItem(
label="Cadastro de tools",
href=tool_intake_view_href,
description="Pre-cadastro validado para novas tools antes da revisao.",
badge="Novo",
),
AdminPanelNavigationItem(
label="Revisao de tools",
href=tool_review_view_href,
description="Fluxo humano de revisao, aprovacao e ativacao.",
badge="Operacao",
),
AdminPanelNavigationItem(
label="Areas do sistema",
href="#modules",
description="Mapa claro dos modulos internos disponiveis.",
badge="Painel",
),
AdminPanelNavigationItem(
label="Fluxo recomendado",
href="#workflow",
description="Sequencia sugerida para operar o admin.",
badge="Guia",
),
]
quick_actions = [
AdminPanelQuickAction(
label="Cadastrar tool",
href=tool_intake_view_href,
button_class="btn-dark",
),
AdminPanelQuickAction(
label="Revisar tools",
href=tool_review_view_href,
button_class="btn-outline-dark",
),
AdminPanelQuickAction(
label="Ver areas",
href="#modules",
button_class="btn-outline-secondary",
),
]
modules = [
AdminPanelModuleCard(
eyebrow="Fluxo de entrada",
title="Cadastro de tools",
description="Tela real para o colaborador preencher metadados, definir parametros e validar o pre-cadastro antes da revisao humana.",
status_label="Tela ativa",
status_variant="success",
highlights=(
"Formulario protegido por sessao web",
"Preview validado antes da persistencia",
"Direcao clara para revisao de diretor",
),
cta_label="Abrir cadastro",
href=tool_intake_view_href,
is_available=True,
),
AdminPanelModuleCard(
eyebrow="Fluxo principal",
title="Revisao de tools",
description="Area operacional do painel para leitura da fila, aprovacao humana e ativacao controlada.",
status_label="Tela ativa",
status_variant="success",
highlights=(
"Fila protegida por sessao web",
"Catalogo ativo para comparacao",
"Leitura clara do workflow de aprovacao",
),
cta_label="Abrir revisao",
href=tool_review_view_href,
is_available=True,
),
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",
highlights=(
"Runtime e banco monitorados",
"Politicas de credencial centralizadas",
"Base pronta para futura tela dedicada",
),
),
AdminPanelModuleCard(
eyebrow="Governanca",
title="Auditoria operacional",
description="Eventos de login, logout, aprovacao e publicacao continuam registrados para rastreabilidade.",
status_label="Auditavel",
status_variant="secondary",
highlights=(
"Historico de operacao interna",
"Base para filtros e timeline",
"Suporte a conformidade do fluxo administrativo",
),
),
]
surface_links = [
AdminPanelSurfaceLink(
method="Acesso",
label="Dashboard administrativa",
href=panel_href,
description="Entrada principal do time interno depois do login.",
),
AdminPanelSurfaceLink(
method="Cadastro",
label="Nova tool",
href=tool_intake_view_href,
description="Formulario real para validar o pre-cadastro de uma nova tool.",
),
AdminPanelSurfaceLink(
method="Operacao",
label="Revisao de tools",
href=tool_review_view_href,
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.",
),
AdminPanelSurfaceLink(
method="Auditoria",
label="Eventos administrativos",
href=audit_href,
description="Consulta de eventos internos para rastrear operacoes sensiveis.",
),
]
roadmap = [
AdminPanelRoadmapItem(
step="01",
title="Entrar pelo login administrativo",
description="A sessao web libera o ambiente interno e evita navegacao confusa antes da autenticacao.",
status_label="Obrigatorio",
),
AdminPanelRoadmapItem(
step="02",
title="Passar pela dashboard",
description="A home protegida organiza os modulos e mostra por onde comecar a operacao.",
status_label="Entrada",
),
AdminPanelRoadmapItem(
step="03",
title="Cadastrar ou validar o pre-draft",
description="Use a nova tela para descrever a tool, seus parametros e o objetivo operacional antes da revisao.",
status_label="Cadastro",
),
AdminPanelRoadmapItem(
step="04",
title="Abrir revisao de tools",
description="Encaminhe a ferramenta para analise humana, aprovacao e ativacao controlada.",
status_label="Principal",
),
AdminPanelRoadmapItem(
step="05",
title="Consultar runtime e auditoria",
description="Quando necessario, acompanhe configuracao e eventos do admin para suportar a decisao operacional.",
status_label="Suporte",
),
]
if can_manage_collaborators:
navigation.insert(
3,
AdminPanelNavigationItem(
label="Colaboradores",
href=collaborator_management_view_href,
description="Cadastro e governanca de acessos internos, exclusivo para diretor.",
badge="Diretor",
),
)
quick_actions.insert(
2,
AdminPanelQuickAction(
label="Gerir equipe",
href=collaborator_management_view_href,
button_class="btn-outline-dark",
),
)
modules.insert(
2,
AdminPanelModuleCard(
eyebrow="Governanca de acesso",
title="Gestao de colaboradores",
description="Tela dedicada para o diretor criar contas internas, acompanhar o status da equipe e manter a entrada administrativa sob controle.",
status_label="Tela ativa",
status_variant="dark",
highlights=(
"Criacao de colaborador com senha inicial",
"Ativacao e desativacao sem tocar no banco manualmente",
"Fluxo exclusivo para diretor",
),
cta_label="Abrir equipe",
href=collaborator_management_view_href,
is_available=True,
),
)
surface_links.insert(
3,
AdminPanelSurfaceLink(
method="Equipe",
label="Gestao de colaboradores",
href=collaborator_management_view_href,
description="Controle de acesso interno para cadastrar e administrar a equipe administrativa.",
),
)
roadmap = [
AdminPanelRoadmapItem(
step="01",
title="Entrar pelo login administrativo",
description="A sessao web libera o ambiente interno e evita navegacao confusa antes da autenticacao.",
status_label="Obrigatorio",
),
AdminPanelRoadmapItem(
step="02",
title="Passar pela dashboard",
description="A home protegida organiza os modulos e mostra por onde comecar a operacao.",
status_label="Entrada",
),
AdminPanelRoadmapItem(
step="03",
title="Cadastrar ou validar o pre-draft",
description="Use a nova tela para descrever a tool, seus parametros e o objetivo operacional antes da revisao.",
status_label="Cadastro",
),
AdminPanelRoadmapItem(
step="04",
title="Organizar a equipe interna",
description="Diretores podem cadastrar novos colaboradores e controlar rapidamente o status de acesso administrativo.",
status_label="Diretor",
),
AdminPanelRoadmapItem(
step="05",
title="Abrir revisao de tools",
description="Encaminhe a ferramenta para analise humana, aprovacao e ativacao controlada.",
status_label="Principal",
),
AdminPanelRoadmapItem(
step="06",
title="Consultar runtime e auditoria",
description="Quando necessario, acompanhe configuracao e eventos do admin para suportar a decisao operacional.",
status_label="Suporte",
),
]
return AdminPanelHomeView(
service="orquestrador-admin",
app_name=settings.admin_app_name,
panel_title="Painel Administrativo",
panel_subtitle=(
"Area interna protegida para operar o admin com mais clareza, foco e navegacao orientada por fluxo."
),
environment=settings.admin_environment,
version=settings.admin_version,
api_prefix=settings.admin_api_prefix or "/",
release_label="Bootstrap UI v1",
navigation=tuple(navigation),
quick_actions=tuple(quick_actions),
metrics=(
AdminPanelMetric(
label="Runtimes independentes",
value="2",
description="Produto e admin seguem isolados para deploy e operacao.",
),
AdminPanelMetric(
label="Perfis internos",
value=str(len(StaffRole)),
description="Hierarquia base com colaborador e diretor.",
),
AdminPanelMetric(
label="Permissoes administrativas",
value=str(len(AdminPermission)),
description="Camada pronta para crescer por modulo sem misturar contexto.",
),
AdminPanelMetric(
label="Refresh token",
value=f"{settings.admin_auth_refresh_token_ttl_days} dias",
description="Sessao web persistida com renovacao controlada.",
),
),
modules=tuple(modules),
surface_links=tuple(surface_links),
roadmap=tuple(roadmap),
)
def _build_login_view(request: Request, settings: AdminSettings) -> AdminLoginPageView:
dashboard_href = str(request.url_for("panel_home"))
auth_endpoint = _build_prefixed_path(settings.admin_api_prefix, "/panel/auth/login")
password_requirements = []
if settings.admin_auth_password_require_uppercase:
password_requirements.append("maiuscula")
if settings.admin_auth_password_require_lowercase:
password_requirements.append("minuscula")
if settings.admin_auth_password_require_digit:
password_requirements.append("digito")
if settings.admin_auth_password_require_symbol:
password_requirements.append("simbolo")
password_policy_label = (
f"Minimo de {settings.admin_auth_password_min_length} caracteres"
+ (f" com {', '.join(password_requirements)}." if password_requirements else ".")
)
return AdminLoginPageView(
app_name=settings.admin_app_name,
title="Login administrativo",
subtitle=(
"Entre primeiro com sua conta interna. A dashboard e os modulos do sistema so aparecem depois da autenticacao."
),
environment=settings.admin_environment,
version=settings.admin_version,
dashboard_href=dashboard_href,
auth_endpoint=auth_endpoint,
email_placeholder="voce@empresa.com",
password_placeholder="Sua senha administrativa",
access_token_ttl_label=f"{settings.admin_auth_access_token_ttl_minutes} minutos",
refresh_token_ttl_label=f"{settings.admin_auth_refresh_token_ttl_days} dias",
password_policy_label=password_policy_label,
security_highlights=(
"Identidade separada do usuario de atendimento",
"Rotacao de refresh token ja implementada",
"Trilha de auditoria para login e logout",
),
integration_notes=(
"A dashboard administrativa so aparece depois da autenticacao do StaffAccount.",
"Revisao, configuracao e operacao interna ficam atras da sessao web do painel.",
"Cookies httpOnly e refresh token rotacionado mantem a sessao do navegador protegida.",
),
)
def _build_tool_intake_view(request: Request, settings: AdminSettings) -> AdminToolIntakePageView:
service = ToolManagementService(settings)
form_payload = service.build_draft_form_payload()
return AdminToolIntakePageView(
app_name=settings.admin_app_name,
title="Cadastro de nova tool",
subtitle=(
"Formulario guiado para o colaborador estruturar uma nova tool, validar o pre-draft e encaminhar a proposta para revisao de diretor."
),
environment=settings.admin_environment,
version=settings.admin_version,
dashboard_href=str(request.url_for("panel_home")),
review_href=str(request.url_for("admin_tool_review_view")),
intake_endpoint=_build_prefixed_path(settings.admin_api_prefix, "/panel/tools/drafts/intake"),
domain_options=tuple(
AdminToolIntakeDomainOption(
value=item["value"],
label=item["label"],
description=item["description"],
)
for item in form_payload["domain_options"]
),
parameter_type_options=tuple(
AdminToolIntakeParameterTypeOption(
value=item["code"].value,
label=item["label"],
description=item["description"],
)
for item in form_payload["parameter_types"]
),
naming_rules=tuple(form_payload["naming_rules"]),
submission_notes=tuple(form_payload["submission_notes"]),
approval_notes=tuple(form_payload["approval_notes"]),
)
def _build_tool_review_view(request: Request, settings: AdminSettings) -> AdminToolReviewPageView:
dashboard_href = str(request.url_for("panel_home"))
overview_endpoint = _build_prefixed_path(settings.admin_api_prefix, "/panel/tools/overview")
contracts_endpoint = _build_prefixed_path(settings.admin_api_prefix, "/panel/tools/contracts")
review_queue_endpoint = _build_prefixed_path(settings.admin_api_prefix, "/panel/tools/review-queue")
publications_endpoint = _build_prefixed_path(settings.admin_api_prefix, "/panel/tools/publications")
return AdminToolReviewPageView(
app_name=settings.admin_app_name,
title="Revisao, aprovacao e ativacao",
subtitle=(
"Hub visual para o time interno acompanhar a fila de revisao, validar o contrato compartilhado e inspecionar o catalogo de tools ativas antes da ativacao."
),
environment=settings.admin_environment,
version=settings.admin_version,
dashboard_href=dashboard_href,
overview_endpoint=overview_endpoint,
contracts_endpoint=contracts_endpoint,
review_queue_endpoint=review_queue_endpoint,
publications_endpoint=publications_endpoint,
workflow=(
AdminToolReviewWorkflowStep(
eyebrow="Leitura inicial",
title="Revisar fila",
description="Carregar a fila de geracao e entender em que gate cada item se encontra.",
status_label="Revisao",
status_variant="info",
),
AdminToolReviewWorkflowStep(
eyebrow="Decisao humana",
title="Aprovar com criterio",
description="Conferir contrato, parametros e prontidao tecnica antes de liberar a proxima etapa.",
status_label="Aprovacao",
status_variant="warning",
),
AdminToolReviewWorkflowStep(
eyebrow="Publicacao",
title="Ativar no catalogo",
description="Usar o catalogo publicado como referencia para a versao que chega ao runtime de produto.",
status_label="Ativacao",
status_variant="success",
),
),
review_notes=(
"Conferir se o gate do item combina com o estado esperado do lifecycle.",
"Observar se a descricao e o objetivo operacional da tool estao claros para o time.",
"Usar o catalogo ativo como comparativo antes de promover uma nova versao.",
),
approval_notes=(
"Verificar nome, descricao e semantica dos parametros antes da aprovacao.",
"Confirmar se a tool respeita a separacao entre admin e product definida nas ADRs.",
"Checar se a publicacao planejada e auditavel e segura para o runtime de produto.",
),
activation_notes=(
"Publicacoes ativas exigem papel com permissao publish_tools.",
"A leitura do catalogo e feita via sessao web do painel para facilitar a operacao do navegador.",
"Sem permissao de publicacao, a tela continua util para revisao, mas bloqueia o catalogo ativo.",
),
)
def _build_collaborator_management_view(
request: Request,
settings: AdminSettings,
) -> AdminCollaboratorManagementPageView:
password_requirements = []
if settings.admin_auth_password_require_uppercase:
password_requirements.append("maiuscula")
if settings.admin_auth_password_require_lowercase:
password_requirements.append("minuscula")
if settings.admin_auth_password_require_digit:
password_requirements.append("digito")
if settings.admin_auth_password_require_symbol:
password_requirements.append("simbolo")
password_policy_label = (
f"Minimo de {settings.admin_auth_password_min_length} caracteres"
+ (f" com {', '.join(password_requirements)}." if password_requirements else ".")
)
return AdminCollaboratorManagementPageView(
app_name=settings.admin_app_name,
title="Gestao de colaboradores",
subtitle=(
"Tela exclusiva de diretor para organizar a equipe interna, criar novos acessos administrativos e controlar rapidamente quem segue ativo no painel."
),
environment=settings.admin_environment,
version=settings.admin_version,
dashboard_href=str(request.url_for("panel_home")),
collection_endpoint=_build_prefixed_path(settings.admin_api_prefix, "/panel/colaboradores"),
password_policy_label=password_policy_label,
onboarding_notes=(
"Novos acessos nascem sempre com papel de colaborador.",
"A senha inicial ja respeita a mesma politica do login administrativo.",
"Ativar ou desativar colaborador nao exige acesso direto ao banco.",
),
governance_notes=(
"Somente diretor acessa esta tela e as rotas de gestao de colaboradores.",
"Cada criacao e alteracao de status gera trilha de auditoria administrativa.",
"A conta de diretor continua fora deste fluxo para evitar mudancas acidentais na governanca principal.",
),
)
def _redirect_to_route(request: Request, route_name: str) -> RedirectResponse:
return RedirectResponse(url=str(request.url_for(route_name)), status_code=302)
def _resolve_settings(request: Request) -> AdminSettings:
app_settings = getattr(request.app.state, "admin_settings", None)
if isinstance(app_settings, AdminSettings):
return app_settings
return get_admin_settings()
def _build_prefixed_path(api_prefix: str, path: str) -> str:
normalized_prefix = api_prefix.rstrip("/")
normalized_path = path if path.startswith("/") else f"/{path}"
if not normalized_prefix:
return normalized_path
if normalized_path == "/":
return f"{normalized_prefix}/"
return f"{normalized_prefix}{normalized_path}"