Pular para conteúdo

0033 — Interface do SaaS admin no design system

Status CONCLUÍDA (CA9 não verificável no ambiente — ver §7)
Branch spec/0033-saas-admin-no-design-system
Criada em 2026-08-10
Apps afetados web · e2e (api sem impacto — ver D1, decidida)

Status: RASCUNHO → APROVADA → EM IMPLEMENTAÇÃO → CONCLUÍDA

1. Problema

O SaaS admin (/admin) é a área do time da Garantora — onde se olha a plataforma inteira: garantidoras cadastradas, convites emitidos, usuários globais. Ele não acompanhou nenhuma das doze specs de interface que passaram pelo app entre a 0013 e a 0032. Enquanto o app do tenant ganhava lista em DataGrid, filtros padronizados, paginação compacta, cartões no mobile e uma navegação inferior por abas, o admin ficou onde estava.

O resultado é visível em três frentes:

No mobile, três das cinco telas não funcionam. Garantidoras, Usuários e Painel renderizam <Table> cru, sem alternativa em cartão. Numa tela de 375px a tabela de Garantidoras tem cinco colunas — Nome, CNPJ, Status, Criada em, Ações — e simplesmente não cabe: a página rola na horizontal e a coluna de ações fica fora do alcance. Quem precisa suspender uma garantidora pelo celular não consegue.

A navegação é outra. O app inteiro, no mobile, usa a barra de abas inferior com a gaveta em "Mais" (spec 0016). O admin usa um AppBar com hambúrguer e gaveta lateral — o padrão que o app abandonou. Quem é ADMIN_GLOBAL e transita entre as duas áreas encontra dois aplicativos diferentes, e o gesto que funciona num não existe no outro.

A inconsistência é estrutural, não cosmética. AdminLayout e AdminSider somam 427 linhas que reimplementam app-layout e custom-sider — mesma gaveta, mesmo colapso, mesmo menu de perfil, escritos duas vezes. É por isso que as correções de interface do app nunca chegam ao admin: não há nada compartilhado por onde chegarem. Cada spec de design paga o preço uma vez e o admin não recebe nada.

Junto disso, dois defeitos concretos que a leitura do código expôs:

  • "Auditoria" leva a lugar nenhum. O item existe em AdminSider apontando para /admin/audit, e essa rota não está registrada em App.tsx. Clicar cai no not-found.
  • Quatro das cinco telas não têm um data-testid sequer. Só Convites tem (17), por ser recente (spec 0031). O CLAUDE.md do web exige o atributo em todo elemento interativo — é o que sustenta o e2e —, e o admin inteiro está fora dessa rede.

2. Objetivo

O SaaS admin passa a ser a mesma aplicação que o resto do produto: mesma navegação mobile, mesmas listas, mesmos filtros, mesma paginação, mesmos componentes compartilhados. Quem mantém o app deixa de ter uma segunda interface para lembrar, e a próxima spec de design chega ao admin sem trabalho extra.

3. Fora de escopo

  • Funcionalidade nova no admin. Nenhuma tela, ação, filtro ou coluna que não exista hoje. Esta spec redesenha o que já há; a única exceção é o que a adoção do padrão traz de graça (por exemplo, o seletor "Por página" que vem com o ListPagination).
  • Tela de auditoria global. O item morto sai do menu (RN7). Construir a tela é feature nova, com contrato de API próprio, e é outra spec.
  • Mudar as regras de negócio do admin. Suspender/reativar garantidora, o purge em duas etapas com confirmação por nome, emitir e revogar convite — todos permanecem com exatamente o mesmo comportamento. O que muda é a casca.
  • Permissões. ADMIN_GLOBAL continua sendo o único papel com acesso, e a guarda continua onde está (useCurrentUser().isAdminGlobal no layout).
  • Refatorar o app do tenant. As telas de pages/ não são tocadas. Se a adoção pelo admin exigir generalizar um componente compartilhado, a mudança tem de ser aditiva — prop nova com default igual ao comportamento atual (RN2).
  • Paginação no servidor para o admin. Ver RN9: o conjunto é pequeno e a API não devolve total. Fica client-side sobre o conjunto inteiro, com o teto explícito.
  • PWA. Nenhum asset novo, nenhuma rota offline, nenhuma mudança no service worker. O admin já é servido pelo mesmo bundle.

