0021 — Seletor de parceira só alcança a primeira página, e em Inquilinos nem a busca resolve¶
| Status | CORRIGIDO |
| Branch | fix/0021-seletor-imobiliaria-limitado-a-50 |
| Aberto em | 2026-09-27 |
| Severidade | ALTA |
| Apps afetados | api · web · e2e |
| Revisão do diagnóstico | aprovado por decisão do usuário, após 3 rodadas |
| Revisão da correção | aprovado na rodada 6, por decisão do usuário de passar de três |
Status:
RASCUNHO→DIAGNOSTICADO→EM CORREÇÃO→CORRIGIDO
1. Sintoma¶
Numa garantidora com mais de 50 imobiliárias parceiras, o seletor de parceira não alcança todas elas — e em Inquilinos não alcança nem digitando o nome exato.
Quem cadastra um inquilino novo abre o campo Imobiliária Parceira, digita o nome da parceira, e o campo responde "Nenhuma imobiliária encontrada." A parceira existe, está ativa, e aparece normalmente na tela de Imobiliárias. Não há erro, não há aviso, não há nada no console: para quem usa, a imobiliária simplesmente não existe naquele campo. O caminho que sobra é cadastrar o inquilino sem parceira — o que significa registrá-lo como carteira própria da garantidora, que é outro dado.
Em Imóveis o sintoma é mais brando: digitar o nome funciona, mas quem não sabe o nome não consegue percorrer a lista — o dropdown abre com 50 e não carrega mais nada ao rolar.
Em Análise de Crédito, ao acrescentar um corresponsável, a lista de pessoas tem o mesmo teto, em 200.
2. Reprodução¶
- Entrar como usuário da garantidora (não de imobiliária) num tenant com mais de 50 parceiras ativas.
- Ir em Inquilinos → Novo.
- No painel Imobiliária Parceira, digitar o nome de uma parceira que não esteja entre as 50 mais recentes — no dev-remote,
Imob 0025 1789386969307, que é a 51ª. - O campo exibe "Nenhuma imobiliária encontrada."
Para Imóveis (sintoma brando): Imóveis → Novo, abrir o mesmo campo e rolar o dropdown até o fim — param 50 opções, sem carregar mais.
Para Análise de Crédito: com mais de 200 inquilinos no tenant, abrir uma análise e tentar acrescentar como corresponsável alguém fora dos 200 mais recentes.
- Ambiente: dev-remote (
https://dev.garantora.com.br/app) — reproduz hoje, sem preparar nada. - Superfície: ambos. Não depende de breakpoint nem do PWA — o defeito é a requisição que o app faz, não o que ele desenha.
- Dados/pré-condições: o tenant Garantidora E2E tem 118 parceiras ativas e 229 inquilinos. Como a janela do seletor de parceira é de 50, 68 parceiras estão fora dela; como a da análise é de 200, 29 inquilinos estão fora.
Medido em 2026-09-27, contra o dev-remote, autenticado como socio@e2e.test — ADMIN_TENANT da Garantidora E2E, que é o papel do passo 1 e tem filtro de tenant:
GET /api/v1/partner-agencies → 50 itens, total: 118
GET /api/v1/partner-agencies?skip=50&limit=1 → "Imob 0025 1789386969307" (a 51ª)
essa 51ª está entre os 50 da 1ª chamada? → não
GET /api/v1/partner-agencies?search=Imob 0025 1789386969307
→ 1 item, é ela
A primeira chamada é exatamente a que o app faz em Inquilinos (partnerAgenciesApi.list(), sem parâmetro). A terceira é a que ele não faz — e mostra que o registro está lá e é alcançável por quem pergunta direito.
E na Análise de Crédito, onde o campo pede limit: 200:
GET /api/v1/renters?limit=200 → 200 itens, total: 229 (29 fora do alcance do campo)
GET /api/v1/renters?limit=201 → HTTP 422 (o teto é do servidor)
A primeira medição deste registro foi feita com um
ADMIN_GLOBALe foi descartada. Esse papel não tem filtro de tenant (apps/api/app/services/scoping.py:48), então ototal: 119que ela produziu era a soma de todas as garantidoras — número que não sustenta uma reprodução cujo passo 1 exige usuário da garantidora. Refeita com o papel certo, a contagem do tenant é 118, e bate com o banco.
3. Esperado × Atual¶
- Esperado: o seletor alcança todas as parceiras da garantidora, por dois caminhos: digitando (busca no servidor, não no que já foi carregado) e rolando — o dropdown carrega a próxima página ao chegar ao fim, até acabarem. Decidido com o usuário em 2026-09-27: rolagem infinita, porque só ela atende quem não sabe o nome.
- Atual: o seletor enxerga uma página e só. Em Imóveis a digitação escapa disso porque consulta o servidor; em Inquilinos e em Análise de Crédito a digitação filtra o que já está na memória, então não escapa.
4. Causa raiz¶
As linhas citadas nesta seção são as do código antes da correção —
git show afd5596:<arquivo>as reproduz. A correção mudou esses arquivos, e a numeração de hoje não é mais a mesma.
Onde¶
O defeito está no cliente, em sete chamadas — quatro arquivos de tela mais o componente SelectionBlock, em três telas do inventário — e não na API:
| Tela | Carga inicial | Busca ao digitar | Filtro do MUI |
|---|---|---|---|
| Inquilinos (form) | apps/web/src/pages/renters/form.tsx:107 — partnerAgenciesApi.list(), sem parâmetro | não existe | apps/web/src/pages/renters/form.tsx:566-567 — <Autocomplete options={agencies}> sem filterOptions, isto é, filtro do cliente |
| Imóveis (form) | apps/web/src/pages/properties/form.tsx:91 | apps/web/src/pages/properties/form.tsx:133 — .list({ search: termo, limit: 50 }) | apps/web/src/pages/properties/form.tsx:263 — filterOptions={(options) => options}, desligado |
| Imóveis (filtro da lista) | apps/web/src/pages/properties/index.tsx:107 — .list({ limit: 50 }) | apps/web/src/pages/properties/index.tsx:119 | apps/web/src/pages/properties/index.tsx:580 — desligado |
| Análise de Crédito (corresponsável) | apps/web/src/pages/credit-analyses/CreditAnalysisForm.tsx:346 — rentersApi.list({ limit: 200 }) | não existe para este campo | apps/web/src/pages/credit-analyses/CreditAnalysisForm.tsx:1583 — options={people.filter(...)}, filtro do cliente |
| Análise de Crédito (imobiliária) | apps/web/src/pages/credit-analyses/CreditAnalysisForm.tsx:340 — apiReq("GET", "/api/v1/partner-agencies?limit=200"), URL montada à mão | não existe | é SelectField, não Autocomplete (apps/web/src/pages/credit-analyses/CreditAnalysisForm.tsx:1241 — options={agencies.map(...)}): não há busca nem filtro, só a lista que chegou |
| Análise de Crédito (busca de inquilino) | apps/web/src/pages/credit-analyses/CreditAnalysisForm.tsx:452 — rentersApi.list({ search, limit: 20 }), usada em :1258 | existe, a cada termo — e o termo vazio também consulta | não há filtro de cliente; a caixa de resultados rola mas não pagina (apps/web/src/pages/credit-analyses/components/SelectionBlock.tsx:241-247) |
| Análise de Crédito (busca de imóvel) | apps/web/src/pages/credit-analyses/CreditAnalysisForm.tsx:457 — propertiesApi.list({ search, limit: 20 }), usada em :1379 | existe, idem | idem — mesmo componente |
O teto é sempre "uma página", e ele chega de três formas — que não se consertam do mesmo jeito:
- Default do servidor, quando o cliente omite
limit:apps/web/src/pages/properties/api.ts:102-105só acrescenta o parâmetro se ele for passado. É o caso de Inquilinos (apps/web/src/pages/renters/form.tsx:107) e do carregamento inicial de Imóveis (apps/web/src/pages/properties/form.tsx:91), que caem noQuery(50, …)deapps/api/app/api/v1/routers/partner_agencies.py:48. - Número escolhido pelo cliente, abaixo do que a API aceita: o filtro da lista de Imóveis manda
limit: 50(apps/web/src/pages/properties/index.tsx:107) e a busca manda o mesmo (:119, eapps/web/src/pages/properties/form.tsx:133) — mas aquele endpoint aceita até 200 (le=200). Aqui subir o número cobriria as 118 parceiras de hoje; o que ele não faz é resolver amanhã, e é por isso que a decisão da seção 3 foi paginar. - Máximo do servidor, o único teto que o cliente não pode levantar: as duas chamadas da Análise de Crédito pedem
limit: 200—:346para inquilinos (apps/api/app/api/v1/routers/renters.py:44) e:340para parceiras (apps/api/app/api/v1/routers/partner_agencies.py:48). Nos dois endpoints 200 é ole=200. Medido:?limit=201responde HTTP 422.
Onde isso já dói e onde só vai doer: na linha de inquilinos o teto já foi ultrapassado — 229 no tenant contra 200 —, e os 29 últimos só são alcançáveis por skip ou search. Na de parceiras a folga é da contagem de hoje (118 contra 200), não do parâmetro: o teto é o mesmo, e some sozinho quando a base crescer. O que as três formas têm em comum, e é o que a correção precisa atacar, é que nenhuma delas faz uma segunda requisição.
A versão anterior desta lista classificava o
limit: 50de Imóveis como "máximo do servidor", e se contradizia duas linhas abaixo da própria citação departner_agencies.py:48, que dizle=200. Cinquenta é escolha do cliente, não teto da API.
Não é regressão de um commit. É o contrário: a correção existe e não foi propagada. O fix af2b7d5 (2026-08-01) estabeleceu a busca no servidor para o seletor de Imóveis, pelo motivo exato deste bug. O seletor de Inquilinos nasceu dez dias depois, em 3ae0d81 (spec 0034, 2026-08-11), escrito no padrão anterior.
O mecanismo¶
A cadeia tem quatro elos, e o terceiro é o que torna o sintoma mudo:
- O app pede a lista uma vez e recebe uma página — as mais recentes, pela ordem
created_at desc, id descdeapps/api/app/repositories/base.py:95. Seja pelo default do servidor, seja pelo máximo dele (ver acima), o que chega ao componente é um recorte; e não há segunda requisição para buscar o resto. - Essa página vai inteira para o estado do componente (
agencies,people). - O
Autocompletedo MUI, semfilterOptions, aplica o filtro padrão dele, que é sobreoptions— ou seja, sobre as 50 já carregadas. Digitar não dispara requisição nenhuma: é umfilterem memória. - Não achando nada, o MUI renderiza o
noOptionsText— que neste campo é "Nenhuma imobiliária encontrada."
O sintoma mente porque o passo 4 não sabe do passo 1. A frase que a pessoa lê é uma afirmação sobre a base ("não encontrada"), produzida por uma busca que nunca chegou à base. O total vem na resposta e é descartado — o toArray de apps/web/src/pages/properties/api.ts:95-98 fica só com os itens —, então nem "50 de 118" o app teria como dizer. Por isso não há erro, log, nem 404: do ponto de vista do código, tudo funcionou.
Em Imóveis o elo 3 está desarmado (filterOptions devolve as opções sem filtrar) e o elo 1 é refeito com search a cada tecla — por isso lá digitar alcança. O que continua faltando em Imóveis é o elo 1 para quem não digita: rolar não pagina.
A causa de fundo é que a regra não tem dono executável. Ela existe como prosa em dois lugares — o comentário de apps/web/src/pages/properties/api.ts:101 ("search filtra no banco. Filtrar no cliente só alcançaria a primeira página.") e a descrição do endpoint em partner_agencies.py — e em nenhum dos dois como código. Não há componente compartilhado de seletor paginado, não há teste que reprove um Autocomplete sem filterOptions sobre lista paginada, e o make preflight não cobre essa classe. Um consumidor novo do mesmo cliente de API reproduz o defeito sem contrariar nada que o repositório verifique — foi o que aconteceu.
O alcance¶
Varredura feita em 2026-09-27, por três eixos — e cada eixo existe porque o anterior deixou passar um ponto real:
grep -rn "<Autocomplete" apps/web/src --include=*.tsx # o componente
grep -rn "Api\.list()" apps/web/src # consumo do cliente tipado, sem limit
grep -rn "limit: [0-9]\|limit=[0-9]" apps/web/src # limite cravado, inclusive URL à mão
O eixo 1 devolve cinco arquivos (renters/form.tsx, properties/form.tsx, properties/index.tsx, credit-analyses/CreditAnalysisForm.tsx, settings/document-types/index.tsx) e não basta: nem todo seletor é um Autocomplete — o campo de tipo do upload é um Select, e só o eixo 2 o encontrou. O eixo 2 também não basta: nem todo consumidor passa pelo cliente de API tipado — o seletor de imobiliária da Análise monta a query string à mão, e só o eixo 3 o encontrou.
- Reproduz hoje, e entra neste fix: Inquilinos (form), Imóveis (form e filtro da lista), Análise de Crédito (o seletor de corresponsável). O
peopleda Análise é consumido só nesse campo (declarado emCreditAnalysisForm.tsx:301, usado em:1583). - Não reproduz hoje, e entra assim mesmo — decisão do usuário em 2026-09-27: o segundo seletor de imobiliária da Análise de Crédito (
CreditAnalysisForm.tsx:340,SelectFieldem:1241). Pedelimit=200contra 118 parceiras, então hoje alcança todas. Entra porque é a mesma entidade, na mesma tela, a poucas linhas do campo que vai ser corrigido — deixá-lo de fora repetiria literalmente o erro de 2026-08-01, que corrigiu um seletor e não o irmão. - Mesmo defeito, latente — fora do escopo por decisão do usuário:
apps/web/src/components/shared/DocumentsPanel.tsx:305chamadocumentTypesApi.list()semlimit, contra o default 50 deapps/api/app/api/v1/routers/document_types.py:39, e alimenta o<Select>de tipo do upload (DocumentsPanel.tsx:934). A superfície é a maior de todas — o painel aparece em Inquilinos, Imóveis, Análise de Crédito e Garantias —, mas a maior garantidora tem 20 tipos hoje, contra teto de 50. Vira dívida nomeada. - Conferidos e seguros:
roles,permissionsependency_typessão chamados semlimit, mas os três endpoints não paginam (nenhum declaralimit), então devolvem a lista inteira. O<Autocomplete>deapps/web/src/pages/settings/document-types/index.tsx:454lista grupos, delistGroups(), que também não pagina (apps/api/app/api/v1/routers/document_types.py:60-64). - Fora por classe, não por contagem:
apps/web/src/pages/settings/document-types/index.tsx:79,apps/web/src/pages/audit-logs/index.tsx:82eapps/web/src/shared/api/documents.ts:109também comem o teto de uma página, mas alimentam tabelas, não seletores — o remédio de uma tabela é a paginação que ela já tem no rodapé, não um dropdown que rola. Não entram neste fix.apps/web/src/pages/billings/api.ts:175também aparece na varredura e é inofensivo:contarAtrasadospedelimit=1justamente porque só usa ototaldo envelope, e descarta os itens. - Mesma classe, outro desenho — e entra no escopo, por decisão do usuário em 2026-09-27: o
SelectionBlockda Análise de Crédito, alimentado porCreditAnalysisForm.tsx:452(inquilinos) e:457(imóveis), ambos comlimit: 20. Ele busca no servidor a cada termo — inclusive com termo vazio, que traz a primeira página (apps/web/src/pages/credit-analyses/components/SelectionBlock.tsx:45,:84-98) —, então quem sabe o nome sempre alcança. O que falta é o mesmo que falta em Imóveis: a caixa de resultados rola (SelectionBlock.tsx:241-247,overflowY: autocommaxHeight) mas não carrega mais ao chegar ao fim. Com 229 inquilinos, quem não sabe o nome vê 20. É o "sintoma brando" da seção 1, na mesma tela do campo de corresponsável que já entrou.
Duas versões anteriores desta seção erraram a varredura, cada uma de um jeito, e as duas valem como aviso. A primeira citava
contexts/color-mode/index.tsxecomponents/icon/mui-adapters.tsxcomo "Autocompletes sobre listas fixas" — nenhum dos dois renderiza um<Autocomplete>(o primeiro temMuiAutocompletecomo override de tema em:60; o segundo só cita a palavra em comentário): ela casou texto, não componente. A segunda corrigiu isso mas parou em dois eixos, e perdeu o seletor que monta a URL à mão. A lição é a mesma nas duas: a varredura tem de ser pelo que o defeito é — consumo de recurso paginado sem segunda requisição —, não pelo formato em que ele costuma aparecer.
O que trocar o filtro do cliente pela busca do servidor muda — achado na rodada 5 da revisão da correção, e lacuna deste diagnóstico. O filtro antigo, em memória, casava sobre o rótulo inteiro do seletor, e ignorando caixa e acento (o padrão do MUI, useAutocomplete.js:32). A busca do servidor casa outra coisa: a de parceiras compara só name e CNPJ (apps/api/app/repositories/partner_agency_repo.py:72-76), nunca o trade_name que o rótulo mostra; e nenhuma busca da API ignora acento — não há unaccent em lugar nenhum. Resultado: digitar o nome fantasia, ou "Imobiliaria" sem acento, responderia "Nenhuma imobiliária encontrada." sobre uma parceira que existe — o mesmo sintoma deste fix, por outro caminho. Vale igual para o corresponsável ("Joao" não acharia "João"). A decisão foi do usuário (§10, itens 5 e 6): a busca da API passa a olhar o nome fantasia e a ignorar acento.
Onde a causa não morde, e vale registrar porque limita o estrago: a edição de inquilino não perde o vínculo. Ela não depende da lista — busca a parceira por id (apps/web/src/pages/renters/form.tsx:115, partnerAgenciesApi.get(...)) — e o campo fica desabilitado no modo de edição, e partner_agency_id não vai no corpo do update. O dano é só na criação: o vínculo não chega a ser feito.
5. Revisão do diagnóstico¶
Preenchido pelo laço do /fix new, rubrica .claude/rubrics/diagnostico.md. Registrar todas as rodadas, inclusive as reprovadas.
| Rodada | Critical | Important | Minor | Veredicto |
|---|---|---|---|---|
| 1 | 1 | 3 | 1 | Reprovado |
| 2 | 0 | 2 | 3 | Reprovado |
| 3 | 0 | 1 | 2 | Reprovado |
Resultado: três rodadas esgotadas sem aprovação do revisor; levado ao usuário, como manda o /fix, e aprovado por decisão dele em 2026-09-27, com o único Important da rodada 3 resolvido pela via que lhe cabia — a de escopo: os dois SelectionBlock entram. Não há achado em aberto.
O que as três rodadas dizem quando lidas juntas, e é o motivo de parar: nenhuma delas contestou o mecanismo do defeito. As três acharam a mesma coisa — a varredura estava incompleta —, e cada uma trouxe um ponto afetado a mais, por um eixo que a anterior não tinha: o DocumentsPanel (seletor que não é Autocomplete), o segundo seletor de imobiliária da Análise (URL montada à mão), e agora o SelectionBlock (busca no servidor, mas sem paginar). O diagnóstico do bug está estável desde a rodada 1; o que continua se movendo é quantos lugares a correção deve cobrir, que é decisão de escopo e não de entendimento.
O que a rodada 3 achou: um Important — os dois consumidores do SelectionBlock (CreditAnalysisForm.tsx:452 e :457) sem classificação nenhuma, agora descritos na seção O alcance e pendentes de decisão na seção 10. Os dois Minor (a chamada :340 não atribuída a nenhuma das três formas de teto, e apps/web/src/pages/billings/api.ts:175 sem classificação) foram corrigidos.
O que a rodada 1 achou, por classe — três, e as três eram de conferibilidade, não de entendimento do defeito:
- Varredura que casou texto, não componente. A seção O alcance citava dois arquivos como
Autocompleteque não renderizam nenhum, e por isso deixava de fora o consumidor paginado que não éAutocomplete(oDocumentsPanel). Corrigido declarando os dois comandos da varredura e refazendo a lista. - Afirmação geral contradizendo a própria tabela. O mecanismo dizia que "o teto vem de o cliente não mandar
limit", o que é falso em duas das quatro linhas — uma mandalimit: 50e a outra mandalimit: 200, que é o máximo do servidor. Corrigido separando default de máximo, e registrando que na Análise nenhumlimitresolve. - Evidência colhida sob o papel errado. A medição inicial usou
ADMIN_GLOBAL, que não tem filtro de tenant, enquanto a reprodução exige usuário da garantidora. Refeita comoADMIN_TENANTda Garantidora E2E: o total do tenant é 118, e a 51ª parceira continua fora da página que o app carrega.
O que a rodada 2 achou, por classe — duas, e nenhuma sobre o mecanismo:
- A varredura ainda tinha um furo, e ele estava dentro do escopo. Os dois eixos declarados não alcançavam consumidor que monta a query string à mão — era o caso do segundo seletor de imobiliária da Análise (
CreditAnalysisForm.tsx:340). Corrigido com um terceiro eixo, e o seletor entrou no escopo por decisão do usuário. - Classificação errada do teto, contradizendo a citação vizinha. O registro chamava o
limit: 50de Imóveis de "máximo do servidor" duas linhas depois de citarpartner_agencies.py:48, que dizle=200. Cinquenta é escolha do cliente; só a linha da Análise tem teto que o cliente não levanta.
Os três Minor (o total 119 da medição descartada, a contagem "três pontos" sobre uma tabela de quatro linhas, e o <Select> do DocumentsPanel em :934 e não :930) foram corrigidos junto.
Pendências aceitas: —
6. Correção¶
A causa é que nenhum dos sete seletores fazia uma segunda requisição. A correção dá a eles uma só maneira de pedir, que pagina: digitar busca no banco (search), rolar até o fim pede a página seguinte (skip), até acabar o total.
apps/api¶
A paginação a API já oferecia (skip, limit, search, total). O que muda, por decisão do usuário na §10, é o que a busca casa: a de parceiras passa a olhar também o trade_name, e a de parceiras e a de inquilinos passam a ignorar acento, com a extensão unaccent do PostgreSQL criada por migration (com downgrade). É contrato de busca, e não de forma: os parâmetros e a resposta não mudam.
app/repositories/busca.py(novo):contem_sem_acento(coluna, termo)—unaccent()na coluna e no termo,ilike, curingas escapados.partner_agency_repo.py: a busca casa razão social ou nome fantasia, sem acento (e o CNPJ, como antes). Passa também a escapar%e_, como a de inquilinos já fazia: antes, buscar%devolvia todas as parceiras do escopo.renter_repo.py: o nome passa a casar sem acento (CPF, CNPJ e e-mail como antes).- Migration
n2u4x6z8b0d2_busca_sem_acento:CREATE EXTENSION IF NOT EXISTS unaccent, comdowngradeque a derruba. O rolegarantorado dev-remote não é superusuário, mas temCREATEno banco, eunaccenté extensão confiável — ele a cria. tests/conftest.py: o SQLite da suíte ganha uma funçãounaccent(NFKD sem as marcas).- Swagger: as descrições de
GET /partner-agencieseGET /rentersdizem o que a busca casa. - Efeito além dos seletores, e esperado: as listas de Imobiliárias e Inquilinos, que usam os mesmos endpoints, passam a achar pelo nome fantasia e sem acento também.
apps/web¶
shared/hooks/useOpcoesPaginadas.ts(novo): o gancho que guarda termo, opções etotal; recomeça do zero quando o termo muda (com espera de 300ms), pede a próxima página porskipemcarregarMais, descarta resposta fora de ordem, não anexa nada enquanto uma busca nova espera para sair, não duplica opção, e zera o termo quando o campo deixa de existir. Junto, três peças pequenas:aoRolarPertoDoFim(disparacarregarMaisa 48px do fim),propsDeSeletorPaginado(o que umAutocompleteprecisa:filterOptionsdesligado, termo indo para a busca e voltando a vazio ao escolher,onScrollnolistbox) ecomAEscolhida(a opção escolhida continua na lista mesmo fora da página — CA3).pages/properties/api.ts:partnerAgenciesApi.pagina(), que devolve{ items, total }. Olist()descartava ototal, e com ele a única pista de que a lista não acabou.- Inquilinos (
pages/renters/form.tsx): o seletor de parceira usa o gancho. - Imóveis: o formulário (
pages/properties/form.tsx) e o filtro da lista (pages/properties/index.tsx) trocam a busca própria pelo gancho. No formulário, a parceira do imóvel em edição passa a vir sempre pelo id; "o tenant tem parceiras?" continua se respondendo na lista sem busca. - Análise de Crédito (
pages/credit-analyses/CreditAnalysisForm.tsx): o seletor de imobiliária deixou de ser umSelectFieldalimentado por uma carga única de 200 e virou umAutocompletepaginado, com o mesmodata-testid; "Todas as imobiliárias" é o campo vazio. O corresponsável usa o gancho sobrerentersApi.list, pedido só quando a linha de acréscimo abre. As buscas de inquilino e de imóvel devolvem a página com ototal. SelectionBlock:onSearchrecebe oskipe pode devolver{ items, total }; a caixa de resultados pede a página seguinte ao chegar ao fim. Devolvendo só a lista, o bloco entende que ela acabou. A propriedaderecorte(no imóvel, a imobiliária escolhida) recomeça a busca quando muda.- Teste:
test/render.tsxganhourolarAteOFim, que fixa a geometria de "chegou ao fim" no elemento e dispara a rolagem (o jsdom não tem layout).
apps/e2e¶
- Teste de regressão:
tests/e2e/seletor-parceira-paginado.spec.tscria 51 parceiras pela API (a faxina as remove) e percorre, em Inquilinos, digitar o nome da mais antiga e rolar a lista até passar de 50 — no desktop e em 375px, conferindo que a tela não rola na horizontal. credit-analyses-form-steps.spec.ts: a escolha de imobiliária pegava a segunda opção porque a primeira era "Todas as imobiliárias"; agora pega a primeira.
7. Risco / impacto colateral¶
Não toca dinheiro: nenhuma das três telas calcula preço, cobrança ou sinistro. O risco é de regressão de escopo: o campo só aparece para quem é da garantidora, e a API responde apenas a própria parceira ao usuário de imobiliária (spec 0034) — paginar não pode virar caminho para uma parceira enxergar outra. A prova disso está na §12 (CA4).
Risco de deploy, que a mudança na API trouxe: a busca de parceiras e a de inquilinos passam a chamar unaccent(). Subir o código sem aplicar a migration n2u4x6z8b0d2 quebra as duas buscas (function unaccent(text) does not exist) — e com elas as listas de Imobiliárias e Inquilinos e todos os seletores deste fix. Em produção, a migration é passo do /ship, antes de a imagem nova atender. O downgrade derruba a extensão e volta junto com o código anterior.
8. Critérios de aceite¶
- [x] CA1. Seguindo os passos da seção 2, a parceira fora das 50 primeiras aparece ao ser digitada, em Inquilinos.
- [x] CA2. Rolando até o fim da lista, em cada uma das sete chamadas da tabela da seção 4, ela carrega mais em vez de parar — até acabarem os registros.
- [x] CA3. A opção já selecionada continua visível mesmo quando não está na página corrente do resultado.
- [x] CA4. Um usuário de imobiliária não alcança nenhuma parceira além da própria por nenhum dos dois caminhos (rolagem ou busca).
- [x] CA5. Existe teste que falha se a busca no servidor for removida.
- [x] CA6. O texto que o rótulo do seletor mostra acha a parceira: digitar o nome fantasia a acha, e a busca de parceiras e de inquilinos ignora acento ("Imobiliaria" acha "Imobiliária", "Joao" acha "João").
9. Plano¶
- [x] Escrever o teste que reproduz o bug e vê-lo falhar
- [x] Corrigir a causa raiz —
apps/web, com o seletor compartilhado - [x] Red-green verificado: passa → reverter → falha → restaurar → passa
- [x]
make test-webinteiro verde (447 casos),npx tsc --noEmitlimpo,make preflightlimpo - [x] Validar no dev-remote, depois de
/devremote-rebuild, em desktop e mobile - [x] API (decisões 5 e 6): busca de parceiras pelo nome fantasia, busca de parceiras e inquilinos sem acento, com teste antes (red-green no container do dev-remote) e Swagger atualizado
- [x] Migration
n2u4x6z8b0d2(extensãounaccent), aplicada no dev-remote e com odowngradeexercitado; em produção, passo do/ship
10. Decisões em aberto¶
Nenhuma. As três que houve foram decididas pelo usuário em 2026-09-27:
- Escopo: as três telas que reproduzem — Inquilinos, Imóveis e Análise de Crédito.
- Comportamento: rolagem infinita no dropdown, com busca no servidor ao digitar; é o único dos três desenhos cogitados que atende quem não sabe o nome.
-
O quinto seletor (o de imobiliária da Análise, que hoje não falha) entra, por estar na mesma tela e ser a mesma entidade. O
DocumentsPanelfica de fora, como dívida nomeada. -
Os dois
SelectionBlockda Análise (CreditAnalysisForm.tsx:452e:457) entram. Trazidos pela rodada 3 do portão; já buscam no servidor, mas não paginam — é o mesmo que falta em Imóveis, que já estava incluído. A regra que sai daqui, e que a correção materializa: seletor de recurso paginado pagina. - A busca de parceiras olha o nome fantasia (decidido em 2026-09-28, depois da rodada 5 da revisão da correção): a API acrescenta o
trade_nameà busca, e o seletor da Análise passa a mostrar "nome (fantasia)", como os outros. - A busca ignora acento, em parceiras e inquilinos (2026-09-28): resolve-se neste fix, com a extensão
unaccente migration — o usuário escolheu isto em vez de deixar como dívida.
11. Revisão da correção¶
| Rodada | Critical | Important | Minor | Veredicto |
|---|---|---|---|---|
| 1 | 0 | 2 | 4 | Reprovado |
| 2 | 0 | 2 | 4 | Reprovado |
| 3 | 0 | 1 | 3 | Reprovado |
| 4 | 0 | 1 | 3 | Reprovado |
| 5 | 0 | 1 | 4 | Reprovado — [REGISTRO INCOMPLETO] |
| 6 | 0 | 0 | 4 | Aprovado |
O que a rodada 1 achou, por classe — duas, as duas defeitos que a própria correção criou no gancho novo, e nenhuma sobre a causa:
- Termo preso. O termo digitado sobrevivia ao campo: fechada e reaberta a linha de participante, o campo vinha vazio mas a lista era o resultado da busca velha, e o filtro da tela a esvaziava — "Nenhuma pessoa disponível", a mesma mensagem mentirosa deste fix. Mais brando nos outros seletores: escolhida uma opção, a lista seguia filtrada pelo que se digitou. Corrigido: o termo zera quando o campo deixa de existir (
ativofalso) e quando o MUI troca o texto pelo rótulo da escolhida (reason: "reset"). - Corrida entre "carregar mais" e busca nova, no gancho e no
SelectionBlock. Durante os 300ms de espera de uma busca nova, rolar até o fim pedia a página seguinte da lista velha com o termo novo, e ela era anexada atrás da primeira página da busca nova — deixando a faixa do meio inalcançável. Corrigido: a busca nova fica "em voo" desde que o termo muda, e não só quando a requisição sai.
Os quatro Minor: o gancho sem teste do que promete (agora tem: corrida, termo, total, duplicata); o corresponsável sem caso de rolagem (agora tem); o filtro da tela esvaziando a página do corresponsável e parando a lista (agora a página seguinte é pedida sozinha, com teste); e a §7 prometendo um negativo de autorização que o diff não trazia (o registro agora diz de onde vem a prova do CA4, na §12).
Red-green da rodada: os três casos novos do gancho (corrida, termo ao fechar, termo ao escolher) deram 3 failed, 4 passed antes da correção e 7 passed depois. O caso da corrida no SelectionBlock, com a correção desfeita, 1 failed, 6 passed; refeita, 7 passed. O do corresponsável com página esvaziada, sem o pedido automático, 1 failed, 16 passed; com ele, 17 passed. Suíte inteira do web verde no make preflight.
O que a rodada 2 achou, por classe — a mesma da rodada 1, e por isso as correções foram isoladas, uma verificação cada, como manda o /fix quando a rodada acha o defeito que a anterior criou:
- A correção do termo preso não alcançava o caminho real. Ela zerava o termo na razão
"reset", que o MUI 9.2.0 não emite ao escolher uma opção ("selectOption") nem ao sair do campo ("blur") — e o teste chamava o handler direto com a razão escolhida à mão, então passava com o defeito vivo. Corrigido: só a digitação é busca, qualquer outra razão zera o termo. O teste agora monta umAutocompletede verdade (shared/hooks/useOpcoesPaginadas.seletor.test.tsx), escolhe uma opção e sai do campo; antes da correção, 2 failed; depois, 2 passed. - Página de um recorte anexada à lista de outro, no
SelectionBlockde imóvel: trocada a imobiliária com a busca aberta, rolar pediaskip=20do recorte novo e o anexava aos 20 do velho. Corrigido com a propriedaderecorte(a imobiliária), que recomeça a busca do zero. Antes, 1 failed; depois, 8 passed no arquivo.
Os Minor, também um por vez: o hasAgencies do formulário de Imóveis lia o total de uma busca velha e desmontava o painel ao limpar uma busca sem resultado (o gancho agora diz de qual busca são as opções, buscaCarregada; o teste confere que o elemento do campo não é remontado — antes, 1 failed); o corresponsável sem comAEscolhida (agora tem; sem ele, 1 failed); a opção chave sem consumidor (saiu, e com ela o carregado); e o skip contado sobre a lista sem repetidas, que prenderia a posição numa página só de repetidas — no gancho e no bloco, agora é o que o servidor já entregou (antes, 1 failed em cada; depois, verde).
Dois testes desta rodada nasceram verdes pelo motivo errado e foram reescritos antes de valerem como prova: o do painel desmontando (o painel some e volta dentro da espera; passou a conferir que o elemento original continua conectado) e o da escolhida (a asserção rodava antes de a busca nova responder, com a lista velha na tela; passou a esperar o pedido).
Depois da rodada: make preflight com a suíte do web verde, tsc limpo, rebuild do web (bundle index-Ch3a6yEt.js) e os nove specs do e2e — 58 passed, 1 skipped (o semeador).
O que a rodada 3 achou — um Important, e ele também é defeito que a correção anterior criou (o pedido automático do corresponsável, da rodada 1). Por isso, e pela regra do /fix para esse caso, a rodada foi tratada isolada, uma correção e uma verificação por vez:
- Laço de requisições quando a página seguinte falha. O pedido automático não tinha lista de dependências e o erro era engolido: com uma falha persistente — ou sem rede, quando o
fetchrejeita na hora —, ele repetia o pedido a cada render, sem limite. Corrigido: o gancho guarda que a última página falhou (falhou), e o pedido automático para ali; rolar tenta de novo, uma busca nova zera. Antes, o teste contou 2 pedidos da página seguinte em 300ms; depois, 1.
Os três Minor: o pedido automático só cobria a lista vazia, e uma lista curta demais para rolar também parava (agora vale abaixo de oito opções, com teste — antes, 1 failed); a escolha automática da imobiliária única refazia a escolha sempre que o termo esvaziava, desfazendo o "todas" de quem limpou o campo (agora é decidida uma vez, com teste — antes, 1 failed, o campo voltava a mostrar a imobiliária); e o caminho de erro da primeira página deixava buscaCarregada com a busca anterior, e um total zero da falha seria lido como "o tenant não tem parceiras" (agora fica nulo, com teste — sem a correção, 1 failed, 7 passed; com, 8 passed).
Depois da rodada: make preflight com a suíte do web verde, tsc limpo, rebuild do web (bundle index-BH8X8Ghj.js) e os nove specs do e2e — 58 passed, 1 skipped (o semeador).
As rodadas 4, 5 e 6 existem por decisão do usuário (2026-09-27 e 2026-09-28). As três rodadas reprovaram por defeitos que a correção anterior criou — a regra do /fix não os conta contra o limite, mas estourar três é decisão de quem pediu o fix, e ele escolheu mais uma rodada em vez de aceitar sem aprovação.
O que a rodada 4 achou — um Important de outra classe, regressão colateral: a carga das imobiliárias da Análise de Crédito perdeu o tolerate403 que a carga antiga tinha. Um perfil personalizado da garantidora sem partner_agencies:list (spec 0041) abria a análise com o seletor vazio; com o gancho, o 403 do GET mandava a aplicação inteira para /unauthorized. A §7 olhava só o isolamento e não viu isto. Corrigido: partnerAgenciesApi.pagina aceita as opções do apiReq, e a Análise pede com tolerate403. O teste troca o window.location por um dublê e afirma que ninguém é mandado a lugar nenhum; antes da correção, ele registrou o destino /unauthorized.
Os Minor: o limite fixo de oito opções do pedido automático não garantia rolagem numa tela alta (agora sai da altura da janela, porque o listbox do MUI tem 40% dela — com teste a 1080px, antes 1 failed); e o gancho desativado zerava só o termo, e as opções da última busca reapareciam na reabertura até a nova responder (agora zera tudo — com teste, antes 1 failed).
Pendência aceita: a página seguinte que falha não aparece para quem usa — a lista parece terminada, sem sinal de que rolar de novo tenta outra vez. Não esconde a causa nem impede o acesso (a próxima rolagem refaz o pedido), e mostrar a falha pede desenho de interface que não é deste fix.
Depois da rodada: make preflight com a suíte do web verde, tsc limpo, rebuild do web (bundle index-3PfB_iUB.js, container recriado às 23:14 UTC) e os nove specs do e2e — 58 passed, 1 skipped (o semeador).
O que a rodada 5 achou — [REGISTRO INCOMPLETO]: trocar o filtro do cliente pela busca do servidor mudou o que acha um registro. O filtro antigo casava o rótulo inteiro, com o nome fantasia e sem acento; a busca da API só name e CNPJ, com acento. Resultado: "Nenhuma imobiliária encontrada." por outro caminho. Pela regra do /fix, saiu do laço e voltou ao registro (§4, O que trocar o filtro...), e o usuário decidiu (§10, itens 5 e 6) resolver os dois aqui — o que trouxe a API para o fix. Red-green da API, no container do dev-remote: revertidos os dois repositórios, os quatro casos novos (nome fantasia, acento nos dois sentidos, fantasia sem acento, inquilino sem acento) deram 4 failed; restaurados, 4 passed. O seletor da Análise passou a mostrar "nome (fantasia)" (teste novo; antes, 1 failed).
Os Minor da rodada, um por vez, cada um com teste que falhou antes da correção: a lista da busca anterior ficava na tela, clicável, durante a espera de uma nova (agora sai na hora); o pedido automático do corresponsável que falha não tinha saída nenhuma, porque não havia o que rolar (agora a tela diz que falhou e o que fazer — a pendência aceita na rodada 4 fica menor); em edição de imóvel, salvar antes de a parceira chegar mandava o vínculo vazio (agora o vínculo que o imóvel já tinha vai junto até ela chegar); e a constante nova estava entre a documentação do SelectField e o componente (movida).
Suíte inteira da API no container: 1227 passed, 1 failed — o test_busca_alcanca_quem_esta_fora_da_primeira_pagina, intermitente documentado no CLAUDE.md (desempate por UUID no SQLite). Isolado, cinco execuções: 4 verdes e 1 vermelho, o cara-ou-coroa de sempre; o diff só acrescenta casos ao fim do arquivo, e a asserção que falha é do caso antigo.
Depois da rodada: make preflight limpo (Swagger completo, downgrade real, suíte do web verde), tsc limpo; rebuild de api, worker e web (bundle index-W1kNCKgv.js), com o entrypoint da api aplicando m1t3w5y7a9c1 -> n2u4x6z8b0d2. O downgrade foi exercitado no dev-remote: desceu a m1t3w5y7a9c1 e a extensão sumiu; subiu de novo e ela voltou. E2E dos nove specs das telas tocadas, com um caso novo do CA6 (nome fantasia digitado sem acento) no spec do fix — 59 passed, 1 skipped (o semeador). Como a busca mais larga também muda a lista de Imobiliárias, o agencies.spec.ts rodou em seguida: 5 passed, 1 skipped (o semeador em massa, já test.skip).
Antes desse verde, o setup de autenticação do e2e falhou em várias execuções seguidas, com "Erro de conexão com o servidor" na tela de login. Não é deste fix, e a evidência é direta: o login pela API respondeu 40 de 40 vezes em sequência (direto e pelo nginx, 0,25–0,45s); uma sonda com o navegador registrou as requisições abortadas com net::ERR_NETWORK_CHANGED, inclusive as de fontes e telemetria de terceiros; e os eventos do Docker mostram outro projeto da VPS subindo e destruindo containers PostgreSQL de testcontainers a cada poucos segundos. Cada container novo cria e remove uma interface de rede, e o Chrome aborta o que está em voo. Encerrada aquela suíte (09:22:33 UTC), o setup passou duas vezes seguidas e o spec de Imobiliárias ficou verde.
Resultado: aprovado na rodada 6, sem Critical nem Important. As rodadas 4 a 6 existem por decisão do usuário, depois de as três primeiras reprovarem por defeitos que a correção anterior criou. Os quatro Minor da rodada 6 foram resolvidos: a documentação da migration justificava um cenário ("a extensão pode já existir") que o downgrade trata errado — agora diz que ele assume ter criado a extensão, num banco exclusivo; a do shim do SQLite diz que ele só equivale ao unaccent do PostgreSQL nos diacríticos latinos; a busca de parceiras ganhou teste do curinga escapado (sem a correção, 1 failed; com, 1 passed); e este registro ganhou o CA6 marcado, a API e a migration no plano (§9) e o risco de deploy da migration na §7.
O teste sobreviveria à reversão? Não. Revertidos para a main os seis arquivos de produto (properties/api.ts, renters/form.tsx, properties/form.tsx, properties/index.tsx, SelectionBlock.tsx, CreditAnalysisForm.tsx) e removido o gancho, com os testes mantidos, os cinco arquivos de teste deram 9 failed, 37 passed: os nove casos novos, um por caminho — Inquilinos (digitar e rolar), Imóveis (formulário e filtro), SelectionBlock, e na Análise a imobiliária, o corresponsável e as buscas de inquilino e de imóvel. Restaurada a correção, 46 passed.
Como cada caso falhou na reversão: os de opção fora da página, com Unable to find role="option" and name "Imob 051" (ou Pessoa 201, ou o …-option-r-21/p-21 do bloco); o da imobiliária da Análise, com Unable to find role="listbox" — o SelectField antigo não abre uma lista rolável sob aquele data-testid; o do SelectionBlock, com results.map is not a function, porque o bloco antigo não sabia receber uma página.
O CA3 tem teste próprio, em shared/hooks/useOpcoesPaginadas.test.ts: tirado de comAEscolhida o acréscimo da escolhida, o caso "a escolhida fora da página entra na frente das opções" falha (1 failed, 2 passed); restaurado, 3 passed.
Defeitos aceitos: —
12. Registro¶
- Os dados de reprodução da §2 mudaram depois do diagnóstico. Em 2026-09-27, na limpeza do acúmulo que o fix 0022 deixou (a faxina do e2e parada desde 2026-09-07), a Garantidora E2E voltou a 6 imobiliárias e perdeu a maior parte dos inquilinos. O
Imob 0025 1789386969307da §2 não existe mais. Por isso o teste e2e cria as 51 parceiras de que precisa, em vez de depender do que houver no banco. - Prova em execução (dev-remote, depois do rebuild do
web, bundleindex-ChcoFBNL.js): os specs das três telas tocadas mais o novo —seletor-parceira-paginado,renters,renter-form-mobile,properties,renters-properties-list,credit-analyses-form-steps,credit-analyses,credit-analyses-mobile,isolamento-imobiliaria— deram 57 passed e 1 skipped (o semeador em massa de imóveis, que já eratest.skip). Acrescentado depois o caso de 375px ao spec novo, ele rodou sozinho: 4 passed (osetupe os três casos). Depois das correções da rodada 1 da revisão, novo rebuild (bundleindex-B4EqASZc.js) e os mesmos nove specs de novo: 58 passed e 1 skipped (o mesmo semeador). - CA4 (isolamento): o campo segue inexistente para quem é de imobiliária, e os ganchos são criados com
ativofalso para esse usuário — nenhuma chamada a/partner-agenciessai da tela dele. Oisolamento-imobiliaria.spec.ts, que confere que o seletor não aparece para oAGENCY_USER, passou. A API continua respondendo só a própria parceira a ele (spec 0034). - Divergiu do plano: o seletor de imobiliária da Análise mudou de componente (
SelectField→Autocomplete), porque umSelectnão tem como buscar nem rolar para mais. Odata-testidficou; um passo do e2e que contava com "Todas as imobiliárias" como primeira opção foi ajustado. - Achado no caminho, fora do escopo: na lista de Imóveis, o nome da parceira em cada linha sai das parceiras que a tela já carregou (
agencyNameOf), então some para quem nunca apareceu no filtro. Com o gancho, as opções encolhem a cada busca, e a linha perderia o nome junto; para não piorar isso, os nomes vistos ficam num mapa que não encolhe. O conserto de verdade — o nome vir com o imóvel — não entra neste fix. - Dívida nomeada, como decidido na §10: o
DocumentsPanelsegue pedindo os tipos de documento sem paginar. - A correção tem migration (
n2u4x6z8b0d2, a extensãounaccent). No dev-remote ela é aplicada pelo entrypoint daapino rebuild; em produção, é passo do/ship— subir o código sem migrar deixaria as buscas de parceiras e inquilinos quebradas (function unaccent(text) does not exist).