Compare commits

...

2 Commits

Author SHA1 Message Date
Vitor Hugo Belorio Simão e210b56b37 ♻️ refactor(admin): simplificar contrato das views
Remove campos que ficaram obsoletos depois da protecao do fluxo web e da simplificacao da tela publica de login.

Com isso, os modelos do painel refletem melhor o comportamento atual da interface administrativa e reduzem ambiguidade para as proximas iteracoes da fase 3.
2 weeks ago
Vitor Hugo Belorio Simão ed1a36ceb6 feat(admin): implementar painel administrativo base
Estrutura a fase 3 do orquestrador-admin com uma camada web propria para o painel interno, assets dedicados e uma dashboard administrativa em Bootstrap para o fluxo do staff.

Integra autenticacao web com cookies httpOnly, protege a navegacao do painel, adiciona snapshots de configuracao do sistema e entrega as primeiras superficies de gestao de tools para revisao, aprovacao e ativacao.

Tambem amplia a cobertura com testes para bootstrap do app, autenticacao web do painel, configuracao administrativa, governanca de tools e validacao da sessao administrativa no navegador.
2 weeks ago

@ -1,7 +1,8 @@
from fastapi import Depends, HTTPException, Request, status
from fastapi import Depends, HTTPException, Request, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from sqlalchemy.orm import Session
from admin_app.api.panel_session import get_panel_access_cookie
from admin_app.core import (
AdminSecurityService,
AdminSettings,
@ -85,18 +86,72 @@ def get_current_staff_context(
) from exc
def get_current_panel_staff_context(
request: Request,
auth_service: AuthService = Depends(get_auth_service),
) -> AuthenticatedStaffContext:
access_token = get_panel_access_cookie(request)
if not access_token:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Sessao administrativa web obrigatoria.",
)
try:
return auth_service.get_authenticated_context(access_token)
except ValueError as exc:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Sessao administrativa web invalida.",
) from exc
def get_optional_panel_staff_context(
request: Request,
auth_service: AuthService = Depends(get_auth_service),
) -> AuthenticatedStaffContext | None:
access_token = get_panel_access_cookie(request)
if not access_token:
return None
try:
return auth_service.get_authenticated_context(access_token)
except ValueError:
return None
def get_current_staff_principal(
context: AuthenticatedStaffContext = Depends(get_current_staff_context),
) -> AuthenticatedStaffPrincipal:
return context.principal
def get_current_panel_staff_principal(
context: AuthenticatedStaffContext = Depends(get_current_panel_staff_context),
) -> AuthenticatedStaffPrincipal:
return context.principal
def get_optional_panel_staff_principal(
context: AuthenticatedStaffContext | None = Depends(get_optional_panel_staff_context),
) -> AuthenticatedStaffPrincipal | None:
if context is None:
return None
return context.principal
def get_current_staff_session_id(
context: AuthenticatedStaffContext = Depends(get_current_staff_context),
) -> int:
return context.session_id
def get_current_panel_staff_session_id(
context: AuthenticatedStaffContext = Depends(get_current_panel_staff_context),
) -> int:
return context.session_id
def require_staff_role(minimum_role: StaffRole):
def dependency(
current_staff: AuthenticatedStaffPrincipal = Depends(get_current_staff_principal),
@ -125,7 +180,21 @@ def require_admin_permission(permission: AdminPermission):
return dependency
def require_panel_admin_permission(permission: AdminPermission):
def dependency(
current_staff: AuthenticatedStaffPrincipal = Depends(get_current_panel_staff_principal),
) -> AuthenticatedStaffPrincipal:
if not role_has_permission(current_staff.role, permission):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"Permissao administrativa insuficiente: '{permission.value}'.",
)
return current_staff
return dependency
def get_current_staff_permissions(
current_staff: AuthenticatedStaffPrincipal = Depends(get_current_staff_principal),
) -> tuple[str, ...]:
return tuple(permission.value for permission in permissions_for_role(current_staff.role))
return tuple(permission.value for permission in permissions_for_role(current_staff.role))

@ -0,0 +1,67 @@
from fastapi import Request, Response
from admin_app.core import AdminAuthenticatedSession, AdminSettings
PANEL_ACCESS_COOKIE_NAME = "orquestrador_admin_panel_access"
PANEL_REFRESH_COOKIE_NAME = "orquestrador_admin_panel_refresh"
PANEL_COOKIE_SAMESITE = "lax"
def get_panel_access_cookie(request: Request) -> str | None:
return request.cookies.get(PANEL_ACCESS_COOKIE_NAME)
def get_panel_refresh_cookie(request: Request) -> str | None:
return request.cookies.get(PANEL_REFRESH_COOKIE_NAME)
def set_panel_auth_cookies(
response: Response,
session: AdminAuthenticatedSession,
settings: AdminSettings,
) -> None:
cookie_path = build_panel_cookie_path(settings)
use_secure = should_use_secure_cookies(settings)
response.set_cookie(
key=PANEL_ACCESS_COOKIE_NAME,
value=session.access_token,
max_age=session.expires_in_seconds,
httponly=True,
secure=use_secure,
samesite=PANEL_COOKIE_SAMESITE,
path=cookie_path,
)
response.set_cookie(
key=PANEL_REFRESH_COOKIE_NAME,
value=session.refresh_token,
max_age=settings.admin_auth_refresh_token_ttl_days * 24 * 60 * 60,
httponly=True,
secure=use_secure,
samesite=PANEL_COOKIE_SAMESITE,
path=cookie_path,
)
def clear_panel_auth_cookies(response: Response, settings: AdminSettings) -> None:
cookie_path = build_panel_cookie_path(settings)
response.delete_cookie(
key=PANEL_ACCESS_COOKIE_NAME,
path=cookie_path,
httponly=True,
samesite=PANEL_COOKIE_SAMESITE,
)
response.delete_cookie(
key=PANEL_REFRESH_COOKIE_NAME,
path=cookie_path,
httponly=True,
samesite=PANEL_COOKIE_SAMESITE,
)
def build_panel_cookie_path(settings: AdminSettings) -> str:
normalized_prefix = settings.admin_api_prefix.rstrip("/")
return normalized_prefix or "/"
def should_use_secure_cookies(settings: AdminSettings) -> bool:
return settings.admin_environment.lower() == "production" and not settings.admin_debug

@ -2,10 +2,16 @@ from fastapi import APIRouter
from admin_app.api.routes.audit import router as audit_router
from admin_app.api.routes.auth import router as auth_router
from admin_app.api.routes.panel_auth import router as panel_auth_router
from admin_app.api.routes.panel_tools import router as panel_tools_router
from admin_app.api.routes.system import router as system_router
from admin_app.api.routes.tools import router as tools_router
# Agrega as rotas do servico administrativo.
api_router = APIRouter()
api_router.include_router(auth_router)
api_router.include_router(panel_auth_router)
api_router.include_router(panel_tools_router)
api_router.include_router(system_router)
api_router.include_router(audit_router)
api_router.include_router(tools_router)
api_router.include_router(audit_router)

@ -0,0 +1,178 @@
from fastapi import APIRouter, Depends, HTTPException, Request, Response, status
from admin_app.api.dependencies import (
get_auth_service,
get_current_panel_staff_context,
get_settings,
)
from admin_app.api.panel_session import (
clear_panel_auth_cookies,
get_panel_access_cookie,
get_panel_refresh_cookie,
set_panel_auth_cookies,
)
from admin_app.api.schemas import (
AdminAuthenticatedStaffResponse,
AdminLoginRequest,
AdminPanelLogoutResponse,
AdminPanelWebSessionResponse,
)
from admin_app.core import AdminAuthenticatedSession, AdminSettings, AuthenticatedStaffContext
from admin_app.services import AuthService
router = APIRouter(prefix="/panel/auth", tags=["panel-auth"])
@router.post("/login", response_model=AdminPanelWebSessionResponse)
def panel_login(
payload: AdminLoginRequest,
request: Request,
response: Response,
settings: AdminSettings = Depends(get_settings),
auth_service: AuthService = Depends(get_auth_service),
):
ip_address, user_agent = _extract_request_metadata(request)
session = auth_service.login(
email=payload.email,
password=payload.password,
ip_address=ip_address,
user_agent=user_agent,
)
if session is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Credenciais administrativas invalidas.",
)
set_panel_auth_cookies(response, session, settings)
return _build_panel_session_response(
session=session,
message="Sessao administrativa web iniciada.",
redirect_to=_build_prefixed_path(settings.admin_api_prefix, "/panel/admin"),
)
@router.post("/refresh", response_model=AdminPanelWebSessionResponse)
def panel_refresh(
request: Request,
response: Response,
settings: AdminSettings = Depends(get_settings),
auth_service: AuthService = Depends(get_auth_service),
):
refresh_token = get_panel_refresh_cookie(request)
if not refresh_token:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Sessao administrativa web sem refresh token.",
)
ip_address, user_agent = _extract_request_metadata(request)
session = auth_service.refresh_session(
refresh_token=refresh_token,
ip_address=ip_address,
user_agent=user_agent,
)
if session is None:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Sessao administrativa web invalida para refresh.",
)
set_panel_auth_cookies(response, session, settings)
return _build_panel_session_response(
session=session,
message="Sessao administrativa web renovada.",
)
@router.get("/session", response_model=AdminPanelWebSessionResponse)
def panel_session(
current_context: AuthenticatedStaffContext = Depends(get_current_panel_staff_context),
settings: AdminSettings = Depends(get_settings),
):
return AdminPanelWebSessionResponse(
service="orquestrador-admin",
status="ok",
message="Sessao administrativa web ativa.",
session_id=current_context.session_id,
expires_in_seconds=settings.admin_auth_access_token_ttl_minutes * 60,
staff_account=AdminAuthenticatedStaffResponse(**current_context.principal.model_dump()),
redirect_to=None,
)
@router.post("/logout", response_model=AdminPanelLogoutResponse)
def panel_logout(
request: Request,
response: Response,
settings: AdminSettings = Depends(get_settings),
auth_service: AuthService = Depends(get_auth_service),
):
ip_address, user_agent = _extract_request_metadata(request)
session_id: int | None = None
access_token = get_panel_access_cookie(request)
if access_token:
try:
current_context = auth_service.get_authenticated_context(access_token)
except ValueError:
current_context = None
if current_context is not None:
auth_service.logout(
current_context.session_id,
actor_staff_account_id=current_context.principal.id,
ip_address=ip_address,
user_agent=user_agent,
)
session_id = current_context.session_id
refresh_token = get_panel_refresh_cookie(request)
if session_id is None and refresh_token:
session_id = auth_service.logout_by_refresh_token(
refresh_token,
ip_address=ip_address,
user_agent=user_agent,
)
clear_panel_auth_cookies(response, settings)
return AdminPanelLogoutResponse(
service="orquestrador-admin",
status="ok",
message="Sessao administrativa web encerrada.",
session_id=session_id,
redirect_to=_build_prefixed_path(settings.admin_api_prefix, "/login"),
)
def _build_panel_session_response(
session: AdminAuthenticatedSession,
*,
message: str,
redirect_to: str | None = None,
) -> AdminPanelWebSessionResponse:
return AdminPanelWebSessionResponse(
service="orquestrador-admin",
status="ok",
message=message,
session_id=session.session_id,
expires_in_seconds=session.expires_in_seconds,
staff_account=AdminAuthenticatedStaffResponse(**session.principal.model_dump()),
redirect_to=redirect_to,
)
def _extract_request_metadata(request: Request) -> tuple[str | None, str | None]:
ip_address = request.client.host if request.client else None
user_agent = request.headers.get("user-agent")
return ip_address, user_agent
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}"

