fix: searchable client combobox in Nova instalação

Replaces the native list with a combobox (name/code/CNPJ search, accent- and
case-insensitive, keyboard + mouse, custom dropdown, hidden client_id = the
Gerenciador's real CLIENTE.ID). The backend resolves client_id against the
active GTI_GERENCIADOR mirror and rejects free text/unknown ids. Adds
Tenant.external_cnpj (migration 0025) mirrored via the internal endpoint.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
gti
2026-09-24 08:12:52 -03:00
co-authored by Claude Sonnet 5
parent 3ccef36c50
commit 9f9e06e9e9
9 changed files with 156 additions and 40 deletions
+11 -6
View File
@@ -208,6 +208,11 @@ def router(session_factory):
if search: query = query.where(Tenant.name.ilike(f"%{search}%"))
return session.scalars(query.order_by(Tenant.name)).all()
def _clients_json(tenants):
import json
# "<" escaped so the payload can never close the <script> tag it is embedded in.
return json.dumps([{"id": t.external_client_id, "name": t.name, "code": t.external_code or "", "cnpj": t.external_cnpj or ""} for t in tenants], ensure_ascii=False).replace("<", "\u003c")
@routes.get("/agents/new", response_class=HTMLResponse)
def new_installation_form(request: Request, search: str | None = None):
session, auth_data, redirect = page_auth(request)
@@ -215,7 +220,7 @@ def router(session_factory):
try:
tenants = _active_gerenciador_tenants(session, search)
releases = session.scalars(select(AgentRelease).where(AgentRelease.status == "PUBLISHED").order_by(AgentRelease.created_at.desc())).all()
return render(request, "agent_new_installation.html", {"tenants": tenants, "releases": releases, "auth": auth_data, "error": None, "search": search or ""})
return render(request, "agent_new_installation.html", {"tenants": tenants, "clients_json": _clients_json(tenants), "releases": releases, "auth": auth_data, "error": None, "search": search or ""})
finally: session.close()
@routes.post("/agents/new")
@@ -225,24 +230,24 @@ def router(session_factory):
try:
values = await form_values(request); web_session, admin = auth_data
require_csrf(web_session, values.get("csrf_token"))
tenant_id = values.get("tenant_id")
client_id = values.get("client_id") # Gerenciador's real CLIENTE.ID, never the text shown in the combobox
display_name = (values.get("display_name") or "").strip()[:160]
expected_hostname = (values.get("expected_hostname") or "").strip()[:255] or None
release_id = values.get("release_id")
errors = []
tenant = session.get(Tenant, int(tenant_id)) if tenant_id and tenant_id.isdigit() else None
tenant = session.scalar(select(Tenant).where(Tenant.source == client_sync.SOURCE, Tenant.external_client_id == int(client_id))) if client_id and client_id.isdigit() else None
release = session.get(AgentRelease, int(release_id)) if release_id and release_id.isdigit() else None
# Server-side, not just a hidden option in the form: a LOCAL or
# inactive tenant can never receive a new installation, matching
# GTI Gerenciador's own active/inactive rule.
if tenant and (tenant.source != client_sync.SOURCE or not tenant.active): tenant = None
if not tenant: errors.append("Selecione um cliente ativo válido (cadastrado no GTI Gerenciador).")
if tenant and not tenant.active: tenant = None
if not tenant: errors.append("Selecione um cliente da lista.")
if not display_name: errors.append("Informe um nome para a instalação.")
if not release or release.status != "PUBLISHED": errors.append("Selecione uma versão publicada do Agent.")
if errors:
tenants = _active_gerenciador_tenants(session)
releases = session.scalars(select(AgentRelease).where(AgentRelease.status == "PUBLISHED").order_by(AgentRelease.created_at.desc())).all()
return render(request, "agent_new_installation.html", {"tenants": tenants, "releases": releases, "auth": auth_data, "error": " ".join(errors), "search": ""}, 400)
return render(request, "agent_new_installation.html", {"tenants": tenants, "clients_json": _clients_json(tenants), "releases": releases, "auth": auth_data, "error": " ".join(errors), "search": ""}, 400)
enrollment_uuid = str(uuid.uuid4())
enr = AgentEnrollment(enrollment_uuid=enrollment_uuid, tenant_id=tenant.id, display_name=display_name, expected_hostname=expected_hostname, release_id=release.id, status="PENDING", created_by=admin.id)
session.add(enr)