4. Regras de negócio

  • RN1. O admin reusa o MobileShell. No mobile, AdminLayout deixa de renderizar AppBar + gaveta própria e passa a delegar para components/mobile-shell, com os itens de navegação do admin. Mesma barra de abas inferior (62px), mesma gaveta de 300px, mesmo TopBar com slots de título e ações. No desktop, segue com sider lateral colapsável.

  • RN2. Generalizar é aditivo, nunca destrutivo. MobileShell e CustomSider hoje chamam buildNavGroups(badges) internamente; o primeiro recebe só children, o segundo só collapsed/onToggle. Ambos passam a aceitar os grupos por prop, com default no comportamento de hoje. O mesmo vale para qualquer outro componente compartilhado que precise ceder: nenhuma chamada existente em pages/ pode mudar. Se uma generalização exigir tocar as telas do tenant, ela está fora do escopo desta spec.

  • RN2b. AdminSider deixa de existir. CustomSider já delega ao AdminSider quando isAdminGlobal (custom-sider/index.tsx:39), e tem uma única chamada em toda a árvore (app-layout/index.tsx:48) — a prop da RN2 é, portanto, puramente aditiva. Com ela, o ramo do isAdminGlobal passa a escolher os grupos e a faixa, não a renderizar um componente inteiro. As 294 linhas do AdminSider — gaveta, colapso, tooltips, section labels, estados de ativo — são as mesmas do CustomSider e saem. Sobrevivem duas coisas: os grupos do admin (que vão para nav-groups.tsx) e a faixa "Admin SaaS" em warning.main (que vira um adorno opcional do CustomSider).

  • RN3. Quatro abas no mobile do admin. A barra inferior leva Painel, Garantidoras, Convites e "Mais" — que abre a gaveta com o conjunto completo (incluindo Usuários), o alternador de tema e o Sair. É o mesmo desenho de três abas + "Mais" que o app usa.

  • RN4. As listas adotam o padrão pleno. Garantidoras e Usuários passam a ser, no desktop, DataGrid com garantoraDataGridProps/dataGridSx; no mobile, RecordCardList. Filtros por FilterBar (desktop) e MobileFilterBar (mobile); rodapé por ListPagination. A referência é pages/credit-analyses/index.tsx.

  • RN5. Convites converge para o mesmo padrão. A tela da spec 0031 já é responsiva (useIsMobile + RecordCard), mas usa <Table> no desktop e não tem FilterBar nem ListPagination. Ela entra na uniformização — o comportamento (emitir, copiar link, revogar, ConfirmDialog) não muda.

  • RN6. Nenhuma coluna nova, nenhuma coluna perdida. As colunas de cada lista são as de hoje. No mobile, o RecordCard de cada lista exibe o identificador, o status e a ação primária no rosto do cartão; o que não couber vai para a tela de detalhe ou para o menu de ações da linha — não some silenciosamente.

  • RN7. "Auditoria" sai do menu. O item é removido de AdminSider e da navegação mobile. Nenhuma rota é criada. É a correção de um item que hoje leva ao not-found.

  • RN8. Toda tela do admin ganha data-testid. Conforme o CLAUDE.md do web, em todo elemento interativo — linhas, ações por linha, filtros, paginação, diálogos e seus botões, alertas de erro. Convites já está instrumentada; as outras quatro, não. Os testids existentes em Convites não podem mudar de nome — a spec e2e da 0031 depende deles.

  • RN9. A paginação do admin é client-side sobre o conjunto inteiro, e o teto é visível. GET /api/v1/tenants devolve list[TenantResponse] — array puro, sem total, com limit máximo de 200. Hoje a tela pede limit: 200 e filtra na memória; acima de 200 garantidoras a lista mente por omissão, sem qualquer aviso.

