You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
orderBy: [categoryGroups.sortOrder] no SQL e, na linha seguinte, .sort((a,b) => decryptField(a.name).localeCompare(decryptField(b.name),'pt-BR')) — descarta o ORDER BY inteiro
/categorias — app/(app)/categorias/page.tsx:21. É a própria tela que renderiza as setas (:8 importa, :60 renderiza <ReorderButtons>)
constgroups=awaitdb.query.categoryGroups.findMany({where: eq(categoryGroups.userId,userId),with: {categories: true},orderBy: [categoryGroups.sortOrder],// ← pedido ao Postgres})returngroups.sort((a,b)=>decryptField(a.name,dek).localeCompare(decryptField(b.name,dek),'pt-BR'))// ↑ re-ordena tudo por nome; o ORDER BY acima não sobrevive a esta linha
O ciclo fecha em nada, e o no-op é total, para toda entrada — não é "às vezes não funciona":
app/(app)/categorias/page.tsx:24 monta groupIds a partir de groups, que já veio ordenado alfabeticamente.
ReorderButtons.tsx:20-23 troca dois ids adjacentes dessa lista e chama reorderCategoryGroups(newOrder).
A action grava sortOrder = 0..n-1 na ordem pedida e chama revalidatePath('/categorias').
A página re-lê por getCategoriesWithGroups, que re-ordena por nome. A saída é byte a byte a mesma de antes do clique.
Não há estado otimista mascarando: ReorderButtons é startTransition(() => reorderCategoryGroups(newOrder)), sem useOptimistic nem estado local de ordem. O usuário clica, o botão pisca (isPending), o servidor grava, a tela volta idêntica.
Agravante para quem for corrigir: createCategoryGroup (lib/actions/categories.ts:31-37) não atribui sortOrder, e o schema tem default(0) (lib/db/schema.ts:82). Todo grupo criado pela UI nasce com sortOrder = 0 — só os dois grupos do seed (lib/actions/reset-account.ts:37,51) têm 0 e 1. Ver a Proposta.
Verificações feitas (tentativa de falsificar o achado)
Alguma leitura honra sortOrder?grep -rn "sortOrder\|sort_order" lib/ app/ components/ — 4 leituras (tabela acima) e nenhuma. As duas que passam orderBy o anulam na linha seguinte; as outras duas nem pedem ordem.
É decisão deliberada documentada? Não. .claude/crypto.md prescreve "ordenar em JS após decrypt" porque ORDER BY sobre coluna cifrada ordena ciphertext — isso justifica ordenar por nome em JS, e é claramente de onde a linha veio. Mas nenhum .claude/*.md diz que grupo deve aparecer em ordem alfabética, e a existência de sortOrder + reorderCategoryGroups + as setas diz o contrário. O remédio da criptografia foi aplicado sobre o critério errado de ordenação.
git log:lib/queries/categories.ts e components/categorias/ReorderButtons.tsx entram no mesmo commit (98c85f4, import inicial do repo), então não há commit isolado defendendo a escolha — não é uma decisão registrada, é uma colisão que nasceu junto.
Já existe issue? Não. search_issues por sortOrder OR reorder OR reordenar no repo devolve 0 resultados; a listagem completa das 45 issues claude-audit também não tem nada da área.
Impacto
Qualquer usuário com dois ou mais grupos de categoria que tente colocá-los na ordem que faz sentido para ele ("Essencial" antes de "Estilo de Vida", "Fixos" no topo) — a ordenação alfabética é imposta em /categorias e /configuracao-mes, e no /dashboard e /panorama a ordem é a arbitrária do Postgres, que pode inclusive mudar entre dois loads sem que nada tenha sido editado.
O modo de falha é o pior tipo para suporte: não há erro, não há toast, o clique "funciona" (o disabled de isFirst/isLast muda, o isPending pisca) e o banco realmente é atualizado. Só o resultado nunca aparece. Não há como o usuário distinguir isso de "o app é assim".
Cobertura
Hoje: nenhum teste toca o caminho.grep -rn "getCategoriesWithGroups\|reorderCategoryGroups\|sortOrder" __tests__/ devolve uma linha: __tests__/integration/actions-reset-account.test.ts:132, que usa .orderBy(schema.categoryGroups.sortOrder) como ORDER BY próprio para conferir os grupos semeados. Ela não exercita nenhuma leitura de lib/queries/ — a correção certa a deixa verde, sem surpresa para quem implementar.
Caso proposto (integração, __tests__/integration/categories.test.ts), com a entrada que só a correção certa aceita:
Criar dois grupos cuja ordem alfabética seja o inverso do sortOrder: ('Zebra', sortOrder 0) e ('Alimentação', sortOrder 1).
O inverso alfabético é o que dá o poder discriminante: a implementação atual devolve ['Alimentação','Zebra'] e falha; qualquer correção que continue ordenando por nome também falha. Um par em ordem alfabética coincidente passaria nos dois mundos e não cobriria nada.
Segundo caso, no nível da action (__tests__/integration/actions-categories.test.ts): reorderCategoryGroups([idB, idA]) e em seguida getCategoriesWithGroups → esperar [B, A]. É o que amarra escrita e leitura; hoje nada garante que a coluna gravada seja lida por alguém.
Terceiro caso, o que protege a Proposta: criar dois grupos pela actioncreateCategoryGroup e assertar que os sortOrder resultantes são distintos. Sem ele, a correção "só apagar o .sort()" passa nos dois casos acima (que inserem sortOrder explícito) e mesmo assim entrega ordem arbitrária a todo usuário real.
Proposta
Duas partes. A primeira sozinha não resolve — é justamente a armadilha desta issue.
(a) lib/queries/categories.ts — ordenar por sortOrder, com o nome só como desempate:
nas duas funções (getCategoriesWithGroups:21 e getCategoriesWithBudgets:87). O orderBy: [categoryGroups.sortOrder] do SQL pode ficar ou sair — ele já é redundante hoje; o que não pode é continuar sendo anulado em silêncio.
Por que o comparador em JS e não simplesmente apagar o .sort() deixando o ORDER BY do SQL trabalhar (exigência 6): o desempate compara nome decifrado, e o Postgres não tem a DEK — ORDER BY name ordenaria ciphertext (.claude/crypto.md, "ORDER BY quebrado"). E o desempate não é detalhe cosmético aqui: como createCategoryGroup:35 não atribui sortOrder e o schema tem default(0) (schema.ts:82), hoje todo grupo criado pela UI está empatado em 0. Apagar o .sort() — que é o reflexo natural de quem lê o diagnóstico — troca "ordem alfabética teimosa" por "ordem arbitrária que muda entre loads", que é pior, e o teste alfabético-invertido acima passaria nessa versão errada porque ele insere sortOrder explícito. Daí o terceiro caso de cobertura.
(b) lib/actions/categories.ts:31-37 — createCategoryGroup atribui sortOrder no fim da lista (MAX(sortOrder) + 1 do usuário, ou a contagem atual) em vez de deixar o default(0). Sem isso, o empate em 0 faz o desempate alfabético carregar a ordenação inteira e as setas continuam parecendo quebradas até o primeiro clique, que é o único momento em que alguém grava uma sequência 0..n-1 completa.
Fora do escopo desta correção, declarado (exigência 8): os sites 3 e 4 da tabela (getCategoryGroupProgress e getAnnualExpensesByGroup) alimentam /dashboard e /panorama e também ignoram sortOrder. Alinhá-los é a mesma ideia, mas mexe em dois arquivos a mais e no shape de saída do /panorama (que remonta os grupos a partir de um Map), então cabe fatiar. Um PR que feche só os sites 1 e 2 deve usar refs #<n>, não closes.
Custo estimado
P para os sites 1 e 2 — lib/queries/categories.ts e lib/actions/categories.ts (2 arquivos com a parte (b), 1 sem ela). M se /dashboard e /panorama entrarem no mesmo PR.
Onde
Escrita (a única):
lib/actions/categories.ts:58-70—reorderCategoryGroups(orderedIds)gravasortOrder = indexpara cada grupo e revalida/categorias.Leituras de
categoryGroups(4 sites, enumerados por conteúdo — exigência 8). Nenhuma respeitasortOrder:lib/queries/categories.ts:15-22getCategoriesWithGroupsorderBy: [categoryGroups.sortOrder]no SQL e, na linha seguinte,.sort((a,b) => decryptField(a.name).localeCompare(decryptField(b.name),'pt-BR'))— descarta oORDER BYinteiro/categorias—app/(app)/categorias/page.tsx:21. É a própria tela que renderiza as setas (:8importa,:60renderiza<ReorderButtons>)lib/queries/categories.ts:73-88getCategoriesWithBudgetsorderByno SQL (:84), re-sort por nome logo abaixo (:88)/configuracao-mes—app/(app)/configuracao-mes/page.tsx:47lib/queries/dashboard.ts:55-56getCategoryGroupProgressdb.query.categoryGroups.findManysemorderBynenhum e sem sort em JS → ordem arbitrária do Postgres/dashboard(viagetDashboardData)lib/queries/panorama.ts:196-199getAnnualExpensesByGroupfindManysemorderBy/panorama—app/(app)/panorama/page.tsx:36Evidência
lib/queries/categories.ts:15-22:O ciclo fecha em nada, e o no-op é total, para toda entrada — não é "às vezes não funciona":
app/(app)/categorias/page.tsx:24montagroupIdsa partir degroups, que já veio ordenado alfabeticamente.ReorderButtons.tsx:20-23troca dois ids adjacentes dessa lista e chamareorderCategoryGroups(newOrder).sortOrder = 0..n-1na ordem pedida e chamarevalidatePath('/categorias').getCategoriesWithGroups, que re-ordena por nome. A saída é byte a byte a mesma de antes do clique.Não há estado otimista mascarando:
ReorderButtonséstartTransition(() => reorderCategoryGroups(newOrder)), semuseOptimisticnem estado local de ordem. O usuário clica, o botão pisca (isPending), o servidor grava, a tela volta idêntica.Agravante para quem for corrigir:
createCategoryGroup(lib/actions/categories.ts:31-37) não atribuisortOrder, e o schema temdefault(0)(lib/db/schema.ts:82). Todo grupo criado pela UI nasce comsortOrder = 0— só os dois grupos do seed (lib/actions/reset-account.ts:37,51) têm 0 e 1. Ver a Proposta.Verificações feitas (tentativa de falsificar o achado)
grep -rn "ReorderButtons"devolveapp/(app)/categorias/page.tsx:8(import) e:60(render), dentro dogroups.map. Não é código morto, ao contrário do caso da [arquitetura] getMonthlyEvolution roda em todo load do dashboard e o resultado é descartado — MonthlyEvolutionChart não é importado em lugar nenhum #58.sortOrder?grep -rn "sortOrder\|sort_order" lib/ app/ components/— 4 leituras (tabela acima) e nenhuma. As duas que passamorderByo anulam na linha seguinte; as outras duas nem pedem ordem..claude/crypto.mdprescreve "ordenar em JS após decrypt" porqueORDER BYsobre coluna cifrada ordena ciphertext — isso justifica ordenar por nome em JS, e é claramente de onde a linha veio. Mas nenhum.claude/*.mddiz que grupo deve aparecer em ordem alfabética, e a existência desortOrder+reorderCategoryGroups+ as setas diz o contrário. O remédio da criptografia foi aplicado sobre o critério errado de ordenação.git log:lib/queries/categories.tsecomponents/categorias/ReorderButtons.tsxentram no mesmo commit (98c85f4, import inicial do repo), então não há commit isolado defendendo a escolha — não é uma decisão registrada, é uma colisão que nasceu junto.search_issuesporsortOrder OR reorder OR reordenarno repo devolve 0 resultados; a listagem completa das 45 issuesclaude-audittambém não tem nada da área.Impacto
Qualquer usuário com dois ou mais grupos de categoria que tente colocá-los na ordem que faz sentido para ele ("Essencial" antes de "Estilo de Vida", "Fixos" no topo) — a ordenação alfabética é imposta em
/categoriase/configuracao-mes, e no/dashboarde/panoramaa ordem é a arbitrária do Postgres, que pode inclusive mudar entre dois loads sem que nada tenha sido editado.O modo de falha é o pior tipo para suporte: não há erro, não há toast, o clique "funciona" (o
disableddeisFirst/isLastmuda, oisPendingpisca) e o banco realmente é atualizado. Só o resultado nunca aparece. Não há como o usuário distinguir isso de "o app é assim".Cobertura
Hoje: nenhum teste toca o caminho.
grep -rn "getCategoriesWithGroups\|reorderCategoryGroups\|sortOrder" __tests__/devolve uma linha:__tests__/integration/actions-reset-account.test.ts:132, que usa.orderBy(schema.categoryGroups.sortOrder)como ORDER BY próprio para conferir os grupos semeados. Ela não exercita nenhuma leitura delib/queries/— a correção certa a deixa verde, sem surpresa para quem implementar.Caso proposto (integração,
__tests__/integration/categories.test.ts), com a entrada que só a correção certa aceita:sortOrder:('Zebra', sortOrder 0)e('Alimentação', sortOrder 1).getCategoriesWithGroups(userId)→ esperar['Zebra', 'Alimentação'].O inverso alfabético é o que dá o poder discriminante: a implementação atual devolve
['Alimentação','Zebra']e falha; qualquer correção que continue ordenando por nome também falha. Um par em ordem alfabética coincidente passaria nos dois mundos e não cobriria nada.__tests__/integration/actions-categories.test.ts):reorderCategoryGroups([idB, idA])e em seguidagetCategoriesWithGroups→ esperar[B, A]. É o que amarra escrita e leitura; hoje nada garante que a coluna gravada seja lida por alguém.createCategoryGroupe assertar que ossortOrderresultantes são distintos. Sem ele, a correção "só apagar o.sort()" passa nos dois casos acima (que inseremsortOrderexplícito) e mesmo assim entrega ordem arbitrária a todo usuário real.Proposta
Duas partes. A primeira sozinha não resolve — é justamente a armadilha desta issue.
(a)
lib/queries/categories.ts— ordenar porsortOrder, com o nome só como desempate:nas duas funções (
getCategoriesWithGroups:21egetCategoriesWithBudgets:87). OorderBy: [categoryGroups.sortOrder]do SQL pode ficar ou sair — ele já é redundante hoje; o que não pode é continuar sendo anulado em silêncio.Por que o comparador em JS e não simplesmente apagar o
.sort()deixando oORDER BYdo SQL trabalhar (exigência 6): o desempate compara nome decifrado, e o Postgres não tem a DEK —ORDER BY nameordenaria ciphertext (.claude/crypto.md, "ORDER BY quebrado"). E o desempate não é detalhe cosmético aqui: comocreateCategoryGroup:35não atribuisortOrdere o schema temdefault(0)(schema.ts:82), hoje todo grupo criado pela UI está empatado em 0. Apagar o.sort()— que é o reflexo natural de quem lê o diagnóstico — troca "ordem alfabética teimosa" por "ordem arbitrária que muda entre loads", que é pior, e o teste alfabético-invertido acima passaria nessa versão errada porque ele inseresortOrderexplícito. Daí o terceiro caso de cobertura.(b)
lib/actions/categories.ts:31-37—createCategoryGroupatribuisortOrderno fim da lista (MAX(sortOrder) + 1do usuário, ou a contagem atual) em vez de deixar odefault(0). Sem isso, o empate em 0 faz o desempate alfabético carregar a ordenação inteira e as setas continuam parecendo quebradas até o primeiro clique, que é o único momento em que alguém grava uma sequência 0..n-1 completa.Fora do escopo desta correção, declarado (exigência 8): os sites 3 e 4 da tabela (
getCategoryGroupProgressegetAnnualExpensesByGroup) alimentam/dashboarde/panoramae também ignoramsortOrder. Alinhá-los é a mesma ideia, mas mexe em dois arquivos a mais e no shape de saída do/panorama(que remonta os grupos a partir de umMap), então cabe fatiar. Um PR que feche só os sites 1 e 2 deve usarrefs #<n>, nãocloses.Custo estimado
P para os sites 1 e 2 —
lib/queries/categories.tselib/actions/categories.ts(2 arquivos com a parte (b), 1 sem ela). M se/dashboarde/panoramaentrarem no mesmo PR.