@ -0,0 +1,170 @@
from fastapi import APIRouter, Depends
from admin_app.api.dependencies import get_settings, require_panel_admin_permission
from admin_app.api.schemas import (
AdminToolContractsResponse,
AdminToolDraftListResponse,
AdminToolManagementActionResponse,
AdminToolOverviewResponse,
AdminToolPublicationListResponse,
AdminToolReviewQueueResponse,
)
from admin_app.core import AdminSettings, AuthenticatedStaffPrincipal
from admin_app.services import ToolManagementService
from shared.contracts import AdminPermission
router = APIRouter(prefix="/panel/tools", tags=["panel-tools"])
def _build_service(settings: AdminSettings) -> ToolManagementService:
return ToolManagementService(settings)
@router.get(
"/overview",
response_model=AdminToolOverviewResponse,
)
def panel_tools_overview(
settings: AdminSettings = Depends(get_settings),
_: AuthenticatedStaffPrincipal = Depends(
require_panel_admin_permission(AdminPermission.MANAGE_TOOL_DRAFTS)
),
):
service = _build_service(settings)
payload = service.build_overview_payload()
return AdminToolOverviewResponse(
service="orquestrador-admin",
mode=payload["mode"],
metrics=payload["metrics"],
workflow=payload["workflow"],
actions=_build_panel_actions(settings),
next_steps=payload["next_steps"],
)
@router.get(
"/contracts",
response_model=AdminToolContractsResponse,
)
def panel_tool_contracts(
settings: AdminSettings = Depends(get_settings),
_: AuthenticatedStaffPrincipal = Depends(
require_panel_admin_permission(AdminPermission.MANAGE_TOOL_DRAFTS)
),
):
service = _build_service(settings)
payload = service.build_contracts_payload()
return AdminToolContractsResponse(
service="orquestrador-admin",
publication_source_service=payload["publication_source_service"],
publication_target_service=payload["publication_target_service"],
lifecycle_statuses=payload["lifecycle_statuses"],
parameter_types=payload["parameter_types"],
publication_fields=payload["publication_fields"],
published_tool_fields=payload["published_tool_fields"],
)
@router.get(
"/drafts",
response_model=AdminToolDraftListResponse,
)
def panel_tool_drafts(
settings: AdminSettings = Depends(get_settings),
_: AuthenticatedStaffPrincipal = Depends(
require_panel_admin_permission(AdminPermission.MANAGE_TOOL_DRAFTS)
),
):
service = _build_service(settings)
payload = service.build_drafts_payload()
return AdminToolDraftListResponse(
service="orquestrador-admin",
storage_status=payload["storage_status"],
message=payload["message"],
drafts=payload["drafts"],
supported_statuses=payload["supported_statuses"],
)
@router.get(
"/review-queue",
response_model=AdminToolReviewQueueResponse,
)
def panel_tool_review_queue(
settings: AdminSettings = Depends(get_settings),
_: AuthenticatedStaffPrincipal = Depends(
require_panel_admin_permission(AdminPermission.REVIEW_TOOL_GENERATIONS)
),
):
service = _build_service(settings)
payload = service.build_review_queue_payload()
return AdminToolReviewQueueResponse(
service="orquestrador-admin",
queue_mode=payload["queue_mode"],
message=payload["message"],
items=payload["items"],
supported_statuses=payload["supported_statuses"],
)
@router.get(
"/publications",
response_model=AdminToolPublicationListResponse,
)
def panel_tool_publications(
settings: AdminSettings = Depends(get_settings),
_: AuthenticatedStaffPrincipal = Depends(
require_panel_admin_permission(AdminPermission.PUBLISH_TOOLS)
),
):
service = _build_service(settings)
payload = service.build_publications_payload()
return AdminToolPublicationListResponse(
service="orquestrador-admin",
source=payload["source"],
target_service=payload["target_service"],
publications=payload["publications"],
)
def _build_panel_actions(settings: AdminSettings) -> list[AdminToolManagementActionResponse]:
return [
AdminToolManagementActionResponse(
key="overview",
label="Overview web de tools",
href=_build_prefixed_path(settings.admin_api_prefix, "/panel/tools/overview"),
required_permission=AdminPermission.MANAGE_TOOL_DRAFTS,
description="Snapshot do dominio de tools pronto para leitura no painel.",
),
AdminToolManagementActionResponse(
key="contracts",
label="Contratos web de tools",
href=_build_prefixed_path(settings.admin_api_prefix, "/panel/tools/contracts"),
required_permission=AdminPermission.MANAGE_TOOL_DRAFTS,
description="Base contratual para a tela de revisao e aprovacao.",
),
AdminToolManagementActionResponse(
key="review_queue",
label="Fila web de revisao",
href=_build_prefixed_path(settings.admin_api_prefix, "/panel/tools/review-queue"),
required_permission=AdminPermission.REVIEW_TOOL_GENERATIONS,
description="Leitura da fila de revisao sob a sessao web do painel.",
),
AdminToolManagementActionResponse(
key="publications",
label="Publicacoes web",
href=_build_prefixed_path(settings.admin_api_prefix, "/panel/tools/publications"),
required_permission=AdminPermission.PUBLISH_TOOLS,
description="Catalogo de tools ativas e prontas para ativacao no produto.",
),
]
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}"

@ -1,18 +1,29 @@
from fastapi import APIRouter, Depends
from fastapi import APIRouter, Depends, Request
from fastapi.responses import RedirectResponse, Response
from admin_app.api.dependencies import (
get_current_staff_permissions,
get_security_service,
get_settings,
require_admin_permission,
)
from admin_app.api.panel_session import (
PANEL_ACCESS_COOKIE_NAME,
PANEL_COOKIE_SAMESITE,
PANEL_REFRESH_COOKIE_NAME,
build_panel_cookie_path,
should_use_secure_cookies,
)
from admin_app.api.schemas import (
AdminCapabilityResponse,
AdminCurrentAccessResponse,
AdminHealthResponse,
AdminRootResponse,
AdminSystemConfigurationResponse,
AdminSystemInfoResponse,
AdminSystemRuntimeConfigurationResponse,
AdminSystemSecurityConfigurationResponse,
)
from admin_app.core import AuthenticatedStaffPrincipal
from admin_app.core import AdminSecurityService, AuthenticatedStaffPrincipal
from admin_app.core.settings import AdminSettings
from admin_app.services.system_service import SystemService
from shared.contracts import AdminPermission
@ -20,18 +31,29 @@ from shared.contracts import AdminPermission
router = APIRouter(tags=["system"])
def _build_service(settings: AdminSettings) -> SystemService:
return SystemService(settings=settings)
def _build_service(
settings: AdminSettings,
security_service: AdminSecurityService,
) -> SystemService:
return SystemService(settings=settings, security_service=security_service)
@router.get("/", response_model=AdminRootResponse)
def root(settings: AdminSettings = Depends(get_settings)):
return _build_service(settings).build_root_payload()
@router.get("/", response_model=None)
def root(
request: Request,
settings: AdminSettings = Depends(get_settings),
) -> Response | dict:
if "text/html" in request.headers.get("accept", ""):
return RedirectResponse(
url=_build_prefixed_path(settings.admin_api_prefix, "/login"),
status_code=302,
)
return SystemService(settings=settings).build_root_payload()
@router.get("/health", response_model=AdminHealthResponse)
def health_check(settings: AdminSettings = Depends(get_settings)):
return _build_service(settings).build_health_payload()
return SystemService(settings=settings).build_health_payload()
@router.get(
@ -40,11 +62,12 @@ def health_check(settings: AdminSettings = Depends(get_settings)):
)
def system_info(
settings: AdminSettings = Depends(get_settings),
security_service: AdminSecurityService = Depends(get_security_service),
_: AuthenticatedStaffPrincipal = Depends(
require_admin_permission(AdminPermission.VIEW_SYSTEM)
),
):
return _build_service(settings).build_system_info_payload()
return _build_service(settings, security_service).build_system_info_payload()
@router.get(
@ -85,3 +108,85 @@ def admin_capabilities(
allowed=True,
role=current_staff.role,
)
@router.get(
"/system/configuration",
response_model=AdminSystemConfigurationResponse,
)
def system_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)
runtime_payload = _build_runtime_configuration_payload(service, settings)
return AdminSystemConfigurationResponse(
service="orquestrador-admin",
runtime=runtime_payload,
security=service.build_security_configuration_payload(),
sources=service.build_configuration_sources_payload(),
)
@router.get(
"/system/configuration/runtime",
response_model=AdminSystemRuntimeConfigurationResponse,
)
def system_runtime_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 AdminSystemRuntimeConfigurationResponse(
service="orquestrador-admin",
runtime=_build_runtime_configuration_payload(service, settings),
)
@router.get(
"/system/configuration/security",
response_model=AdminSystemSecurityConfigurationResponse,
)
def system_security_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 AdminSystemSecurityConfigurationResponse(
service="orquestrador-admin",
security=service.build_security_configuration_payload(),
)
def _build_runtime_configuration_payload(
service: SystemService,
settings: AdminSettings,
) -> dict:
runtime_payload = service.build_runtime_configuration_payload()
runtime_payload["panel_session"] = {
"access_cookie_name": PANEL_ACCESS_COOKIE_NAME,
"refresh_cookie_name": PANEL_REFRESH_COOKIE_NAME,
"cookie_path": build_panel_cookie_path(settings),
"same_site": PANEL_COOKIE_SAMESITE,
"secure_cookies": should_use_secure_cookies(settings),
}
return runtime_payload
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}"

@ -0,0 +1,177 @@
from fastapi import APIRouter, Depends
from admin_app.api.dependencies import get_settings, require_admin_permission
from admin_app.api.schemas import (
AdminToolContractsResponse,
AdminToolDraftListResponse,
AdminToolManagementActionResponse,
AdminToolOverviewResponse,
AdminToolPublicationListResponse,
AdminToolReviewQueueResponse,
)
from admin_app.core import AdminSettings, AuthenticatedStaffPrincipal
from admin_app.services import ToolManagementService
from shared.contracts import AdminPermission
router = APIRouter(prefix="/tools", tags=["tools"])
def _build_service(settings: AdminSettings) -> ToolManagementService:
return ToolManagementService(settings)
@router.get(
"/overview",
response_model=AdminToolOverviewResponse,
)
def tools_overview(
settings: AdminSettings = Depends(get_settings),
_: AuthenticatedStaffPrincipal = Depends(
require_admin_permission(AdminPermission.MANAGE_TOOL_DRAFTS)
),
):
service = _build_service(settings)
payload = service.build_overview_payload()
return AdminToolOverviewResponse(
service="orquestrador-admin",
mode=payload["mode"],
metrics=payload["metrics"],
workflow=payload["workflow"],
actions=_build_actions(settings),
next_steps=payload["next_steps"],
)
@router.get(
"/contracts",
response_model=AdminToolContractsResponse,
)
def tool_contracts(
settings: AdminSettings = Depends(get_settings),
_: AuthenticatedStaffPrincipal = Depends(
require_admin_permission(AdminPermission.MANAGE_TOOL_DRAFTS)
),
):
service = _build_service(settings)
payload = service.build_contracts_payload()
return AdminToolContractsResponse(
service="orquestrador-admin",
publication_source_service=payload["publication_source_service"],
publication_target_service=payload["publication_target_service"],
lifecycle_statuses=payload["lifecycle_statuses"],
parameter_types=payload["parameter_types"],
publication_fields=payload["publication_fields"],
published_tool_fields=payload["published_tool_fields"],
)
@router.get(
"/drafts",
response_model=AdminToolDraftListResponse,
)
def tool_drafts(
settings: AdminSettings = Depends(get_settings),
_: AuthenticatedStaffPrincipal = Depends(
require_admin_permission(AdminPermission.MANAGE_TOOL_DRAFTS)
),
):
service = _build_service(settings)
payload = service.build_drafts_payload()
return AdminToolDraftListResponse(
service="orquestrador-admin",
storage_status=payload["storage_status"],
message=payload["message"],
drafts=payload["drafts"],
supported_statuses=payload["supported_statuses"],
)
@router.get(
"/review-queue",
response_model=AdminToolReviewQueueResponse,
)
def tool_review_queue(
settings: AdminSettings = Depends(get_settings),
_: AuthenticatedStaffPrincipal = Depends(
require_admin_permission(AdminPermission.REVIEW_TOOL_GENERATIONS)
),
):
service = _build_service(settings)
payload = service.build_review_queue_payload()
return AdminToolReviewQueueResponse(
service="orquestrador-admin",
queue_mode=payload["queue_mode"],
message=payload["message"],
items=payload["items"],
supported_statuses=payload["supported_statuses"],
)
@router.get(
"/publications",
response_model=AdminToolPublicationListResponse,
)
def tool_publications(
settings: AdminSettings = Depends(get_settings),
_: AuthenticatedStaffPrincipal = Depends(
require_admin_permission(AdminPermission.PUBLISH_TOOLS)
),
):
service = _build_service(settings)
payload = service.build_publications_payload()
return AdminToolPublicationListResponse(
service="orquestrador-admin",
source=payload["source"],
target_service=payload["target_service"],
publications=payload["publications"],
)
def _build_actions(settings: AdminSettings) -> list[AdminToolManagementActionResponse]:
return [
AdminToolManagementActionResponse(
key="overview",
label="Overview de tools",
href=_build_prefixed_path(settings.admin_api_prefix, "/tools/overview"),
required_permission=AdminPermission.MANAGE_TOOL_DRAFTS,
description="Snapshot inicial da governanca de tools no admin.",
),
AdminToolManagementActionResponse(
key="contracts",
label="Contratos compartilhados",
href=_build_prefixed_path(settings.admin_api_prefix, "/tools/contracts"),
required_permission=AdminPermission.MANAGE_TOOL_DRAFTS,
description="Enumera lifecycle, tipos de parametro e campos de publicacao.",
),
AdminToolManagementActionResponse(
key="drafts",
label="Fila de drafts",
href=_build_prefixed_path(settings.admin_api_prefix, "/tools/drafts"),
required_permission=AdminPermission.MANAGE_TOOL_DRAFTS,
description="Base do cadastro de novas tools e estados vazios da fase atual.",
),
AdminToolManagementActionResponse(
key="review_queue",
label="Fila de revisao",
href=_build_prefixed_path(settings.admin_api_prefix, "/tools/review-queue"),
required_permission=AdminPermission.REVIEW_TOOL_GENERATIONS,
description="Superficie para validacao, revisao tecnica e aprovacao humana.",
),
AdminToolManagementActionResponse(
key="publications",
label="Catalogo de publicacoes",
href=_build_prefixed_path(settings.admin_api_prefix, "/tools/publications"),
required_permission=AdminPermission.PUBLISH_TOOLS,
description="Catalogo bootstrap de tools ativas voltadas ao runtime de produto.",
),
]
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}"

