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:
@@ -74,7 +74,7 @@ def sync_from_gerenciador(session, clients: list[dict]) -> SyncResult:
|
||||
|
||||
if row is None:
|
||||
name = wanted_name if wanted_name not in taken_names else f"{wanted_name} ({code})"
|
||||
row = Tenant(name=name, source=SOURCE, external_client_id=external_id, external_code=code, active=active, synced_at=now)
|
||||
row = Tenant(name=name, source=SOURCE, external_client_id=external_id, external_code=code, external_cnpj=client.get("cnpj"), active=active, synced_at=now)
|
||||
session.add(row)
|
||||
taken_names.add(name)
|
||||
created += 1
|
||||
@@ -89,6 +89,9 @@ def sync_from_gerenciador(session, clients: list[dict]) -> SyncResult:
|
||||
if row.external_code != code:
|
||||
row.external_code = code
|
||||
changed = True
|
||||
if row.external_cnpj != client.get("cnpj"):
|
||||
row.external_cnpj = client.get("cnpj")
|
||||
changed = True
|
||||
if row.active != active:
|
||||
row.active = active
|
||||
changed = True
|
||||
|
||||
@@ -229,6 +229,7 @@ class Tenant(Base):
|
||||
source: Mapped[str] = mapped_column(String(16), default="LOCAL", server_default="LOCAL") # LOCAL | GTI_GERENCIADOR
|
||||
external_client_id: Mapped[int | None] = mapped_column(Integer, nullable=True, index=True)
|
||||
external_code: Mapped[str | None] = mapped_column(String(16), nullable=True)
|
||||
external_cnpj: Mapped[str | None] = mapped_column(String(20), nullable=True) # digits only, for the client selector search
|
||||
active: Mapped[bool] = mapped_column(Boolean, default=True, server_default="true")
|
||||
synced_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
|
||||
|
||||
|
||||
+11
-6
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user