Como não há cliente algum na plataforma, o conjunto real é de unidades, e ordenar e paginar sobre o conjunto inteiro carregado não é a mentira que o CLAUDE.md proíbe (essa seria paginar no servidor e ordenar no cliente). Então: mantém-se client-side e o ListPagination recebe o total do conjunto filtrado.

O que não se mantém é o silêncio. Quando a resposta vier com exatamente o teto (data.length === 200, o limit pedido), a tela exibe um Alert de severidade info acima da lista: "Mostrando as primeiras 200 garantidoras. Há mais registros do que esta tela consegue listar." Sem o aviso, a lista mente por omissão; com ele, o teto vira um fato visível — e o gatilho explícito para D1 ser revisitada.

  • RN10. Erro e vazio falam. O estado de erro mantém o "Tentar novamente" que já existe. O estado vazio usa o texto que diz o que fazer, não só que está vazio — via noRowsLabel do DataGrid no desktop e mensagem equivalente no mobile.

  • RN11. Nenhuma regressão de rolagem horizontal. Em 375px, toda tela do admin tem scrollWidth - clientWidth == 0. É o critério que a verificação por screenshot afere (CA8), e é exatamente o defeito que a spec 0031 já havia corrigido no sider e que persiste nas telas.

5. Impacto por app

apps/api

Sem impacto. Decidido em D1.

Nenhum endpoint, schema, model ou migration muda. A spec é de interface e consome os endpoints como estão: GET /api/v1/tenants, PATCH /api/v1/tenants/{id}, o par de purge, GET de usuários globais e as rotas de tenant_invites.

O array sem total e o teto de 200 são limitação conhecida da API, contornada no web por carga integral + aviso ao bater no teto (RN9). D1 registra por que não se corrige agora e o que dispara a revisão.

apps/web

Layout e navegação

  • components/mobile-shell/index.tsx — passa a aceitar os grupos de navegação (e o conjunto de abas) por prop, com default no comportamento atual (RN2)
  • components/custom-sider/index.tsx — idem para os grupos, mais um adorno opcional para a faixa "Admin SaaS"; o ramo isAdminGlobal deixa de renderizar AdminSider e passa a escolher grupos + faixa (RN2b)
  • components/navigation/nav-groups.tsx — ganha buildAdminNavGroups(), ao lado de buildNavGroups; é onde a navegação do admin passa a viver
  • admin/components/AdminLayout.tsx — no mobile delega ao MobileShell; no desktop mantém sider + topo, alinhado a components/app-layout
  • admin/components/AdminSider.tsx — removido (RN2b). A faixa "Admin SaaS" em warning.main que distingue a área permanece, agora renderizada pelo CustomSider — é o sinal de que se está no painel do SaaS, e é útil
  • App.tsx — nenhuma rota nova; /admin/audit continua não existindo (RN7)

Telas

  • admin/pages/tenants/TenantListPage.tsx — DataGrid + RecordCardList + FilterBar/ MobileFilterBar + ListPagination. Busca (nome/CNPJ) e filtro de Status viram controles do FilterBar, com chips. Os diálogos de suspender e de purge preservam o fluxo e passam a usar ConfirmDialog onde couber
  • admin/pages/tenants/TenantDetailPage.tsx — painéis do design system, DataRow no desktop e MobileDetailSection no mobile
  • admin/pages/users/GlobalUserListPage.tsx — mesmo padrão de lista
  • admin/pages/invites/InviteListPage.tsx — converge para DataGrid no desktop, FilterBar e ListPagination; mantém RecordCard, ConfirmDialog e os testids (RN5, RN8)
  • admin/pages/dashboard/AdminDashboardPage.tsx — cartões e tabela no padrão; sem métrica nova