@ -2,7 +2,8 @@ from datetime import datetime
from pydantic import BaseModel, Field, field_validator
from shared.contracts import StaffRole
from admin_app.core import AdminCredentialStrategy
from shared.contracts import AdminPermission, ServiceName, StaffRole, ToolLifecycleStatus, ToolParameterType
class AdminRootResponse(BaseModel):
@ -67,6 +68,59 @@ class AdminAuditListResponse(BaseModel):
events: list[AdminAuditEntryResponse]
class AdminRuntimeApplicationConfigurationResponse(BaseModel):
app_name: str
environment: str
version: str
api_prefix: str
debug: bool
class AdminRuntimeDatabaseConfigurationResponse(BaseModel):
host: str
port: int
name: str
cloud_sql_configured: bool
class AdminPanelSessionConfigurationResponse(BaseModel):
access_cookie_name: str
refresh_cookie_name: str
cookie_path: str
same_site: str
secure_cookies: bool
class AdminSystemRuntimeConfigurationPayload(BaseModel):
application: AdminRuntimeApplicationConfigurationResponse
database: AdminRuntimeDatabaseConfigurationResponse
panel_session: AdminPanelSessionConfigurationResponse
class AdminConfigurationSourceResponse(BaseModel):
key: str
source: str
mutable: bool
description: str
class AdminSystemRuntimeConfigurationResponse(BaseModel):
service: str
runtime: AdminSystemRuntimeConfigurationPayload
class AdminSystemSecurityConfigurationResponse(BaseModel):
service: str
security: AdminCredentialStrategy
class AdminSystemConfigurationResponse(BaseModel):
service: str
runtime: AdminSystemRuntimeConfigurationPayload
security: AdminCredentialStrategy
sources: list[AdminConfigurationSourceResponse]
class AdminLoginRequest(BaseModel):
email: str
password: str = Field(min_length=1)
@ -97,4 +151,127 @@ class AdminLogoutResponse(BaseModel):
service: str
status: str
message: str
session_id: int
session_id: int
class AdminPanelWebSessionResponse(BaseModel):
service: str
status: str
message: str
session_id: int
expires_in_seconds: int
staff_account: AdminAuthenticatedStaffResponse
redirect_to: str | None = None
class AdminPanelLogoutResponse(BaseModel):
service: str
status: str
message: str
session_id: int | None
redirect_to: str
class AdminToolManagementMetricResponse(BaseModel):
key: str
label: str
value: str
description: str
class AdminToolLifecycleStageResponse(BaseModel):
code: ToolLifecycleStatus
label: str
description: str
class AdminToolParameterTypeResponse(BaseModel):
code: ToolParameterType
label: str
description: str
class AdminToolManagementActionResponse(BaseModel):
key: str
label: str
href: str
required_permission: AdminPermission
description: str
class AdminToolOverviewResponse(BaseModel):
service: str
mode: str
metrics: list[AdminToolManagementMetricResponse]
workflow: list[AdminToolLifecycleStageResponse]
actions: list[AdminToolManagementActionResponse]
next_steps: list[str]
class AdminToolContractsResponse(BaseModel):
service: str
publication_source_service: ServiceName
publication_target_service: ServiceName
lifecycle_statuses: list[AdminToolLifecycleStageResponse]
parameter_types: list[AdminToolParameterTypeResponse]
publication_fields: list[str]
published_tool_fields: list[str]
class AdminToolDraftSummaryResponse(BaseModel):
draft_id: str
tool_name: str
display_name: str
status: ToolLifecycleStatus
summary: str
owner_name: str | None = None
updated_at: datetime | None = None
class AdminToolDraftListResponse(BaseModel):
service: str
storage_status: str
message: str
drafts: list[AdminToolDraftSummaryResponse]
supported_statuses: list[ToolLifecycleStatus]
class AdminToolReviewQueueEntryResponse(BaseModel):
entry_id: str
tool_name: str
display_name: str
status: ToolLifecycleStatus
gate: str
summary: str
owner_name: str | None = None
queued_at: datetime | None = None
class AdminToolReviewQueueResponse(BaseModel):
service: str
queue_mode: str
message: str
items: list[AdminToolReviewQueueEntryResponse]
supported_statuses: list[ToolLifecycleStatus]
class AdminToolPublicationSummaryResponse(BaseModel):
publication_id: str
tool_name: str
display_name: str
description: str
domain: str
version: int
status: ToolLifecycleStatus
parameter_count: int
implementation_module: str
implementation_callable: str
published_by: str | None = None
published_at: datetime | None = None
class AdminToolPublicationListResponse(BaseModel):
service: str
source: str
target_service: ServiceName
publications: list[AdminToolPublicationSummaryResponse]

@ -1,7 +1,9 @@
from fastapi import FastAPI
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
from admin_app.api.router import api_router
from admin_app.core.settings import AdminSettings, get_admin_settings
from admin_app.view import PANEL_STATIC_DIRECTORY, PANEL_STATIC_MOUNT_NAME, panel_router
# Fabrica explicita do runtime administrativo para facilitar testes e futura configuracao.
@ -13,5 +15,19 @@ def create_app(settings: AdminSettings | None = None) -> FastAPI:
debug=resolved_settings.admin_debug,
)
app.state.admin_settings = resolved_settings
app.mount(
_build_panel_static_path(resolved_settings.admin_api_prefix),
StaticFiles(directory=str(PANEL_STATIC_DIRECTORY)),
name=PANEL_STATIC_MOUNT_NAME,
)
app.include_router(api_router, prefix=resolved_settings.admin_api_prefix)
app.include_router(panel_router, prefix=resolved_settings.admin_api_prefix)
return app
def _build_panel_static_path(api_prefix: str) -> str:
normalized_prefix = api_prefix.rstrip("/")
if normalized_prefix:
return f"{normalized_prefix}/panel/assets"
return "/panel/assets"

@ -5,6 +5,7 @@ from admin_app.services.audit_service import (
)
from admin_app.services.auth_service import AuthService
from admin_app.services.system_service import SystemService
from admin_app.services.tool_management_service import ToolManagementService
__all__ = [
"AdminAuditEventType",
@ -12,4 +13,5 @@ __all__ = [
"AuditService",
"AuthService",
"SystemService",
"ToolManagementService",
]

@ -1,4 +1,4 @@
from datetime import datetime, timezone
from datetime import datetime, timezone
from admin_app.core import (
AdminAuthenticatedSession,
@ -104,6 +104,28 @@ class AuthService:
)
return True
def logout_by_refresh_token(
self,
refresh_token: str,
*,
ip_address: str | None,
user_agent: str | None,
) -> int | None:
token_hash = self.security_service.hash_refresh_token(refresh_token)
staff_session = self.session_repository.get_by_refresh_token_hash(token_hash)
if staff_session is None:
return None
account = self.account_repository.get_by_id(staff_session.staff_account_id)
actor_staff_account_id = account.id if account is not None and account.is_active else None
self.logout(
staff_session.id,
actor_staff_account_id=actor_staff_account_id,
ip_address=ip_address,
user_agent=user_agent,
)
return staff_session.id
def get_authenticated_context(self, access_token: str) -> AuthenticatedStaffContext:
claims = self.security_service.decode_access_token(access_token)
staff_session = self.session_repository.get_by_id(claims.sid)
@ -202,9 +224,17 @@ class AuthService:
)
@staticmethod
def _is_session_active(staff_session: StaffSession | None) -> bool:
def _normalize_datetime(value: datetime) -> datetime:
if value.tzinfo is None or value.tzinfo.utcoffset(value) is None:
return value.replace(tzinfo=timezone.utc)
return value.astimezone(timezone.utc)
@classmethod
def _is_session_active(cls, staff_session: StaffSession | None) -> bool:
if staff_session is None:
return False
if staff_session.revoked_at is not None:
return False
return staff_session.expires_at >= datetime.now(timezone.utc)
expires_at = cls._normalize_datetime(staff_session.expires_at)
return expires_at >= datetime.now(timezone.utc)

@ -1,9 +1,15 @@
from admin_app.core import AdminCredentialStrategy, AdminSecurityService
from admin_app.core.settings import AdminSettings
class SystemService:
def __init__(self, settings: AdminSettings):
def __init__(
self,
settings: AdminSettings,
security_service: AdminSecurityService | None = None,
):
self.settings = settings
self.security_service = security_service or AdminSecurityService(settings)
def build_root_payload(self) -> dict:
return {
@ -29,3 +35,51 @@ class SystemService:
"api_prefix": self.settings.admin_api_prefix,
"debug": self.settings.admin_debug,
}
def build_runtime_configuration_payload(self) -> dict:
return {
"application": {
"app_name": self.settings.admin_app_name,
"environment": self.settings.admin_environment,
"version": self.settings.admin_version,
"api_prefix": self.settings.admin_api_prefix,
"debug": self.settings.admin_debug,
},
"database": {
"host": self.settings.admin_db_host,
"port": self.settings.admin_db_port,
"name": self.settings.admin_db_name,
"cloud_sql_configured": bool(self.settings.admin_db_cloud_sql_connection_name),
},
}
def build_security_configuration_payload(self) -> AdminCredentialStrategy:
return self.security_service.build_credential_strategy()
def build_configuration_sources_payload(self) -> list[dict]:
return [
{
"key": "application",
"source": "env",
"mutable": False,
"description": "Metadados principais do admin runtime vindos de AdminSettings.",
},
{
"key": "database",
"source": "env",
"mutable": False,
"description": "Conexao administrativa derivada das variaveis de ambiente do servico.",
},
{
"key": "security",
"source": "env",
"mutable": False,
"description": "Politicas de senha, token e bootstrap lidas do runtime administrativo.",
},
{
"key": "panel_session",
"source": "runtime",
"mutable": False,
"description": "Cookies e sessao web do painel derivam da configuracao ativa do admin.",
},
]

@ -0,0 +1,305 @@
from __future__ import annotations
from dataclasses import dataclass
from datetime import UTC, datetime
from admin_app.core.settings import AdminSettings
from shared.contracts import ServiceName, ToolLifecycleStatus, ToolParameterType
@dataclass(frozen=True)
class BootstrapToolCatalogEntry:
tool_name: str
display_name: str
description: str
domain: str
parameter_count: int
_BOOTSTRAP_TOOL_CATALOG: tuple[BootstrapToolCatalogEntry, ...] = (
BootstrapToolCatalogEntry(
tool_name="consultar_estoque",
display_name="Consultar estoque",
description="Consulta veiculos disponiveis no estoque comercial.",
domain="vendas",
parameter_count=4,
),
BootstrapToolCatalogEntry(
tool_name="validar_cliente_venda",
display_name="Validar cliente para venda",
description="Avalia elegibilidade de credito para operacoes de venda.",
domain="vendas",
parameter_count=2,
),
BootstrapToolCatalogEntry(
tool_name="avaliar_veiculo_troca",
display_name="Avaliar veiculo de troca",
description="Estima o valor de entrada de um veiculo usado.",
domain="vendas",
parameter_count=3,
),
BootstrapToolCatalogEntry(
tool_name="agendar_revisao",
display_name="Agendar revisao",
description="Abre um agendamento de revisao ou manutencao.",
domain="revisao",
parameter_count=6,
),
BootstrapToolCatalogEntry(
tool_name="listar_agendamentos_revisao",
display_name="Listar agendamentos de revisao",
description="Consulta a fila de agendamentos de revisao do cliente.",
domain="revisao",
parameter_count=3,
),
BootstrapToolCatalogEntry(
tool_name="cancelar_agendamento_revisao",
display_name="Cancelar agendamento de revisao",
description="Cancela um agendamento existente por protocolo.",
domain="revisao",
parameter_count=2,
),
BootstrapToolCatalogEntry(
tool_name="editar_data_revisao",
display_name="Editar data de revisao",
description="Remarca uma revisao para um novo horario.",
domain="revisao",
parameter_count=2,
),
BootstrapToolCatalogEntry(
tool_name="realizar_pedido",
display_name="Realizar pedido",
description="Efetiva um pedido de compra com o veiculo escolhido.",
domain="vendas",
parameter_count=2,
),
BootstrapToolCatalogEntry(
tool_name="listar_pedidos",
display_name="Listar pedidos",
description="Consulta pedidos ja abertos pelo cliente.",
domain="vendas",
parameter_count=3,
),
BootstrapToolCatalogEntry(
tool_name="cancelar_pedido",
display_name="Cancelar pedido",
description="Cancela um pedido existente com motivo registrado.",
domain="vendas",
parameter_count=2,
),
BootstrapToolCatalogEntry(
tool_name="consultar_frota_aluguel",
display_name="Consultar frota de aluguel",
description="Lista veiculos disponiveis para locacao.",
domain="locacao",
parameter_count=6,
),
BootstrapToolCatalogEntry(
tool_name="abrir_locacao_aluguel",
display_name="Abrir locacao de aluguel",
description="Inicia um contrato de locacao de veiculo.",
domain="locacao",
parameter_count=7,
),
BootstrapToolCatalogEntry(
tool_name="registrar_devolucao_aluguel",
display_name="Registrar devolucao de aluguel",
description="Fecha uma locacao e devolve o veiculo para a frota.",
domain="locacao",
parameter_count=4,
),
BootstrapToolCatalogEntry(
tool_name="registrar_pagamento_aluguel",
display_name="Registrar pagamento de aluguel",
description="Registra comprovantes e pagamentos de contratos de locacao.",
domain="locacao",
parameter_count=7,
),
BootstrapToolCatalogEntry(
tool_name="limpar_contexto_conversa",
display_name="Limpar contexto de conversa",
description="Reinicia o contexto operacional atual do atendimento.",
domain="orquestracao",
parameter_count=1,
),
BootstrapToolCatalogEntry(
tool_name="continuar_proximo_pedido",
display_name="Continuar proximo pedido",
description="Retoma o proximo pedido pendente do fluxo atual.",
domain="orquestracao",
parameter_count=0,
),
BootstrapToolCatalogEntry(
tool_name="descartar_pedidos_pendentes",
display_name="Descartar pedidos pendentes",
description="Descarta apenas a fila pendente de pedidos do contexto.",
domain="orquestracao",
parameter_count=1,
),
BootstrapToolCatalogEntry(
tool_name="cancelar_fluxo_atual",
display_name="Cancelar fluxo atual",
description="Interrompe o fluxo corrente sem apagar todo o contexto.",
domain="orquestracao",
parameter_count=1,
),
)
_LIFECYCLE_DESCRIPTIONS = {
ToolLifecycleStatus.DRAFT: "Estado inicial de uma tool ainda em definicao.",
ToolLifecycleStatus.GENERATED: "Implementacao gerada e pronta para analise tecnica.",
ToolLifecycleStatus.VALIDATED: "Tool validada automaticamente com verificacoes basicas.",
ToolLifecycleStatus.APPROVED: "Versao revisada e aprovada para publicacao controlada.",
ToolLifecycleStatus.ACTIVE: "Tool publicada e apta a abastecer o runtime de produto.",
ToolLifecycleStatus.FAILED: "Falha registrada na geracao, validacao ou ativacao.",
ToolLifecycleStatus.ARCHIVED: "Versao retirada de circulacao e mantida apenas para historico.",
}
_PARAMETER_TYPE_DESCRIPTIONS = {
ToolParameterType.STRING: "Texto livre, codigos e identificadores.",
ToolParameterType.INTEGER: "Valores inteiros para limites, anos e contagens.",
ToolParameterType.NUMBER: "Valores numericos decimais, como preco e diaria.",
ToolParameterType.BOOLEAN: "Marcadores verdadeiro ou falso para decisoes operacionais.",
ToolParameterType.OBJECT: "Estruturas compostas para payloads complexos.",
ToolParameterType.ARRAY: "Colecoes ordenadas de valores.",
}
class ToolManagementService:
def __init__(self, settings: AdminSettings):
self.settings = settings
def build_overview_payload(self) -> dict:
catalog = self.list_publication_catalog()
return {
"mode": "bootstrap_catalog",
"metrics": [
{
"key": "active_catalog",
"label": "Tools mapeadas",
"value": str(len(catalog)),
"description": "Catalogo bootstrap refletindo a base de tools conhecida no monorepo.",
},
{
"key": "lifecycle_stages",
"label": "Etapas de lifecycle",
"value": str(len(ToolLifecycleStatus)),
"description": "Estados compartilhados entre governanca administrativa e publicacao.",
},
{
"key": "parameter_types",
"label": "Tipos de parametro",
"value": str(len(ToolParameterType)),
"description": "Tipos aceitos pelo contrato inicial de publicacao de tools.",
},
{
"key": "draft_persistence",
"label": "Persistencia de drafts",
"value": "pendente",
"description": "A fase atual entrega as superficies e o contrato; entidades de draft ainda nao existem.",
},
],
"workflow": self.build_lifecycle_payload(),
"next_steps": [
"Criar entidades administrativas para ToolDraft, ToolValidationRun e ToolPublication.",
"Ligar o formulario de cadastro de novas tools a uma persistencia propria do admin.",
"Abrir filas de revisao, aprovacao e ativacao com auditoria ponta a ponta.",
],
}
def build_contracts_payload(self) -> dict:
return {
"publication_source_service": ServiceName.ADMIN,
"publication_target_service": ServiceName.PRODUCT,
"lifecycle_statuses": self.build_lifecycle_payload(),
"parameter_types": [
{
"code": parameter_type,
"label": parameter_type.value.upper(),
"description": _PARAMETER_TYPE_DESCRIPTIONS[parameter_type],
}
for parameter_type in ToolParameterType
],
"publication_fields": [
"source_service",
"target_service",
"publication_id",
"published_tool",
"emitted_at",
],
"published_tool_fields": [
"tool_name",
"display_name",
"description",
"version",
"status",
"parameters",
"implementation_module",
"implementation_callable",
"checksum",
"published_at",
"published_by",
],
}
def build_drafts_payload(self) -> dict:
return {
"storage_status": "pending_persistence",
"message": (
"As rotas de gestao de tools ja existem, mas a persistencia de ToolDraft ainda sera criada nas proximas etapas."
),
"drafts": [],
"supported_statuses": [ToolLifecycleStatus.DRAFT],
}
def build_review_queue_payload(self) -> dict:
return {
"queue_mode": "bootstrap_empty_state",
"message": (
"A fila de revisao ainda opera em estado vazio ate a criacao das entidades de geracao e validacao."
),
"items": [],
"supported_statuses": [
ToolLifecycleStatus.GENERATED,
ToolLifecycleStatus.VALIDATED,
ToolLifecycleStatus.APPROVED,
ToolLifecycleStatus.FAILED,
],
}
def build_publications_payload(self) -> dict:
return {
"source": "bootstrap_catalog",
"target_service": ServiceName.PRODUCT,
"publications": self.list_publication_catalog(),
}
def build_lifecycle_payload(self) -> list[dict]:
return [
{
"code": status,
"label": status.value.replace("_", " ").title(),
"description": _LIFECYCLE_DESCRIPTIONS[status],
}
for status in ToolLifecycleStatus
]
def list_publication_catalog(self) -> list[dict]:
published_at = datetime.now(UTC)
return [
{
"publication_id": f"bootstrap::{entry.tool_name}::v1",
"tool_name": entry.tool_name,
"display_name": entry.display_name,
"description": entry.description,
"domain": entry.domain,
"version": 1,
"status": ToolLifecycleStatus.ACTIVE,
"parameter_count": entry.parameter_count,
"implementation_module": "app.services.tools.handlers",
"implementation_callable": entry.tool_name,
"published_by": "bootstrap_catalog",
"published_at": published_at,
}
for entry in _BOOTSTRAP_TOOL_CATALOG
]

@ -0,0 +1,9 @@
from admin_app.view.assets import PANEL_STATIC_DIRECTORY, PANEL_STATIC_MOUNT_NAME
from admin_app.view.router import panel_router
__all__ = [
"PANEL_STATIC_DIRECTORY",
"PANEL_STATIC_MOUNT_NAME",
"panel_router",
]

@ -0,0 +1,4 @@
from pathlib import Path
PANEL_STATIC_MOUNT_NAME = "admin_panel_assets"
PANEL_STATIC_DIRECTORY = Path(__file__).resolve().parent / "static"