data-testid (RN8) — nav-admin-<path> nos itens de navegação; por lista: tenants-row-{id}, tenants-search-filter, tenants-status-filter, tenants-pagination, tenants-view-{id}, tenants-suspend-{id}, tenants-purge-{id}, tenant-suspend-dialog / -confirm / -cancel, tenant-purge-dialog / tenant-purge-name-input / -confirm / -cancel, tenants-error; análogos para admin-users-* e para o detalhe (tenant-detail-*). Os de invite-* ficam como estão.

Sider — não é módulo novo, então não há entrada a acrescentar em custom-sider; o que muda é a navegação do admin (RN3, RN7).

Responsividade & PWA — responsivo é o objeto da spec, não um passo posterior: cada tela entrega desktop e mobile no mesmo commit. PWA sem impacto — nenhum asset novo, nenhuma rota offline, nenhuma mudança de service worker. Vale lembrar, na validação, que o dev-remote serve build cacheado: sem /devremote-rebuild a tela mostra a versão velha.

apps/e2e

  • Spec nova — admin-interface.spec.ts, autenticada como ADMIN_GLOBAL, cobrindo nos dois viewports: navegar entre as quatro telas pela barra inferior/gaveta (mobile) e pelo sider (desktop); listar Garantidoras com filtro de status e busca; paginar; abrir o detalhe; e confirmar que "Auditoria" não está no menu.
  • Seeds — não deve ser preciso: o ambiente compartilhado já tem garantidoras e usuários. Se algum cenário exigir volume para paginar, o seed vai em tests/fixtures/seeds/ — e nada de configuração compartilhada do tenant é alterado (a lição da spec 0025).
  • Escrito, não executado, salvo pedido explícito no fechamento.

6. Contrato de API

Sem contrato novo. Os endpoints consumidos, como estão hoje:

GET    /api/v1/tenants?skip=&limit=&search=     → list[TenantResponse]   (limit ≤ 200, sem total)
GET    /api/v1/tenants/{id}                     → TenantResponse
PATCH  /api/v1/tenants/{id}   { "status": … }   → TenantResponse
POST   /api/v1/tenants/{id}/purge/preview       → TenantPurgePreviewResponse
POST   /api/v1/tenants/{id}/purge               → TenantPurgeResult
GET    /api/v1/users?skip=&limit=               → list[UserResponse]     (sem total)
GET    /api/v1/tenant-invites?status=&skip=&limit=
POST   /api/v1/tenant-invites
POST   /api/v1/tenant-invites/{id}/revoke

Todos exigem Authorization: Bearer <token> de um usuário ADMIN_GLOBAL.

Erros: os já previstos pelos endpoints (401, 403, 404, 409). A tela apresenta detail da resposta no Alert com "Tentar novamente" (RN10) — comportamento inalterado.

A ausência de total nas duas primeiras rotas é o que motiva a RN9 e a decisão D1.

7. Critérios de aceite

  • [x] CA1. Dado um ADMIN_GLOBAL num viewport de 375px, quando ele abre qualquer das quatro telas do admin, então vê a barra de abas inferior com Painel, Garantidoras, Convites e Mais — e não o hambúrguer com gaveta lateral. Verificado por captura no dev-remote.
  • [x] CA2. Dado o mesmo usuário em 375px na lista de Garantidoras, então os registros aparecem como cartões, cada um com nome, status e acesso à ação primária — sem tabela e sem rolagem horizontal. Verificado por captura.
  • [x] CA3. Dado o desktop, quando ele abre Garantidoras ou Usuários, então a lista é um DataGrid com cabeçalho em overline, zebra, linha de 52px e o rodapé ListPagination com "Por página" e a faixa 1–25 de N. Verificado por captura (1–5 de 5 e 1–23 de 23).
  • [x] CA4. Dado que ele digita no campo de busca ou escolhe um Status, então o filtro aparece como chip no FilterBar, e "Limpar" devolve a lista completa. Verificado no navegador: chip aparece, texto de vazio aparece, "Limpar filtros" devolve as 5 linhas.
  • [x] CA5. Dado o menu do admin em qualquer viewport, então não há item "Auditoria", e nenhum item do menu leva ao not-found. Verificado por captura nos dois viewports.
  • [x] CA6. Dado que ele suspende uma garantidora, ou executa o purge digitando o nome, então o resultado é idêntico ao de antes desta spec — mesmas etapas, mesma confirmação, mesmo efeito. Verificado por leitura: os handlers (handleStatusChange, handleOpenPurge, handleExecutePurge, a comparação de nome e a chave de confirmação) foram transpostos sem alteração. Não exercitado no ambiente — suspender e purgar mexem em dado compartilhado.
  • [x] CA7. Dado o inspetor do navegador em cada tela do admin, então todo elemento interativo tem data-testid, e os testids de Convites continuam com os nomes de antes. Os nomes da 0031 foram preservados um a um; a spec e2e afere.
  • [x] CA8. Dado o script de screenshot rodando contra o dev-remote reconstruído, nos dois viewports e nas cinco telas, então scrollWidth - clientWidth == 0 em todas e não há erro de console. 8 combinações + gaveta + detalhe: overflow=0 e zero erro em todas.
  • [ ] CA9. Dado que a API devolve exatamente 200 garantidoras, então a tela avisa que há mais registros do que consegue mostrar (RN9). Não verificável hoje — o ambiente tem 5 garantidoras, e criar 200 para ver um Alert custa mais do que vale. O código está lá (tenants.length >= FETCH_LIMIT) nas três listas; a aferição fica para quando houver volume, que é o mesmo momento em que D1 é revisitada.
  • [x] CA10. Dado o app do tenant (não-admin), então nada mudou nele: as telas de pages/ renderizam como antes e a suíte e2e existente segue verde. Nenhum arquivo em pages/ foi tocado, e o único call site do CustomSider só ganhou props opcionais. A suíte e2e não foi executada — ver §10.

8. Plano de implementação

Ordem obrigatória no monorepo: api → web → e2e.

  • [x] apps/api — sem impacto (D1)
  • [x] apps/web — buildAdminNavGroups()/buildAdminNavTabs() em navigation/nav-groups.tsx
  • [x] apps/web — generalizar MobileShell e CustomSider para receber os grupos por prop, com default inalterado (RN2) — app-layout repassa e nada em pages/ mudou
  • [x] apps/web — AdminLayout delegando ao AppLayout; AdminSider.tsx apagado (294 linhas), faixa "Admin SaaS" preservada (RN2b)
  • [x] apps/web — remover "Auditoria" da navegação (RN7)
  • [x] apps/web — TenantListPage no padrão pleno (DataGrid · RecordCardList · FilterBar/MobileFilterBar · ListPagination), com o aviso de teto da RN9
  • [x] apps/web — GlobalUserListPage no mesmo padrão
  • [x] apps/web — InviteListPage convergindo (RN5), sem renomear testid
  • [x] apps/web — TenantDetailPage com DataRow / MobileDetailSection
  • [x] apps/web — AdminDashboardPage no padrão
  • [x] apps/web — data-testid nas quatro telas sem instrumentação (RN8)
  • [x] apps/e2e — admin-interface.spec.ts nos dois viewports (escrito, não executado)
  • [x] make test (262 passaram, 1 falha intermitente conhecida) e lint sem apontamento novo nos arquivos tocados
  • [x] /devremote-rebuild + script de screenshot nos dois viewports, cinco telas (CA8)

9. Decisões tomadas