@ -0,0 +1,725 @@
from html import escape
from admin_app.view.view_models import (
AdminLoginPageView,
AdminPanelHomeView,
AdminPanelMetric,
AdminPanelModuleCard,
AdminPanelNavigationItem,
AdminPanelQuickAction,
AdminPanelRoadmapItem,
AdminPanelSurfaceLink,
AdminToolReviewPageView,
AdminToolReviewWorkflowStep,
)
BOOTSTRAP_CSS_HREF = "https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
BOOTSTRAP_JS_HREF = "https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
_BADGE_CLASS_MAP = {
"success": "bg-success-subtle text-success-emphasis border border-success-subtle",
"warning": "bg-warning-subtle text-warning-emphasis border border-warning-subtle",
"info": "bg-info-subtle text-info-emphasis border border-info-subtle",
"primary": "bg-primary-subtle text-primary-emphasis border border-primary-subtle",
"secondary": "bg-secondary-subtle text-secondary-emphasis border border-secondary-subtle",
"dark": "bg-dark-subtle text-dark-emphasis border border-dark-subtle",
}
def render_panel_home(
view: AdminPanelHomeView,
*,
css_href: str,
js_href: str,
) -> str:
navigation_markup = _render_navigation(view.navigation)
quick_actions_markup = _render_quick_actions(view.quick_actions)
metrics_markup = _render_metrics(view.metrics)
modules_markup = _render_modules(view.modules)
surface_links_markup = _render_surface_links(view.surface_links)
roadmap_markup = _render_roadmap(view.roadmap)
panel_title = escape(view.panel_title)
app_name = escape(view.app_name)
panel_subtitle = escape(view.panel_subtitle)
environment = escape(view.environment)
version = escape(view.version)
api_prefix = escape(view.api_prefix)
service = escape(view.service)
release_label = escape(view.release_label)
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>{panel_title}</title>
<meta name="description" content="{panel_subtitle}">
<link rel="stylesheet" href="{BOOTSTRAP_CSS_HREF}">
<link rel="stylesheet" href="{escape(css_href, quote=True)}">
</head>
<body class="admin-view-body">
<div class="container-xxl py-4 py-lg-5">
<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 p-lg-4">
<span class="badge rounded-pill text-bg-dark px-3 py-2">{release_label}</span>
<h1 class="display-6 fw-semibold mt-3 mb-2">{panel_title}</h1>
<p class="text-secondary mb-4">{panel_subtitle}</p>
<nav class="d-grid gap-2">
{navigation_markup}
</nav>
<div id="runtime" class="admin-runtime-block rounded-4 p-3 mt-4">
<p class="text-uppercase small fw-semibold text-secondary mb-3">Runtime 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">{app_name}</strong>
</div>
<div class="d-flex justify-content-between gap-3">
<span class="text-secondary">Servico</span>
<strong class="text-end">{service}</strong>
</div>
<div class="d-flex justify-content-between gap-3">
<span class="text-secondary">Ambiente</span>
<strong class="text-end text-uppercase">{environment}</strong>
</div>
<div class="d-flex justify-content-between gap-3">
<span class="text-secondary">Versao</span>
<strong class="text-end">{version}</strong>
</div>
<div class="d-flex justify-content-between gap-3">
<span class="text-secondary">Prefixo API</span>
<strong class="text-end">{api_prefix}</strong>
</div>
</div>
</div>
</div>
</div>
</aside>
<section class="col-12 col-xl-8 col-xxl-9">
<div id="overview" 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 gap-2 mb-3">
<span class="badge rounded-pill bg-white text-dark border">Area interna protegida</span>
<span class="badge rounded-pill bg-dark-subtle text-dark-emphasis border border-dark-subtle">Minimal Bootstrap</span>
</div>
<div class="row g-4 align-items-end">
<div class="col-lg-7">
<h2 class="display-5 fw-semibold mb-3">Dashboard do administrador</h2>
<p class="lead text-secondary mb-0">
A home protegida organiza o trabalho do time interno por fluxo, com foco no que realmente importa depois do login.
</p>
</div>
<div class="col-lg-5">
<div class="d-grid gap-2 admin-quick-actions">
{quick_actions_markup}
</div>
</div>
</div>
</div>
</div>
<div class="row g-3 mb-4">
{metrics_markup}
</div>
<div class="row g-4">
<div class="col-12 col-xxl-7">
<div id="modules" class="card border-0 shadow-sm admin-surface-card h-100">
<div class="card-body p-4">
<div class="d-flex flex-wrap justify-content-between align-items-start gap-3 mb-4">
<div>
<p class="text-uppercase small fw-semibold text-secondary mb-2">Areas do sistema</p>
<h3 class="h2 fw-semibold mb-2">Onde o time interno opera</h3>
<p class="text-secondary mb-0">
A dashboard agora funciona como ponto de orientacao para entrar nas areas certas sem expor atalhos desnecessarios.
</p>
</div>
</div>
<div class="row g-3">
{modules_markup}
</div>
</div>
</div>
</div>
<div class="col-12 col-xxl-5">
<div id="surfaces" class="card border-0 shadow-sm admin-surface-card mb-4">
<div class="card-body p-4">
<p class="text-uppercase small fw-semibold text-secondary mb-2">Acessos disponiveis</p>
<h3 class="h3 fw-semibold mb-3">Entradas claras para as areas protegidas</h3>
<div class="list-group list-group-flush admin-link-list">
{surface_links_markup}
</div>
</div>
</div>
<div id="roadmap" class="card border-0 shadow-sm admin-surface-card">
<div class="card-body p-4">
<p class="text-uppercase small fw-semibold text-secondary mb-2">Fluxo recomendado</p>
<h3 class="h3 fw-semibold mb-3">Como navegar no painel</h3>
<div class="vstack gap-3">
{roadmap_markup}
</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_login_page(
view: AdminLoginPageView,
*,
css_href: str,
js_href: str,
) -> str:
security_markup = _render_text_list(view.security_highlights)
notes_markup = _render_text_list(view.integration_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-login-page">
<div class="container-xxl py-4 py-lg-5">
<div class="row justify-content-center">
<div class="col-12 col-xl-10">
<div class="row g-4 align-items-stretch">
<div class="col-12 col-lg-5">
<div class="card border-0 shadow-sm admin-login-card h-100">
<div class="card-body p-4 p-lg-5 d-flex flex-column justify-content-center">
<div class="mb-4">
<div class="d-flex flex-wrap gap-2 mb-3">
<span class="badge rounded-pill text-bg-dark">Acesso restrito</span>
<span class="badge rounded-pill bg-white text-dark border">{escape(view.app_name)}</span>
</div>
<p class="text-uppercase small fw-semibold text-secondary mb-2">Entrada do painel</p>
<h1 class="display-6 fw-semibold mb-3">{escape(view.title)}</h1>
<p class="text-secondary mb-0">{escape(view.subtitle)}</p>
</div>
<form class="admin-login-form" data-admin-login-form="true" data-auth-endpoint="{escape(view.auth_endpoint, quote=True)}" data-dashboard-href="{escape(view.dashboard_href, quote=True)}">
<div class="mb-3">
<label class="form-label fw-semibold" for="admin-login-email">Email administrativo</label>
<input class="form-control form-control-lg rounded-4" id="admin-login-email" name="email" type="email" placeholder="{escape(view.email_placeholder, quote=True)}" autocomplete="username" required>
</div>
<div class="mb-3">
<label class="form-label fw-semibold" for="admin-login-password">Senha</label>
<input class="form-control form-control-lg rounded-4" id="admin-login-password" name="password" type="password" placeholder="{escape(view.password_placeholder, quote=True)}" autocomplete="current-password" required>
</div>
<div class="small text-secondary rounded-4 admin-login-policy mb-4">
<strong>Politica atual:</strong> {escape(view.password_policy_label)}
</div>
<button class="btn btn-dark btn-lg rounded-pill w-100 d-inline-flex align-items-center justify-content-center gap-2" type="submit">
<span data-submit-label>Entrar no painel</span>
<span class="spinner-border spinner-border-sm d-none" data-submit-spinner aria-hidden="true"></span>
</button>
</form>
<div class="alert d-none mt-4 mb-0 rounded-4" id="admin-login-feedback" role="status"></div>
<p class="small text-secondary mt-4 mb-0">
O restante do sistema administrativo so fica disponivel depois da autenticacao do StaffAccount.
</p>
</div>
</div>
</div>
<div class="col-12 col-lg-7">
<div class="card border-0 shadow-sm admin-login-info-card h-100">
<div class="card-body p-4 p-lg-5 d-flex flex-column gap-4">
<div>
<div class="d-flex flex-wrap gap-2 mb-3">
<span class="badge rounded-pill bg-body-tertiary text-secondary border">Ambiente {escape(view.environment)}</span>
<span class="badge rounded-pill bg-body-tertiary text-secondary border">Versao {escape(view.version)}</span>
</div>
<h2 class="display-6 fw-semibold mb-3">Depois do login, o painel organiza o fluxo por voce</h2>
<p class="lead text-secondary mb-0">
Primeiro vem a dashboard administrativa protegida. A partir dela, o time acessa revisao, governanca e acompanhamento do ambiente sem atalhos confusos antes da autenticacao.
</p>
</div>
<div class="row g-3">
<div class="col-12 col-md-4">
<div class="admin-login-kpi rounded-4 p-3 h-100">
<p class="small text-uppercase fw-semibold text-secondary mb-2">Access token</p>
<div class="h4 fw-semibold mb-1">{escape(view.access_token_ttl_label)}</div>
<p class="small text-secondary mb-0">Janela curta para a sessao ativa.</p>
</div>
</div>
<div class="col-12 col-md-4">
<div class="admin-login-kpi rounded-4 p-3 h-100">
<p class="small text-uppercase fw-semibold text-secondary mb-2">Refresh token</p>
<div class="h4 fw-semibold mb-1">{escape(view.refresh_token_ttl_label)}</div>
<p class="small text-secondary mb-0">Continuidade controlada da sessao web.</p>
</div>
</div>
<div class="col-12 col-md-4">
<div class="admin-login-kpi rounded-4 p-3 h-100">
<p class="small text-uppercase fw-semibold text-secondary mb-2">Acesso</p>
<div class="h4 fw-semibold mb-1">Protegido</div>
<p class="small text-secondary mb-0">Liberado apenas apos autenticacao.</p>
</div>
</div>
</div>
<div class="row g-4 mt-0">
<div class="col-12 col-xl-6">
<div class="admin-login-note rounded-4 p-4 h-100">
<p class="text-uppercase small fw-semibold text-secondary mb-2">O que fica liberado</p>
<h3 class="h4 fw-semibold mb-3">Fluxo apos o login</h3>
<ul class="small text-secondary ps-3 mb-0">
{notes_markup}
</ul>
</div>
</div>
<div class="col-12 col-xl-6">
<div class="admin-login-note rounded-4 p-4 h-100">
<p class="text-uppercase small fw-semibold text-secondary mb-2">Base de seguranca</p>
<h3 class="h4 fw-semibold mb-3">Como a entrada esta protegida</h3>
<ul class="small text-secondary ps-3 mb-0">
{security_markup}
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script src="{BOOTSTRAP_JS_HREF}" defer></script>
<script src="{escape(js_href, quote=True)}" defer></script>
</body>
</html>
"""
def _render_navigation(items: tuple[AdminPanelNavigationItem, ...]) -> str:
links: list[str] = []
for item in items:
badge_markup = ""
if item.badge:
badge_markup = (
f'<span class="badge rounded-pill bg-body-tertiary text-secondary border">{escape(item.badge)}</span>'
)
active_class = " active shadow-sm" if item.is_active else ""
links.append(
f"""
<a class="btn btn-link text-decoration-none text-start rounded-4 px-3 py-3 admin-nav-link{active_class}" href="{escape(item.href, quote=True)}">
<div class="d-flex justify-content-between align-items-start gap-3">
<div>
<div class="fw-semibold text-dark">{escape(item.label)}</div>
<div class="small text-secondary mt-1">{escape(item.description)}</div>
</div>
{badge_markup}
</div>
</a>
""".strip()
)
return "\n".join(links)
def _render_quick_actions(items: tuple[AdminPanelQuickAction, ...]) -> str:
return "\n".join(
f'<a class="btn {escape(item.button_class)} btn-lg rounded-pill px-4" href="{escape(item.href, quote=True)}">{escape(item.label)}</a>'
for item in items
)
def _render_metrics(items: tuple[AdminPanelMetric, ...]) -> str:
cards: list[str] = []
for item in items:
cards.append(
f"""
<div class="col-12 col-md-6 col-xxl-3">
<div class="card border-0 shadow-sm h-100 admin-metric-card">
<div class="card-body p-4">
<p class="small text-uppercase fw-semibold text-secondary mb-3">{escape(item.label)}</p>
<div class="display-6 fw-semibold mb-2">{escape(item.value)}</div>
<p class="text-secondary mb-0">{escape(item.description)}</p>
</div>
</div>
</div>
""".strip()
)
return "\n".join(cards)
def _render_modules(items: tuple[AdminPanelModuleCard, ...]) -> str:
cards: list[str] = []
for item in items:
highlights = "".join(
f'<li class="mb-2">{escape(highlight)}</li>'
for highlight in item.highlights
)
cta_markup = '<span class="small fw-semibold text-secondary">Em preparacao</span>'
if item.href and item.cta_label and item.is_available:
cta_markup = (
f'<a class="btn btn-sm btn-outline-dark rounded-pill px-3" '
f'href="{escape(item.href, quote=True)}">{escape(item.cta_label)}</a>'
)
badge_classes = _BADGE_CLASS_MAP.get(item.status_variant, _BADGE_CLASS_MAP["secondary"])
cards.append(
f"""
<div class="col-12 col-md-6">
<article class="card border-0 h-100 admin-module-card">
<div class="card-body p-4 d-flex flex-column gap-3">
<div class="d-flex flex-wrap justify-content-between gap-3 align-items-start">
<div>
<p class="small text-uppercase fw-semibold text-secondary mb-2">{escape(item.eyebrow)}</p>
<h4 class="h5 fw-semibold mb-0">{escape(item.title)}</h4>
</div>
<span class="badge rounded-pill {badge_classes}">{escape(item.status_label)}</span>
</div>
<p class="text-secondary mb-0">{escape(item.description)}</p>
<ul class="small text-secondary ps-3 mb-0">
{highlights}
</ul>
<div class="pt-1 mt-auto">
{cta_markup}
</div>
</div>
</article>
</div>
""".strip()
)
return "\n".join(cards)
def _render_surface_links(items: tuple[AdminPanelSurfaceLink, ...]) -> str:
cards: list[str] = []
for item in items:
cards.append(
f"""
<a class="list-group-item list-group-item-action border-0 rounded-4 px-0 admin-surface-link" href="{escape(item.href, quote=True)}">
<div class="d-flex justify-content-between align-items-start gap-3">
<div>
<div class="small text-uppercase fw-semibold text-secondary mb-2">{escape(item.method)}</div>
<div class="fw-semibold text-dark">{escape(item.label)}</div>
<div class="small text-secondary mt-1">{escape(item.description)}</div>
</div>
<span class="badge rounded-pill bg-body-tertiary text-secondary border">Abrir</span>
</div>
</a>
""".strip()
)
return "\n".join(cards)
def _render_roadmap(items: tuple[AdminPanelRoadmapItem, ...]) -> str:
cards: list[str] = []
for item in items:
cards.append(
f"""
<div class="admin-roadmap-item rounded-4 p-3">
<div class="d-flex justify-content-between align-items-start gap-3">
<div>
<div class="small text-uppercase fw-semibold text-secondary mb-2">Etapa {escape(item.step)}</div>
<div class="fw-semibold text-dark">{escape(item.title)}</div>
<div class="small text-secondary mt-1">{escape(item.description)}</div>
</div>
<span class="badge rounded-pill bg-body-tertiary text-secondary border">{escape(item.status_label)}</span>
</div>
</div>
""".strip()
)
return "\n".join(cards)
def _render_text_list(items: tuple[str, ...]) -> str:
return "\n".join(
f"<li class=\"mb-2\">{escape(item)}</li>"
for item in items
)
def render_tool_review_page(
view: AdminToolReviewPageView,
*,
css_href: str,
js_href: str,
) -> str:
workflow_markup = _render_tool_review_workflow(view.workflow)
review_notes_markup = _render_text_list(view.review_notes)
approval_notes_markup = _render_text_list(view.approval_notes)
activation_notes_markup = _render_text_list(view.activation_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-tool-review-page">
<div class="container-xxl py-4 py-lg-5" data-admin-tool-review-board="true" data-overview-endpoint="{escape(view.overview_endpoint, quote=True)}" data-contracts-endpoint="{escape(view.contracts_endpoint, quote=True)}" data-review-queue-endpoint="{escape(view.review_queue_endpoint, quote=True)}" data-publications-endpoint="{escape(view.publications_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">Review hub</span>
<span class="badge rounded-pill bg-body-tertiary text-secondary border">Bootstrap UI</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">
<a class="btn btn-dark rounded-pill" href="{escape(view.dashboard_href, quote=True)}">Voltar ao dashboard</a>
<span class="badge rounded-pill bg-body-tertiary text-secondary border px-3 py-2">Acesso ja autenticado</span>
</div>
<div class="admin-runtime-block p-3 mb-3">
<p class="text-uppercase small fw-semibold text-secondary mb-3">Resumo do workspace</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">Atualizacao</span>
<strong class="text-end" data-tool-review-last-sync>Pendente</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">Revisao humana</p>
<h2 class="h5 fw-semibold mb-3">O que observar na fila</h2>
<ul class="small text-secondary ps-3 mb-0">
{review_notes_markup}
</ul>
</div>
<div class="admin-tool-review-note p-4">
<p class="text-uppercase small fw-semibold text-secondary mb-2">Ativacao controlada</p>
<h2 class="h5 fw-semibold mb-3">Como fechar a publicacao</h2>
<ul class="small text-secondary ps-3 mb-0">
{activation_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">Governanca de tools</span>
<span class="badge rounded-pill bg-success-subtle text-success-emphasis border border-success-subtle">Revisao e ativacao</span>
</div>
<h2 class="display-5 fw-semibold mb-3">Fluxo visual de aprovacao no painel</h2>
<p class="lead text-secondary mb-0">
Esta tela conecta a sessao web do painel aos snapshots administrativos de tools para que o time consiga revisar a fila, conferir contratos e acompanhar o catalogo ativo.
</p>
</div>
<div class="d-grid gap-2 admin-quick-actions">
<button class="btn btn-dark btn-lg rounded-pill px-4" type="button" data-admin-tool-refresh>
<span data-tool-refresh-label>Atualizar leitura</span>
<span class="spinner-border spinner-border-sm d-none" data-tool-refresh-spinner aria-hidden="true"></span>
</button>
<a class="btn btn-outline-dark btn-lg rounded-pill px-4" href="{escape(view.dashboard_href, quote=True)}">Voltar ao overview</a>
</div>
</div>
</div>
</div>
<div class="alert d-none rounded-4 mb-4" id="admin-tool-review-feedback" role="status"></div>
<div class="row g-3 mb-4">
<div class="col-12 col-md-4">
<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">Fila de revisao</p>
<div class="display-6 fw-semibold mb-2" data-tool-review-queue-count>0</div>
<p class="text-secondary mb-0">Items aguardando leitura tecnica ou aprovacao humana.</p>
</div>
</div>
</div>
<div class="col-12 col-md-4">
<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">Publicacoes ativas</p>
<div class="display-6 fw-semibold mb-2" data-tool-review-publication-count>0</div>
<p class="text-secondary mb-0">Catalogo publicado e pronto para abastecer o runtime de produto.</p>
</div>
</div>
</div>
<div class="col-12 col-md-4">
<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">Etapas do contrato</p>
<div class="display-6 fw-semibold mb-2" data-tool-review-lifecycle-count>{len(view.workflow)}</div>
<p class="text-secondary mb-0">Workflow compartilhado entre revisao, aprovacao e ativacao.</p>
</div>
</div>
</div>
</div>
<div class="card border-0 shadow-sm admin-surface-card mb-4">
<div class="card-body p-4">
<div class="d-flex flex-wrap justify-content-between align-items-start gap-3 mb-4">
<div>
<p class="text-uppercase small fw-semibold text-secondary mb-2">Pipeline visual</p>
<h3 class="h3 fw-semibold mb-2">Etapas que a tela acompanha</h3>
<p class="text-secondary mb-0">Os cards abaixo resumem o trajeto de uma tool desde a analise ate a ativacao no produto.</p>
</div>
</div>
<div class="row g-3">
{workflow_markup}
</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">
<div class="d-flex flex-wrap justify-content-between align-items-start gap-3 mb-3">
<div>
<p class="text-uppercase small fw-semibold text-secondary mb-2">Fila atual</p>
<h3 class="h3 fw-semibold mb-2">Revisao tecnica e aprovacao</h3>
<p class="text-secondary mb-0">A fila abaixo e lida da superficie web do painel e respeita o papel da sessao autenticada.</p>
</div>
<span class="badge rounded-pill bg-body-tertiary text-secondary border" data-tool-review-queue-mode>Bootstrap</span>
</div>
<div class="admin-tool-review-grid" data-tool-review-queue-list>
<div class="admin-tool-empty-state rounded-4 p-4">
<h4 class="h5 fw-semibold mb-2">Nenhum item carregado ainda</h4>
<p class="text-secondary mb-0">Clique em atualizar leitura para sincronizar a fila de revisao do painel.</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 d-flex flex-column gap-4">
<div>
<p class="text-uppercase small fw-semibold text-secondary mb-2">Checklist de aprovacao</p>
<h3 class="h3 fw-semibold mb-2">Playbook para a decisao humana</h3>
<p class="text-secondary mb-0">Aprovacao e ativacao continuam controladas pelo papel administrativo e pela leitura do contrato compartilhado.</p>
</div>
<div class="admin-tool-review-note p-4">
<p class="text-uppercase small fw-semibold text-secondary mb-2">Antes de aprovar</p>
<ul class="small text-secondary ps-3 mb-0">
{approval_notes_markup}
</ul>
</div>
<div>
<p class="text-uppercase small fw-semibold text-secondary mb-2">Lifecycle disponivel</p>
<div class="vstack gap-2" data-tool-contract-lifecycle>
<div class="small text-secondary">Aguardando leitura do contrato compartilhado...</div>
</div>
</div>
<div>
<p class="text-uppercase small fw-semibold text-secondary mb-2">Tipos de parametro</p>
<div class="d-flex flex-wrap gap-2" data-tool-parameter-types>
<span class="badge rounded-pill bg-body-tertiary text-secondary border">Aguardando</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="card border-0 shadow-sm admin-surface-card mt-4">
<div class="card-body p-4">
<div class="d-flex flex-wrap justify-content-between align-items-start gap-3 mb-3">
<div>
<p class="text-uppercase small fw-semibold text-secondary mb-2">Catalogo ativo</p>
<h3 class="h3 fw-semibold mb-2">Ativacao e superficie publicada</h3>
<p class="text-secondary mb-0">Quando a sessao tem permissao de publicacao, o painel tambem exibe o catalogo conhecido de tools ativas.</p>
</div>
<span class="badge rounded-pill bg-body-tertiary text-secondary border" data-tool-publication-source>Catalogo</span>
</div>
<div class="row g-3" data-tool-publication-list>
<div class="col-12">
<div class="admin-tool-empty-state rounded-4 p-4">
<h4 class="h5 fw-semibold mb-2">Catalogo ainda nao sincronizado</h4>
<p class="text-secondary mb-0">A leitura da ativacao aparece aqui assim que a sessao web carregar as publicacoes disponiveis.</p>
</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_tool_review_workflow(items: tuple[AdminToolReviewWorkflowStep, ...]) -> str:
cards: list[str] = []
for item in items:
badge_classes = _BADGE_CLASS_MAP.get(item.status_variant, _BADGE_CLASS_MAP["secondary"])
cards.append(
f"""
<div class="col-12 col-md-6 col-xxl-3">
<article class="admin-tool-workflow-card p-4 h-100">
<div class="d-flex flex-wrap justify-content-between align-items-start gap-3 mb-3">
<div>
<p class="small text-uppercase fw-semibold text-secondary mb-2">{escape(item.eyebrow)}</p>
<h4 class="h5 fw-semibold mb-0">{escape(item.title)}</h4>
</div>
<span class="badge rounded-pill {badge_classes}">{escape(item.status_label)}</span>
</div>
<p class="text-secondary mb-0">{escape(item.description)}</p>
</article>
</div>
""".strip()
)
return "\n".join(cards)

@ -0,0 +1,402 @@
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.view.assets import PANEL_STATIC_MOUNT_NAME
from admin_app.view.rendering import render_login_page, render_panel_home, render_tool_review_page
from admin_app.view.view_models import (
AdminLoginPageView,
AdminPanelHomeView,
AdminPanelMetric,
AdminPanelModuleCard,
AdminPanelNavigationItem,
AdminPanelQuickAction,
AdminPanelRoadmapItem,
AdminPanelSurfaceLink,
AdminToolReviewPageView,
AdminToolReviewWorkflowStep,
)
from shared.contracts import AdminPermission, StaffRole
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)
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/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))
def _build_home_view(request: Request, settings: AdminSettings) -> AdminPanelHomeView:
panel_href = str(request.url_for("panel_home"))
tool_review_view_href = str(request.url_for("admin_tool_review_view"))
system_configuration_href = _build_prefixed_path(settings.admin_api_prefix, "/system/configuration")
audit_href = _build_prefixed_path(settings.admin_api_prefix, "/audit/events")
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=(
AdminPanelNavigationItem(
label="Dashboard",
href=panel_href,
description="Entrada principal do ambiente interno.",
badge="Ativo",
is_active=True,
),
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="Revisar tools",
href=tool_review_view_href,
button_class="btn-dark",
),
AdminPanelQuickAction(
label="Ver areas",
href="#modules",
button_class="btn-outline-dark",
),
AdminPanelQuickAction(
label="Ver fluxo",
href="#workflow",
button_class="btn-outline-secondary",
),
),
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 viewer, staff e admin.",
),
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=(
AdminPanelModuleCard(
eyebrow="Fluxo principal",
title="Revisao de tools",
description="A principal 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",
),
),
AdminPanelModuleCard(
eyebrow="Seguranca",
title="Sessao administrativa",
description="Acesso ao painel protegido por StaffAccount, token assinado e refresh token rotacionado.",
status_label="Protegido",
status_variant="success",
highlights=(
"StaffAccount isolado do usuario final",
"Cookies httpOnly no navegador",
"Rotacao controlada da sessao web",
),
),
),
surface_links=(
AdminPanelSurfaceLink(
method="Acesso",
label="Dashboard administrativa",
href=panel_href,
description="Entrada principal do time interno depois do login.",
),
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="Abrir revisao de tools",
description="Use o hub de revisao para analisar fila, contrato e ativacao das tools.",
status_label="Principal",
),
AdminPanelRoadmapItem(
step="04",
title="Consultar runtime e auditoria",
description="Quando necessario, acompanhe configuracao e eventos do admin para suportar a decisao operacional.",
status_label="Suporte",
),
),
)
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")
session_endpoint = _build_prefixed_path(settings.admin_api_prefix, "/panel/auth/session")
logout_endpoint = _build_prefixed_path(settings.admin_api_prefix, "/panel/auth/logout")
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,
session_endpoint=session_endpoint,
logout_endpoint=logout_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_review_view(request: Request, settings: AdminSettings) -> AdminToolReviewPageView:
dashboard_href = str(request.url_for("panel_home"))
login_href = str(request.url_for("admin_login_view"))
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,
login_href=login_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 _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}"

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

@ -0,0 +1,261 @@
:root {
--admin-bg: #f6f1e8;
--admin-surface: rgba(255, 255, 255, 0.84);
--admin-surface-strong: rgba(255, 255, 255, 0.92);
--admin-ink: #20242f;
--admin-muted: #677084;
--admin-accent: #144d47;
--admin-accent-soft: rgba(20, 77, 71, 0.08);
--admin-line: rgba(32, 36, 47, 0.08);
--admin-shadow: 0 24px 60px rgba(56, 44, 23, 0.11);
}
body.admin-view-body {
min-height: 100vh;
color: var(--admin-ink);
background:
radial-gradient(circle at top left, rgba(20, 77, 71, 0.18), transparent 28%),
radial-gradient(circle at top right, rgba(193, 106, 51, 0.16), transparent 24%),
linear-gradient(180deg, #fbf7f1 0%, var(--admin-bg) 100%);
}
.admin-shell-card,
.admin-hero-card,
.admin-surface-card,
.admin-metric-card,
.admin-module-card,
.admin-login-card,
.admin-login-info-card {
background: var(--admin-surface);
backdrop-filter: blur(18px);
box-shadow: var(--admin-shadow);
}
.admin-shell-card,
.admin-hero-card,
.admin-surface-card,
.admin-metric-card,
.admin-login-card,
.admin-login-info-card {
border-radius: 1.75rem;
}
.admin-module-card,
.admin-roadmap-item,
.admin-runtime-block,
.admin-login-kpi,
.admin-login-note,
.admin-login-policy,
.admin-login-session-card {
border-radius: 1.35rem;
}
.admin-sidebar-sticky {
position: sticky;
top: 1.5rem;
}
.admin-runtime-block,
.admin-module-card,
.admin-roadmap-item,
.admin-surface-link,
.admin-login-kpi,
.admin-login-note,
.admin-login-policy,
.admin-login-session-card {
background: var(--admin-surface-strong);
border: 1px solid var(--admin-line);
}
.admin-hero-card,
.admin-login-info-card,
.admin-login-card {
position: relative;
overflow: hidden;
border: 1px solid rgba(20, 77, 71, 0.08);
}
.admin-hero-card::after,
.admin-login-info-card::after,
.admin-login-card::after {
content: "";
position: absolute;
inset: auto -6rem -8rem auto;
width: 16rem;
height: 16rem;
border-radius: 50%;
background: radial-gradient(circle, rgba(20, 77, 71, 0.17), transparent 72%);
}
.admin-nav-link {
background: rgba(255, 255, 255, 0.7);
border: 1px solid transparent;
}
.admin-nav-link:hover {
background: rgba(255, 255, 255, 0.94);
border-color: rgba(20, 77, 71, 0.1);
}
.admin-nav-link.active {
background: linear-gradient(135deg, #163f3a 0%, #215a53 100%);
color: #fff;
}
.admin-nav-link.active .text-dark,
.admin-nav-link.active .text-secondary,
.admin-nav-link.active .badge {
color: #fff !important;
}
.admin-nav-link.active .badge {
background: rgba(255, 255, 255, 0.14) !important;
border-color: rgba(255, 255, 255, 0.16) !important;
}
.admin-metric-card {
border: 1px solid rgba(20, 77, 71, 0.06);
}
.admin-module-card {
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.5);
}
.admin-surface-link {
padding: 1rem 1.1rem;
margin-bottom: 0.75rem;
}
.admin-surface-link:hover {
background: rgba(20, 77, 71, 0.05);
}
.admin-roadmap-item {
position: relative;
padding-left: 1.3rem !important;
}
.admin-roadmap-item::before {
content: "";
position: absolute;
top: 1rem;
bottom: 1rem;
left: 0;
width: 4px;
border-radius: 999px;
background: linear-gradient(180deg, rgba(20, 77, 71, 0.72), rgba(20, 77, 71, 0.18));
}
.admin-quick-actions .btn {
min-height: 3.25rem;
}
.admin-login-form .form-control {
border: 1px solid rgba(20, 77, 71, 0.12);
background: rgba(255, 255, 255, 0.9);
}
.admin-login-form .form-control:focus {
border-color: rgba(20, 77, 71, 0.32);
box-shadow: 0 0 0 0.25rem rgba(20, 77, 71, 0.12);
}
.admin-login-policy,
.admin-login-kpi,
.admin-login-note,
.admin-login-session-card {
position: relative;
z-index: 1;
}
#admin-login-feedback {
position: relative;
z-index: 1;
}
[data-panel-ready="true"] .admin-hero-card,
[data-panel-ready="true"] .admin-shell-card,
[data-panel-ready="true"] .admin-surface-card,
[data-panel-ready="true"] .admin-metric-card,
[data-panel-ready="true"] .admin-login-card,
[data-panel-ready="true"] .admin-login-info-card,
[data-panel-ready="true"] .admin-login-session-card {
animation: admin-fade-up 520ms ease both;
}
[data-panel-ready="true"] .admin-metric-card:nth-child(2),
[data-panel-ready="true"] .admin-surface-card:nth-of-type(2),
[data-panel-ready="true"] .admin-login-info-card {
animation-delay: 120ms;
}
@keyframes admin-fade-up {
from {
opacity: 0;
transform: translateY(14px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (max-width: 1199px) {
.admin-sidebar-sticky {
position: static;
}
}
@media (max-width: 767px) {
.admin-shell-card,
.admin-hero-card,
.admin-surface-card,
.admin-metric-card,
.admin-login-card,
.admin-login-info-card {
border-radius: 1.4rem;
}
.admin-module-card,
.admin-roadmap-item,
.admin-runtime-block,
.admin-login-kpi,
.admin-login-note,
.admin-login-policy,
.admin-login-session-card {
border-radius: 1.1rem;
}
}
.admin-tool-review-note,
.admin-tool-workflow-card,
.admin-tool-review-card,
.admin-tool-publication-card,
.admin-tool-inline-note,
.admin-tool-empty-state {
background: var(--admin-surface-strong);
border: 1px solid var(--admin-line);
border-radius: 1.35rem;
}
.admin-tool-workflow-card,
.admin-tool-review-card,
.admin-tool-publication-card,
.admin-tool-empty-state,
.admin-tool-inline-note {
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.5);
}
.admin-tool-review-grid {
display: grid;
gap: 1rem;
}
.admin-tool-inline-note {
padding: 0.9rem 1rem;
}
.admin-tool-review-page .admin-hero-card::after {
background: radial-gradient(circle, rgba(20, 77, 71, 0.2), transparent 72%);
}

@ -0,0 +1,106 @@
from pydantic import BaseModel
class AdminPanelNavigationItem(BaseModel):
label: str
href: str
description: str
badge: str | None = None
is_active: bool = False
class AdminPanelQuickAction(BaseModel):
label: str
href: str
button_class: str = "btn-outline-dark"
class AdminPanelMetric(BaseModel):
label: str
value: str
description: str
class AdminPanelModuleCard(BaseModel):
eyebrow: str
title: str
description: str
status_label: str
status_variant: str = "secondary"
highlights: tuple[str, ...] = ()
cta_label: str | None = None
href: str | None = None
is_available: bool = False
class AdminPanelSurfaceLink(BaseModel):
method: str
label: str
href: str
description: str
class AdminPanelRoadmapItem(BaseModel):
step: str
title: str
description: str
status_label: str
class AdminPanelHomeView(BaseModel):
service: str
app_name: str
panel_title: str
panel_subtitle: str
environment: str
version: str
api_prefix: str
release_label: str
navigation: tuple[AdminPanelNavigationItem, ...]
quick_actions: tuple[AdminPanelQuickAction, ...]
metrics: tuple[AdminPanelMetric, ...]
modules: tuple[AdminPanelModuleCard, ...]
surface_links: tuple[AdminPanelSurfaceLink, ...]
roadmap: tuple[AdminPanelRoadmapItem, ...]
class AdminLoginPageView(BaseModel):
app_name: str
title: str
subtitle: str
environment: str
version: str
dashboard_href: str
auth_endpoint: str
email_placeholder: str
password_placeholder: str
access_token_ttl_label: str
refresh_token_ttl_label: str
password_policy_label: str
security_highlights: tuple[str, ...]
integration_notes: tuple[str, ...]
class AdminToolReviewWorkflowStep(BaseModel):
eyebrow: str
title: str
description: str
status_label: str
status_variant: str = "secondary"
class AdminToolReviewPageView(BaseModel):
app_name: str
title: str
subtitle: str
environment: str
version: str
dashboard_href: str
overview_endpoint: str
contracts_endpoint: str
review_queue_endpoint: str
publications_endpoint: str
workflow: tuple[AdminToolReviewWorkflowStep, ...]
review_notes: tuple[str, ...]
approval_notes: tuple[str, ...]
activation_notes: tuple[str, ...]

@ -1,4 +1,4 @@
import unittest
import unittest
from fastapi.testclient import TestClient
@ -9,7 +9,7 @@ from shared.contracts import StaffRole
class AdminAppBootstrapTests(unittest.TestCase):
def test_admin_app_root_endpoint(self):
def test_admin_app_root_endpoint_returns_json_for_non_browser_requests(self):
app = create_app(AdminSettings(admin_environment="staging"))
client = TestClient(app)
@ -26,6 +26,15 @@ class AdminAppBootstrapTests(unittest.TestCase):
},
)
def test_admin_app_root_endpoint_redirects_browser_to_login(self):
app = create_app(AdminSettings())
client = TestClient(app)
response = client.get("/", headers={"accept": "text/html"}, follow_redirects=False)
self.assertEqual(response.status_code, 302)
self.assertTrue(response.headers["location"].endswith("/login"))
def test_admin_app_health_endpoint(self):
app = create_app(AdminSettings(admin_version="1.2.3"))
client = TestClient(app)

@ -1,4 +1,4 @@
import unittest
import unittest
from datetime import datetime, timedelta, timezone
from admin_app.core import AdminSecurityService, AdminSettings
@ -182,6 +182,21 @@ class AdminAuthServiceTests(unittest.TestCase):
with self.assertRaises(ValueError):
self.auth_service.get_authenticated_context(session.access_token)
def test_get_authenticated_context_accepts_naive_session_expiry_from_database(self):
session = self.auth_service.login(
email="admin@empresa.com",
password="SenhaMuitoSegura!123",
ip_address="127.0.0.1",
user_agent="unittest",
)
stored_session = self.session_repository.get_by_id(session.session_id)
stored_session.expires_at = stored_session.expires_at.replace(tzinfo=None)
self.session_repository.save(stored_session)
context = self.auth_service.get_authenticated_context(session.access_token)
self.assertEqual(context.session_id, session.session_id)
self.assertEqual(context.principal.email, "admin@empresa.com")
def test_refresh_session_rejects_expired_session(self):
session = self.auth_service.login(
email="admin@empresa.com",
@ -203,4 +218,4 @@ class AdminAuthServiceTests(unittest.TestCase):
if __name__ == "__main__":
unittest.main()
unittest.main()

@ -0,0 +1,163 @@
import unittest
from fastapi.testclient import TestClient
from admin_app.api.dependencies import get_auth_service, get_current_panel_staff_context
from admin_app.api.panel_session import PANEL_ACCESS_COOKIE_NAME, PANEL_REFRESH_COOKIE_NAME
from admin_app.app_factory import create_app
from admin_app.core import (
AdminAuthenticatedSession,
AdminSettings,
AuthenticatedStaffContext,
AuthenticatedStaffPrincipal,
)
from shared.contracts import StaffRole
class _FakePanelAuthService:
def login(self, email: str, password: str, *, ip_address: str | None, user_agent: str | None):
if email == "admin@empresa.com" and password == "SenhaMuitoSegura!123":
principal = AuthenticatedStaffPrincipal(
id=1,
email="admin@empresa.com",
display_name="Administrador",
role=StaffRole.ADMIN,
is_active=True,
)
return AdminAuthenticatedSession(
session_id=77,
access_token="panel-access-token",
refresh_token="panel-refresh-token",
token_type="bearer",
expires_in_seconds=1800,
principal=principal,
)
return None
def refresh_session(self, refresh_token: str, *, ip_address: str | None, user_agent: str | None):
if refresh_token == "panel-refresh-token":
principal = AuthenticatedStaffPrincipal(
id=1,
email="admin@empresa.com",
display_name="Administrador",
role=StaffRole.ADMIN,
is_active=True,
)
return AdminAuthenticatedSession(
session_id=77,
access_token="panel-access-token-next",
refresh_token="panel-refresh-token-next",
token_type="bearer",
expires_in_seconds=1800,
principal=principal,
)
return None
def get_authenticated_context(self, access_token: str) -> AuthenticatedStaffContext:
if access_token in {"panel-access-token", "panel-access-token-next"}:
return AuthenticatedStaffContext(
principal=AuthenticatedStaffPrincipal(
id=1,
email="admin@empresa.com",
display_name="Administrador",
role=StaffRole.ADMIN,
is_active=True,
),
session_id=77,
)
raise ValueError("invalid token")
def logout(
self,
session_id: int,
*,
actor_staff_account_id: int | None,
ip_address: str | None,
user_agent: str | None,
) -> bool:
return session_id == 77
def logout_by_refresh_token(
self,
refresh_token: str,
*,
ip_address: str | None,
user_agent: str | None,
) -> int | None:
if refresh_token in {"panel-refresh-token", "panel-refresh-token-next"}:
return 77
return None
class AdminPanelAuthWebTests(unittest.TestCase):
def setUp(self):
app = create_app(AdminSettings(admin_auth_token_secret="test-secret"))
app.dependency_overrides[get_auth_service] = lambda: _FakePanelAuthService()
self.client = TestClient(app)
self.app = app
def tearDown(self):
self.app.dependency_overrides.clear()
def test_panel_login_sets_http_only_cookies_and_returns_session_payload(self):
response = self.client.post(
"/panel/auth/login",
json={"email": "admin@empresa.com", "password": "SenhaMuitoSegura!123"},
)
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["session_id"], 77)
self.assertEqual(response.json()["redirect_to"], "/panel/admin")
self.assertEqual(response.cookies.get(PANEL_ACCESS_COOKIE_NAME), "panel-access-token")
self.assertEqual(response.cookies.get(PANEL_REFRESH_COOKIE_NAME), "panel-refresh-token")
set_cookie_headers = response.headers.get_list("set-cookie")
self.assertTrue(any("HttpOnly" in header for header in set_cookie_headers))
def test_panel_refresh_rotates_cookie_backed_session(self):
self.client.cookies.set(PANEL_REFRESH_COOKIE_NAME, "panel-refresh-token")
response = self.client.post("/panel/auth/refresh")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["message"], "Sessao administrativa web renovada.")
self.assertEqual(response.cookies.get(PANEL_ACCESS_COOKIE_NAME), "panel-access-token-next")
self.assertEqual(response.cookies.get(PANEL_REFRESH_COOKIE_NAME), "panel-refresh-token-next")
def test_panel_session_reads_authenticated_staff_from_cookie_context(self):
app = create_app(AdminSettings(admin_auth_token_secret="test-secret"))
app.dependency_overrides[get_current_panel_staff_context] = lambda: AuthenticatedStaffContext(
principal=AuthenticatedStaffPrincipal(
id=1,
email="admin@empresa.com",
display_name="Administrador",
role=StaffRole.ADMIN,
is_active=True,
),
session_id=77,
)
client = TestClient(app)
try:
response = client.get("/panel/auth/session")
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["staff_account"]["email"], "admin@empresa.com")
self.assertEqual(response.json()["session_id"], 77)
def test_panel_logout_clears_cookies_and_returns_login_redirect(self):
self.client.cookies.set(PANEL_ACCESS_COOKIE_NAME, "panel-access-token")
self.client.cookies.set(PANEL_REFRESH_COOKIE_NAME, "panel-refresh-token")
response = self.client.post("/panel/auth/logout")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["redirect_to"], "/login")
set_cookie_headers = response.headers.get_list("set-cookie")
self.assertTrue(any(PANEL_ACCESS_COOKIE_NAME in header and "Max-Age=0" in header for header in set_cookie_headers))
self.assertTrue(any(PANEL_REFRESH_COOKIE_NAME in header and "Max-Age=0" in header for header in set_cookie_headers))
if __name__ == "__main__":
unittest.main()

@ -0,0 +1,83 @@
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 AdminPanelToolsWebTests(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=21,
email="staff@empresa.com",
display_name="Equipe Web",
role=role,
is_active=True,
)
return TestClient(app), app
def test_panel_tools_overview_is_available_for_staff_session(self):
client, app = self._build_client_with_role(StaffRole.STAFF)
try:
response = client.get("/admin/panel/tools/overview")
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 200)
payload = response.json()
self.assertEqual(payload["mode"], "bootstrap_catalog")
self.assertIn("/admin/panel/tools/contracts", [item["href"] for item in payload["actions"]])
def test_panel_tools_review_queue_is_available_for_staff_session(self):
client, app = self._build_client_with_role(StaffRole.STAFF)
try:
response = client.get("/admin/panel/tools/review-queue")
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json()["queue_mode"], "bootstrap_empty_state")
def test_panel_tools_publications_require_admin_publication_permission(self):
client, app = self._build_client_with_role(StaffRole.STAFF)
try:
response = client.get("/admin/panel/tools/publications")
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 403)
self.assertEqual(
response.json()["detail"],
"Permissao administrativa insuficiente: 'publish_tools'.",
)
def test_panel_tools_publications_return_catalog_for_admin_session(self):
client, app = self._build_client_with_role(StaffRole.ADMIN)
try:
response = client.get("/admin/panel/tools/publications")
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 200)
payload = response.json()
self.assertEqual(payload["target_service"], "product")
self.assertGreaterEqual(len(payload["publications"]), 10)
self.assertIn("consultar_estoque", [item["tool_name"] for item in payload["publications"]])
if __name__ == "__main__":
unittest.main()

@ -0,0 +1,134 @@
import unittest
from fastapi.testclient import TestClient
from admin_app.app_factory import create_app
from admin_app.core import AdminSettings, AuthenticatedStaffPrincipal
from admin_app.api.dependencies import get_current_staff_principal
from shared.contracts import StaffRole
class AdminSystemConfigurationWebTests(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=10,
email="staff@empresa.com",
display_name="Equipe Interna",
role=role,
is_active=True,
)
return TestClient(app), app
def test_configuration_routes_require_manage_settings_permission(self):
client, app = self._build_client_with_role(StaffRole.STAFF)
try:
response = client.get("/admin/system/configuration", 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_configuration_overview_returns_runtime_security_and_sources(self):
settings = AdminSettings(
admin_auth_token_secret="test-secret",
admin_app_name="Admin Interno",
admin_environment="development",
admin_version="0.9.0",
admin_api_prefix="/admin",
admin_debug=True,
admin_db_host="db.internal",
admin_db_port=3307,
admin_db_name="orquestrador_admin_dev",
admin_db_cloud_sql_connection_name="project:region:instance",
admin_auth_password_pepper="pepper",
admin_auth_access_token_ttl_minutes=45,
admin_auth_refresh_token_ttl_days=10,
admin_bootstrap_enabled=True,
admin_bootstrap_email="bootstrap@empresa.com",
admin_bootstrap_display_name="Bootstrap Admin",
admin_bootstrap_password="SenhaMuitoSegura!123",
admin_bootstrap_role="admin",
)
client, app = self._build_client_with_role(StaffRole.ADMIN, settings)
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["service"], "orquestrador-admin")
self.assertEqual(payload["runtime"]["application"]["app_name"], "Admin Interno")
self.assertEqual(payload["runtime"]["database"]["host"], "db.internal")
self.assertTrue(payload["runtime"]["database"]["cloud_sql_configured"])
self.assertEqual(payload["runtime"]["panel_session"]["cookie_path"], "/admin")
self.assertFalse(payload["runtime"]["panel_session"]["secure_cookies"])
self.assertEqual(payload["security"]["tokens"]["access_token_ttl_minutes"], 45)
self.assertTrue(payload["security"]["password"]["pepper_configured"])
self.assertTrue(payload["security"]["bootstrap"]["enabled"])
self.assertTrue(payload["security"]["bootstrap"]["password_configured"])
self.assertIn("panel_session", [item["key"] for item in payload["sources"]])
def test_runtime_configuration_route_exposes_panel_cookie_metadata(self):
settings = AdminSettings(
admin_auth_token_secret="test-secret",
admin_api_prefix="/admin",
admin_environment="production",
admin_debug=False,
)
client, app = self._build_client_with_role(StaffRole.ADMIN, settings)
try:
response = client.get("/admin/system/configuration/runtime", headers={"Authorization": "Bearer token"})
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 200)
runtime = response.json()["runtime"]
self.assertEqual(runtime["panel_session"]["access_cookie_name"], "orquestrador_admin_panel_access")
self.assertEqual(runtime["panel_session"]["refresh_cookie_name"], "orquestrador_admin_panel_refresh")
self.assertEqual(runtime["panel_session"]["same_site"], "lax")
self.assertTrue(runtime["panel_session"]["secure_cookies"])
def test_security_configuration_route_returns_credential_strategy_snapshot(self):
settings = AdminSettings(
admin_auth_token_secret="test-secret",
admin_api_prefix="/admin",
admin_auth_password_min_length=14,
admin_auth_token_issuer="admin-runtime",
admin_auth_refresh_token_bytes=48,
admin_bootstrap_enabled=True,
admin_bootstrap_role="admin",
)
client, app = self._build_client_with_role(StaffRole.ADMIN, settings)
try:
response = client.get("/admin/system/configuration/security", headers={"Authorization": "Bearer token"})
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 200)
security = response.json()["security"]
self.assertEqual(security["password"]["min_length"], 14)
self.assertEqual(security["tokens"]["issuer"], "admin-runtime")
self.assertEqual(security["tokens"]["refresh_token_bytes"], 48)
self.assertEqual(security["bootstrap"]["role"], "admin")
if __name__ == "__main__":
unittest.main()

@ -0,0 +1,135 @@
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 AdminToolsWebTests(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=11,
email="staff@empresa.com",
display_name="Equipe de Tools",
role=role,
is_active=True,
)
return TestClient(app), app
def test_tools_overview_requires_manage_tool_drafts_permission(self):
client, app = self._build_client_with_role(StaffRole.VIEWER)
try:
response = client.get("/admin/tools/overview", headers={"Authorization": "Bearer token"})
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 403)
self.assertEqual(
response.json()["detail"],
"Permissao administrativa insuficiente: 'manage_tool_drafts'.",
)
def test_tools_overview_returns_metrics_workflow_and_actions(self):
client, app = self._build_client_with_role(StaffRole.STAFF)
try:
response = client.get("/admin/tools/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"], "bootstrap_catalog")
self.assertEqual(payload["metrics"][0]["value"], "18")
self.assertIn("active", [item["code"] for item in payload["workflow"]])
self.assertIn("/admin/tools/contracts", [item["href"] for item in payload["actions"]])
self.assertIn("ToolDraft", payload["next_steps"][0])
def test_tools_contracts_return_shared_contract_snapshot(self):
client, app = self._build_client_with_role(StaffRole.STAFF)
try:
response = client.get("/admin/tools/contracts", headers={"Authorization": "Bearer token"})
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 200)
payload = response.json()
self.assertEqual(payload["publication_source_service"], "admin")
self.assertEqual(payload["publication_target_service"], "product")
self.assertIn("draft", [item["code"] for item in payload["lifecycle_statuses"]])
self.assertIn("string", [item["code"] for item in payload["parameter_types"]])
self.assertIn("published_tool", payload["publication_fields"])
def test_tools_drafts_return_empty_state_until_persistence_exists(self):
client, app = self._build_client_with_role(StaffRole.STAFF)
try:
response = client.get("/admin/tools/drafts", headers={"Authorization": "Bearer token"})
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 200)
payload = response.json()
self.assertEqual(payload["storage_status"], "pending_persistence")
self.assertEqual(payload["drafts"], [])
self.assertEqual(payload["supported_statuses"], ["draft"])
def test_tools_review_queue_is_available_for_staff(self):
client, app = self._build_client_with_role(StaffRole.STAFF)
try:
response = client.get("/admin/tools/review-queue", headers={"Authorization": "Bearer token"})
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 200)
payload = response.json()
self.assertEqual(payload["queue_mode"], "bootstrap_empty_state")
self.assertEqual(payload["items"], [])
self.assertIn("validated", payload["supported_statuses"])
def test_tools_publications_require_publish_tools_permission(self):
client, app = self._build_client_with_role(StaffRole.STAFF)
try:
response = client.get("/admin/tools/publications", headers={"Authorization": "Bearer token"})
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 403)
self.assertEqual(
response.json()["detail"],
"Permissao administrativa insuficiente: 'publish_tools'.",
)
def test_tools_publications_return_bootstrap_catalog_for_admin(self):
client, app = self._build_client_with_role(StaffRole.ADMIN)
try:
response = client.get("/admin/tools/publications", headers={"Authorization": "Bearer token"})
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 200)
payload = response.json()
self.assertEqual(payload["source"], "bootstrap_catalog")
self.assertEqual(payload["target_service"], "product")
self.assertGreaterEqual(len(payload["publications"]), 10)
self.assertIn("consultar_estoque", [item["tool_name"] for item in payload["publications"]])
first = payload["publications"][0]
self.assertEqual(first["status"], "active")
self.assertEqual(first["implementation_module"], "app.services.tools.handlers")
if __name__ == "__main__":
unittest.main()

@ -0,0 +1,163 @@
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() -> AuthenticatedStaffContext:
return AuthenticatedStaffContext(
principal=AuthenticatedStaffPrincipal(
id=7,
email="admin@empresa.com",
display_name="Administrador",
role=StaffRole.ADMIN,
is_active=True,
),
session_id=77,
)
class AdminViewBootstrapTests(unittest.TestCase):
def test_panel_entry_redirects_to_login_without_session(self):
app = create_app(AdminSettings(admin_app_name="Admin Interno", admin_version="1.4.0"))
client = TestClient(app)
response = client.get("/panel", follow_redirects=False)
self.assertEqual(response.status_code, 302)
self.assertTrue(response.headers["location"].endswith("/login"))
def test_panel_entry_redirects_to_admin_dashboard_with_session(self):
app = create_app(AdminSettings(admin_app_name="Admin Interno", admin_version="1.4.0"))
app.dependency_overrides[get_optional_panel_staff_context] = lambda: _build_panel_context()
client = TestClient(app)
try:
response = client.get("/panel", follow_redirects=False)
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 302)
self.assertTrue(response.headers["location"].endswith("/panel/admin"))
def test_login_page_redirects_to_dashboard_when_session_exists(self):
app = create_app(AdminSettings(admin_app_name="Admin Interno", admin_version="1.4.0"))
app.dependency_overrides[get_optional_panel_staff_context] = lambda: _build_panel_context()
client = TestClient(app)
try:
response = client.get("/login", follow_redirects=False)
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 302)
self.assertTrue(response.headers["location"].endswith("/panel/admin"))
def test_login_page_renders_focused_auth_experience(self):
app = create_app(AdminSettings(admin_app_name="Admin Interno", admin_version="1.4.0"))
client = TestClient(app)
response = client.get("/login")
self.assertEqual(response.status_code, 200)
self.assertIn("Login administrativo", response.text)
self.assertIn("Acesso restrito", response.text)
self.assertIn('data-admin-login-form="true"', response.text)
self.assertIn('data-auth-endpoint="/panel/auth/login"', response.text)
self.assertIn('data-dashboard-href="http://testserver/panel/admin"', response.text)
self.assertNotIn('data-session-endpoint=', response.text)
self.assertNotIn('data-logout-endpoint=', response.text)
self.assertNotIn("Voltar ao dashboard", response.text)
self.assertNotIn("/panel/tools/review", response.text)
def test_admin_dashboard_renders_bootstrap_dashboard_when_session_exists(self):
app = create_app(AdminSettings(admin_app_name="Admin Interno", admin_version="1.4.0"))
app.dependency_overrides[get_optional_panel_staff_context] = lambda: _build_panel_context()
client = TestClient(app)
try:
response = client.get("/panel/admin")
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 200)
self.assertIn("text/html", response.headers["content-type"])
self.assertIn("Painel Administrativo", response.text)
self.assertIn("Dashboard do administrador", response.text)
self.assertIn("Areas do sistema", response.text)
self.assertIn("Entradas claras para as areas protegidas", response.text)
self.assertIn("Revisao de tools", response.text)
self.assertIn("/panel/tools/review", response.text)
self.assertIn("/panel/assets/styles/panel.css", response.text)
self.assertNotIn("API pronta para ser plugada na UI", response.text)
def test_tool_review_page_redirects_to_login_without_session(self):
app = create_app(AdminSettings(admin_app_name="Admin Interno", admin_version="1.4.0"))
client = TestClient(app)
response = client.get("/panel/tools/review", follow_redirects=False)
self.assertEqual(response.status_code, 302)
self.assertTrue(response.headers["location"].endswith("/login"))
def test_tool_review_page_renders_web_data_endpoints_when_session_exists(self):
app = create_app(AdminSettings(admin_app_name="Admin Interno", admin_version="1.4.0"))
app.dependency_overrides[get_optional_panel_staff_context] = lambda: _build_panel_context()
client = TestClient(app)
try:
response = client.get("/panel/tools/review")
finally:
app.dependency_overrides.clear()
self.assertEqual(response.status_code, 200)
self.assertIn("Revisao, aprovacao e ativacao", response.text)
self.assertIn('data-admin-tool-review-board="true"', response.text)
self.assertIn('data-overview-endpoint="/panel/tools/overview"', response.text)
self.assertIn('data-contracts-endpoint="/panel/tools/contracts"', response.text)
self.assertIn('data-review-queue-endpoint="/panel/tools/review-queue"', response.text)
self.assertIn('data-publications-endpoint="/panel/tools/publications"', response.text)
self.assertNotIn("Abrir login administrativo", response.text)
def test_prefixed_panel_routes_apply_auth_gate(self):
app = create_app(AdminSettings(admin_api_prefix="/admin"))
client = TestClient(app)
panel_response = client.get("/admin/panel", follow_redirects=False)
login_response = client.get("/admin/login")
review_response = client.get("/admin/panel/tools/review", follow_redirects=False)
css_response = client.get("/admin/panel/assets/styles/panel.css")
self.assertEqual(panel_response.status_code, 302)
self.assertEqual(login_response.status_code, 200)
self.assertEqual(review_response.status_code, 302)
self.assertEqual(css_response.status_code, 200)
self.assertTrue(panel_response.headers["location"].endswith("/admin/login"))
self.assertTrue(review_response.headers["location"].endswith("/admin/login"))
self.assertIn('data-auth-endpoint="/admin/panel/auth/login"', login_response.text)
self.assertIn('data-dashboard-href="http://testserver/admin/panel/admin"', login_response.text)
self.assertNotIn('data-session-endpoint=', login_response.text)
self.assertNotIn('data-logout-endpoint=', login_response.text)
self.assertIn("/admin/panel/assets/styles/panel.css", login_response.text)
self.assertIn("--admin-bg", css_response.text)
def test_prefixed_admin_dashboard_and_review_render_when_session_exists(self):
app = create_app(AdminSettings(admin_api_prefix="/admin"))
app.dependency_overrides[get_optional_panel_staff_context] = lambda: _build_panel_context()
client = TestClient(app)
try:
panel_response = client.get("/admin/panel/admin")
review_response = client.get("/admin/panel/tools/review")
finally:
app.dependency_overrides.clear()
self.assertEqual(panel_response.status_code, 200)
self.assertEqual(review_response.status_code, 200)
self.assertIn("Dashboard do administrador", panel_response.text)
self.assertIn("/admin/panel/tools/review", panel_response.text)
self.assertIn('data-overview-endpoint="/admin/panel/tools/overview"', review_response.text)
self.assertIn('data-publications-endpoint="/admin/panel/tools/publications"', review_response.text)
if __name__ == "__main__":
unittest.main()
Loading…
Cancel
Save