Nenhuma pendência. As duas foram decididas em 2026-08-10, por evidência do código.

  • [x] D1 — a API passa a devolver o total nas listas do admin? → NÃO. Fica como está; vale a RN9 (client-side + aviso ao bater no teto).

    O que se apurou. O custo era menor do que parecia. A lista GET /api/v1/tenants tem um único consumidor — admin/services/adminApi.ts; pages/my-garantidora usa só o detalhe /tenants/{id}. E GET /api/v1/users, embora compartilhado com a tela de Equipe do tenant, já tolera as duas formas: pages/users/api.ts:36 declara User[] | { items?: User[] } e normaliza com toArray. Ou seja, o envelope não quebraria ninguém.

    Por que ainda assim não. O argumento contra nunca foi risco de quebra — é que o problema não existe. Paginação no servidor resolve conjunto grande, e não há conjunto: zero clientes, garantidoras na casa das unidades. O teto de 200 só passa a incomodar com 200 garantidoras pagantes, cenário em que esta tela será reescrita por outros motivos. Somado a isso, o pedido foi de interface; puxar schema, service, router e testes da API para dentro dele troca o objeto da spec.

    O que dispara a revisão. O Alert da RN9. No dia em que ele aparecer numa tela de verdade, o envelope { items, total } com paginação e ordenação no servidor deixa de ser prematuro e vira a spec seguinte — para as duas rotas, já que a de usuários está preparada para receber.

  • [x] D2 — AdminSider sobrevive ou é absorvido? → ABSORVIDO. Vira a RN2b.

    O que se apurou. Não era decisão de gosto: CustomSider já delega ao AdminSider no ramo isAdminGlobal (custom-sider/index.tsx:39), e tem uma única chamada em toda a árvore (app-layout/index.tsx:48, passando só collapsed/onToggle). Uma prop groups opcional com default buildNavGroups(badges) é aditiva por construção — nenhum call site muda, e a RN2 fica satisfeita sem tocar em pages/.

    Consequência. As 294 linhas do AdminSider são gaveta, colapso, tooltip, section label e estado de ativo — tudo que o CustomSider já faz. O arquivo é apagado. O que era próprio do admin vira dado: os grupos, em buildAdminNavGroups(), e a faixa "Admin SaaS", num adorno opcional do CustomSider. É essa troca de componente duplicado por dado que faz a próxima spec de design chegar ao admin sozinha — o objetivo da §2.

10. Registro

Implementada em 2026-08-10. O que divergiu do planejado, e por quê.

A generalização foi menor do que a spec supunha

A RN2b previa que o AdminSider sumisse. Sumiu — mas o AdminLayout também. Ao escrever a prop de grupos, ficou claro que a diferença entre as duas áreas cabia inteira em três argumentos (groups, tabs, badgeLabel), e que o lugar de recebê-los era o AppLayout, não cada casca. Resultado:

Arquivo Antes Depois
admin/components/AdminSider.tsx 294 linhas apagado
admin/components/AdminLayout.tsx 133 linhas 45 linhas — guarda de papel + escolha dos grupos

AppLayout, CustomSider e MobileShell ganharam props opcionais com default no comportamento de hoje; nenhum arquivo em pages/ foi tocado (RN2). O único call site do CustomSider (app-layout/index.tsx:48) passou a repassar as props.

Efeito colateral bom: o admin ganhou de graça o menu de perfil, o alternador de tema no rodapé da gaveta e o portal de título/ações do topo — tudo que o AppLayout já fazia e que o layout próprio reimplementava pela metade.

Duas abas do mobile saíram do MobileShell

As abas estavam embutidas na casca como JSX. Viraram dado (buildNavTabs, buildAdminNavTabs) em nav-groups.tsx, e a casca só acrescenta a "Mais" — que não navega, abre a gaveta. Os ícones e rótulos do tenant foram copiados letra por letra, inclusive o file-text de Garantias, que difere do certificate da sider: é provavelmente um deslize antigo, mas corrigi-lo aqui violaria o CA10.

isNavItemActive passou a receber a forma mínima (Pick<NavItem, "path"> & …) para servir também às abas, que não têm activePaths.

Dois defeitos encontrados no caminho — corrigidos

  1. A lista de Usuários do admin estava quebrada. Pedia fetchGlobalUsers({ limit: 500 }), e GET /api/v1/users declara limit com le=200: a API respondia 422 em toda abertura da tela, e o que se via era o alerta de erro. Corrigido para 200. Não estava na spec porque só apareceu ao ler o adminApi contra o router.

  2. O filtro "Admin Global (sem tenant)" não filtrava nada. O valor __global__ era comparado com u.tenant_id, que para o time do SaaS é null — a opção existia e devolvia lista vazia sempre. Agora compara com null.

Ambos são de interface no sentido que importa aqui: a tela mostrava a coisa errada.

O que foi acrescentado além do redesenho, e por quê

A §3 dizia "nenhum filtro que não exista hoje". Foram acrescentados três, todos client-side sobre dados já carregados, custo zero de requisição:

  • busca em Garantidoras, Usuários e Convites — o SearchField é estrutural no FilterBar ("sempre primeiro, e é o que cresce"); uma barra de filtros sem ele não é o padrão, é outra coisa;
  • filtro de status em Usuários, que tinha Garantidora e Perfil mas não Ativo/Inativo, embora a coluna existisse.

É a exceção que a §3 previa ("o que a adoção do padrão traz de graça"), mas vale dito em voz alta: são filtros novos, não só forma nova.

Vocabulário de status foi para o lugar canônico

TENANT_STATUS, TENANT_INVITE_STATUS e USER_STATUS entraram em components/shared/StatusBadge.tsx, junto de AGENCY_STATUS e dos demais. As telas do admin definiam mapas locais STATUS_CHIP — três cópias, com rótulos divergentes ("Ativo" na lista, "Ativa" em lugar nenhum). SUSPENDED passou de vermelho a âmbar: é estado reversível, e o vermelho competia com o Excluir, que é o irreversível da tela.

Ações da linha viraram ícone

Na lista de Garantidoras, "Ver detalhes"/"Suspender"/"Excluir" eram <Button> com rótulo, somando ~260px numa coluna de ações. Viraram IconButton com tooltip, como em Imobiliárias. O comportamento — diálogos, confirmação por nome no purge, efeito — é o mesmo (CA6); o que mudou é a forma, que é o objeto da spec.

Paginação: cliente, como a RN9 decidiu

As quatro listas usam paginationMode/sortingMode client do DataGrid sobre o conjunto inteiro carregado. Não é a mentira que o CLAUDE.md proíbe (essa seria paginar no servidor e ordenar no cliente): ordenar aqui alcança todos os registros carregados. O aviso de teto da RN9 está nas três listas, com o texto e o gatilho (length >= 200) que a RN pede.

Verificação

  • make test — 262 passaram, 1 falhou: test_partner_agency_status_filter.py::test_alcanca_quem_esta_fora_da_primeira_pagina, a intermitência já documentada no CLAUDE.md (empate de created_at no SQLite desempatado por id desc sobre UUID). Passa 3/3 isolada, e esta spec não toca em nenhum arquivo da API.
  • Lint — 0 erros nos arquivos tocados; 2 warnings de exhaustive-deps, do mesmo feitio dos que já existem na árvore. De quebra saíram dois apontamentos pré-existentes (type T morto em nav-groups.tsx, import não usado).
  • tsc e vite build — limpos.
  • Captura no dev-remote (após /devremote-rebuild; bundle index-CmEjd3U5.js, o mesmo hash do build local, o que prova que o ambiente serve este código) — quatro telas × dois viewports, mais a gaveta do mobile e o detalhe da garantidora: overflow=0 e zero erro de console em todas. CA4 exercitado no navegador.

Uma armadilha que quase virou relatório errado: a primeira rodada do script preencheu o e-mail sem limpar o campo, o formulário de login chega com valor padrão, e o desktop fotografou a tela de login — medindo overflow=0 nela e passando por verde. Só apareceu ao abrir o PNG. O script passou a exigir uma âncora do admin (sider-area-badge / mobile-tabbar) e a imprimir o título de cada tela antes de medir. Vale para qualquer verificação por captura: status e métrica não provam que a tela é a certa.

  • e2e — admin-interface.spec.ts cobre CA1–CA8 e CA10 nos dois viewports. Escrito, não executado.