# Review do front PIX admin (2026-06-24)

Auditoria de 6 áreas do painel ADMIN PIX (`pix/frontend/admin/src`) vs backend (`pix/backend/apps`). Severidades: P0 quebrado, P1 funcional-errado, P2 UX/i18n, P3 polish.



---

## i18n, acentuação pt-br, travessão de IA, tradução


I have all the evidence I need. Here is the audit report.

# Auditoria de i18n, acentuação pt-br e tradução — Painel ADMIN PIX Monetarie

Escopo: `pix/frontend/admin/src` (locales + componentes `.vue`). Foco: acentuação pt-br, travessão de IA, strings cravadas (hardcoded) fora do i18n, chaves faltando e tradução. Cada item traz `arquivo:linha`, severidade, descrição objetiva e correção recomendada. Nada foi alterado, apenas auditado.

## Resumo executivo

- O sistema de i18n existe e funciona, mas **62 de 105 views (59%) ignoram o i18n por completo** e cravam texto pt-br direto no template. Várias dessas strings cravadas estão **sem acento**, inclusive em rótulos de destaque (KPIs do painel de saldos).
- Pior caso recorrente: a chave i18n correta e acentuada **já existe** no `pt-BR.json`, mas o componente crava uma versão errada e sem acento (ex.: "RESERVA BANCARIA" cravado vs `balance.bankReserve` = "RESERVA BANCÁRIA").
- **13 travessões de IA ("—")** dentro de strings do `pt-BR.json`.
- Erro de acentuação recorrente no JSON: demonstrativo "esta" escrito como verbo "está" ("está ação", "está tela", "está lista").
- Os **4 locales secundários (en-US, es-ES, fr, zh-HK) estão selecionáveis no seletor de idioma**, mas cada um cobre só 30 de 53 módulos. Quem trocar o idioma vê metade do app em português (fallback), e o que está traduzido vem **sem acento** no espanhol e no francês.
- O seletor de idioma da tela de Configurações é um controle morto, com código de locale incompatível.

---

## P0 — Quebrado (controle morto / código de locale incompatível)

**1. [`views/settings/SettingsView.vue:62-65`]** — P0
O seletor de idioma da tela de Configurações tem lista própria cravada (`{ value: 'pt-br' }`, `{ value: 'en' }`) que **não está ligada ao vue-i18n** (o arquivo não importa `useI18n` nem `useLocale`, e o `@change` só grava num objeto local de settings, linha 152). Além de não trocar o idioma, os códigos `'pt-br'`/`'en'` não batem com os códigos reais do i18n (`'pt-BR'`, `'en-US'`, `'es-ES'`, `'zh-HK'`, `'fr'`). Resultado: a aba "Idioma" da tela de Configurações não faz nada e nunca reflete o idioma ativo. Correção: remover a lista cravada, importar `useLocale`/`useI18n`, usar `availableLocales` (já definido em `composables/useLocale.ts:17`) e chamar `setLocale` no change. O seletor real e funcional está em `components/layout/Header.vue:44`.

---

## P1 — Funcional-errado (i18n bypass + acento em rótulo de destaque + locales secundários furados)

**2. [`views/balance/BalanceDashboardView.vue:181`]** — P1
KPI cravado "RESERVA BANCARIA" sem acento. A chave correta `balance.bankReserve` = "RESERVA BANCÁRIA" já existe no `pt-BR.json:1855`. Correção: usar `{{ t('balance.bankReserve') }}`.

**3. [`views/balance/BalanceDashboardView.vue:211`]** — P1
KPI cravado "PIX INSTANTANEO" sem acento. Chave correta `balance.pixInstant` = "PIX INSTANTÂNEO" (`pt-BR.json:1856`). Correção: `{{ t('balance.pixInstant') }}`.

**4. [`views/balance/BalanceDashboardView.vue:241`]** — P1
KPI cravado "SALDO DISPONIVEL TOTAL" sem acento. Chave correta `balance.totalAvailable` = "SALDO DISPONÍVEL TOTAL" (`pt-BR.json:1857`). Correção: `{{ t('balance.totalAvailable') }}`.

**5. [`views/balance/BalanceDashboardView.vue:345`]** — P1
Texto cravado "Injecao de liquidez" sem acento (deveria ser "Injeção"). Existe `balance.liquidity.title` = "Injeção de Liquidez" (`pt-BR.json:1853`). Correção: corrigir para "Injeção" e idealmente migrar para i18n.

**6. [62 views, ex.: `views/balance/`, `views/reports/`, `views/accounting/`, `views/alcada/`, `views/participants/`, `views/keys/`, `views/sessions/`, `views/statements/`, `views/qrcodes/`, `views/simulator/`, `views/settings/`, `views/RecurrenceListView.vue`]** — P1
62 de 105 views não usam `$t`/`useI18n` e cravam todo o texto pt-br no template (10 a 36 strings por arquivo). Mesmo com o `pt-BR.json` cobrindo grande parte do vocabulário, esses textos nunca passam pelo i18n: não traduzem ao trocar o idioma e fogem do controle de qualidade central. Exemplos concretos cravados: `views/settings/SettingsView.vue:17-20,92,100,108,278,294,310` ("Preferências", "Notificações", "Configurações", "Personalize sua experiência...", "Restaurar Padrão", "Notificações por Email"...); `views/RecurrenceListView.vue:78,135,283` ("Diário", "Nova Recorrência", "Próximo"); `views/transactions/TransactionListView.vue:291,297` ("Anterior", "Próximo"). Correção: migrar para chaves i18n (boa parte já existe, ex.: `common.previous`, `common.next`, `settings.*`).

**7. [`src/locales/en-US.json`, `es-ES.json`, `fr.json`, `zh-HK.json` (todos)]** — P1
Os 4 locales secundários estão expostos no seletor de idioma (`components/layout/Header.vue:44-48` e `composables/useLocale.ts:17-22`), mas cada um cobre só **30 de 53 módulos**. Faltam por completo (caem em fallback pt-br): `transactions`, `keys`, `claims`, `qrcodes`, `balance`, `certificates`, `cert`, `sessions`, `statements`, `settings`, `automation`, `scopes`, `wizard`, `forms`, `pixMessages`, `systemHealth`, `commandPalette`, `apiClients`, `pix_automatico`, `xml`, `infraction`, `claim`, `refund`. Resultado: ao escolher English/Español/Français/中文, telas inteiras (Transações, Chaves, Saldos, Certificados, Configurações, devoluções/claims do MED) aparecem em português misturado com o idioma escolhido. Correção: completar os 23 módulos faltantes em cada locale, ou remover do seletor os idiomas que não estão completos até serem traduzidos.

**8. [validações cravadas em `views/balance/`]** — P1
Mensagens de validação de formulário (mostradas ao usuário) trocam o verbo "é" pelo conectivo "e", invertendo o sentido:
- `views/balance/DebitRequestView.vue:106` "Valor mínimo e R$ 1.000,00" (deveria ser "é R$").
- `views/balance/CreditRequestView.vue:50` "Valor mínimo e R$ 1.000,00"; `:52` "Valor máximo e R$ 100.000.000,00".
- `views/balance/LiquidityInjectionView.vue:94` "Valor mínimo para aporte e R$ 1.000.000,00"; `:105` "Origem dos recursos e obrigatória"; `:112` "Data prevista e obrigatória".
Correção: trocar "e" por "é" nessas mensagens.

---

## P2 — UX / i18n / acentuação visível ao usuário

### 2.a — Travessão de IA ("—") no `pt-BR.json`

**9. [`src/locales/pt-BR.json:692,693,694,695`]** — P2
Travessão de IA "—" em `messages.types.CAMT060.useCases` ("Saldo do momento — saldo mais recente...", "Saldo em data anterior — fechamento...", "Lançamento — detalhe...", "Relação de lançamentos — janela..."). Correção: trocar por dois-pontos, vírgula ou parênteses (ex.: "Saldo do momento: saldo mais recente da Conta PI").

**10. [`src/locales/pt-BR.json:849`]** — P2
`security.mfa.finishSetup` = "Salvei meus códigos — Concluir" usa travessão de IA. Correção: "Salvei meus códigos, concluir" ou "Salvei meus códigos. Concluir".

**11. [`src/locales/pt-BR.json:1394,1395,1396,1397,1398,1399`]** — P2
`transactions.list.options.msgType*` usam travessão ("pacs.008 — Pagamento", "pacs.002 — Confirmação", "pacs.004 — Devolução", "camt.052 — Consulta Saldo", "camt.053 — Extrato", "camt.060 — Solicitação Saldo"). Correção: trocar "—" por ":" ou parênteses (ex.: "pacs.008: Pagamento").

**12. [`src/locales/pt-BR.json:1420`]** — P2
`transactions.list.bulkReturn.intro` contém "...podem ser devolvidas — as demais retornarão erro...". Correção: substituir "—" por vírgula/ponto e vírgula.

**13. [`src/locales/pt-BR.json:1539`]** — P2
`med.common.onlyStatusAllowsAction` = "Status atual: {status} — apenas {required} permite gerar." Correção: trocar "—" por ":" ou ".".

### 2.b — Acentuação errada no `pt-BR.json`

**14. [`src/locales/pt-BR.json:657`]** — P2
`messages.confirmAction` = "...deseja executar está ação?" usa o verbo "está" no lugar do demonstrativo "esta". Correção: "executar esta ação".

**15. [`src/locales/pt-BR.json:664`]** — P2
`messages.unauthorized` = "Você não está autorizado a executar está ação" — o segundo "está" está errado. Correção: "executar esta ação" (o primeiro "está", do verbo, está correto).

**16. [`src/locales/pt-BR.json:1210`]** — P2
`scopeManagement.adminLocked` = "...não pode ser reduzido por está tela." Correção: "por esta tela".

**17. [`src/locales/pt-BR.json:2221`]** — P2
`autoRefresh.rtUnavailable` = "Tempo real indisponível para está lista" Correção: "para esta lista".

**18. [`src/locales/pt-BR.json:2007`]** — P2
`cert.smoke_test.no_response_xml` = "...(modelo assincrono - PIBR.002 chega via stream de saida)." Dois erros: "assincrono" deveria ser "assíncrono" e "saida" deveria ser "saída". Correção: "modelo assíncrono ... stream de saída".

### 2.c — Acentuação errada em texto cravado nos componentes (visível ao usuário)

**19. [`views/balance/BalanceParametersView.vue:248`]** — P2
"Solicitação Automatica de Crédito" — "Automatica" sem acento. Correção: "Automática".

**20. [`views/balance/BalanceParametersView.vue:249`]** — P2
"Criar solicitação automatica quando o saldo atingir..." — "automatica" sem acento. Correção: "automática".

**21. [`views/balance/BalanceParametersView.vue:327`]** — P2
"Limite para Urgencia Crítica" — "Urgencia" sem acento. Correção: "Urgência".

**22. [`views/balance/BalanceParametersView.vue:337`]** — P2
"Solicitações acima deste valor sao marcadas como criticas" — "sao" e "criticas" sem acento. Correção: "são marcadas como críticas".

**23. [`views/balance/DebitRequestView.vue:45`]** — P2
"Processamento prioritario em até 4 horas" — "prioritario" sem acento. Correção: "prioritário".

**24. [`views/balance/DebitRequestView.vue:181`]** — P2
"Você será notificado quando houver atualizacoes." — "atualizacoes" sem acento. Correção: "atualizações".

**25. [`views/balance/DebitRequestView.vue:228`]** — P2
"Solicitar reducao de saldo na Reserva Bancária ou PIX Instantâneo" — "reducao" sem acento. Correção: "redução".

**26. [`views/balance/DebitRequestView.vue:493`]** — P2
Rótulo de campo "Urgencia *" sem acento. Correção: "Urgência *".

**27. [`views/balance/CreditRequestView.vue:26`]** — P2
"Processamento prioritario em até 4 horas" — "prioritario" sem acento. Correção: "prioritário".

**28. [`views/balance/CreditRequestView.vue:118`]** — P2
"Você será notificado quando houver atualizacoes." — "atualizacoes" sem acento. Correção: "atualizações".

**29. [`views/balance/CreditRequestView.vue:232` e `:339`]** — P2
Rótulo "Urgencia *" (linha 232) e "Urgencia" (linha 339) sem acento. Correção: "Urgência".

**30. [`views/balance/LiquidityInjectionView.vue:49`]** — P2
"Processamento prioritario em até 12 horas" — "prioritario" sem acento. Correção: "prioritário".

**31. [`views/balance/LiquidityInjectionView.vue:215`]** — P2
"Injecao de liquidez para Reserva Bancária ou PIX Instantâneo" — "Injecao" sem acento. Correção: "Injeção".

**32. [`views/balance/LiquidityInjectionView.vue:243`]** — P2
"...requerem aprovação executiva adicional alem de LSO/RSO." — "alem" sem acento. Correção: "além".

**33. [`views/balance/LiquidityInjectionView.vue:244`]** — P2
"Solicitações com urgencia crítica requerem aprovação executiva adicional." — "urgencia" sem acento. Correção: "urgência".

**34. [`views/balance/LiquidityInjectionView.vue:257`]** — P2
"O aporte de liquidez e utilizado para injecao emergencial..." — "e" (deveria ser "é") e "injecao" (deveria ser "injeção"). Correção: "é utilizado para injeção emergencial".

**35. [`views/balance/LiquidityInjectionView.vue:366`]** — P2
"Data e hora em que os recursos estarao disponíveis para aporte" — "estarao" sem acento. Correção: "estarão".

**36. [`views/balance/LiquidityInjectionView.vue:413`]** — P2
Placeholder "...incluindo projecoes de demanda, situacao atual de saldo..." — "projecoes" e "situacao" sem acento. Correção: "projeções de demanda, situação atual".

**37. [`views/balance/LiquidityInjectionView.vue:418`]** — P2
"Justificativa detalhada e necessária para aprovação." — "e" deveria ser "é". Correção: "é necessária".

**38. [`views/balance/LiquidityInjectionView.vue:457`]** — P2
Rótulo "Urgencia" sem acento. Correção: "Urgência".

**39. [`views/MonitoringView.vue:255`]** — P2
Texto visível "Servico" sem acento (apesar de o arquivo usar i18n em outros pontos). Correção: "Serviço" ou usar chave i18n.

**40. [`views/messages/MessageBuilderView.vue:299`]** — P2
"Campos obrigatorios:" — "obrigatorios" sem acento. Correção: "obrigatórios".

**41. [`views/keys/KeyCreateView.vue:121`]** — P2
"Chaves EVP sao geradas automaticamente pelo BACEN" — "sao" sem acento. Correção: "são".

**42. [`views/transactions/TransactionDetailView.vue:78`]** — P2
Rótulo de dropdown "AC03 - Conta credora invalida" — "invalida" sem acento. Correção: "inválida". (Observação: há mais rótulos cravados nesse mapa de erros, linhas 65-160, que deveriam ser revisados/centralizados.)

**43. [`views/messages/MessageCenterView.vue:325`]** — P2
"Nenhuma mensagem de {{ ... 'saida' }} encontrada" — o literal "saida" (linha 325, ramo `else`) está sem acento. Correção: "saída".

**44. [`views/reports/CustomReportView.vue:101`]** — P2
"...Os campos e filtros abaixo sao salvos como a definição do relatório personalizado." — "sao" sem acento. Correção: "são".

**45. [`views/reports/ReportSchedulerView.vue:8`]** — P2
"Gerencie a geração automatica de relatórios" — "automatica" sem acento. Correção: "automática".

### 2.d — Tradução inconsistente / mistura de idiomas / desatualizada (pt-BR.json)

**46. [`src/locales/pt-BR.json:87` e `:174` vs `:154`]** — P2
Inconsistência de termo para "claim": `commandPalette.pages.claims` = "Claims" e `nav.claimsShort` = "Claims", mas `nav.claims` = "Reivindicações". O mesmo conceito aparece ora em inglês, ora em português no mesmo painel. Correção: padronizar (sugestão: "Reivindicações" em todos, ou "Claims" como abreviação consistente só onde houver restrição de espaço, documentando a regra).

**47. [`src/locales/pt-BR.json:93`]** — P2
`commandPalette.pages.auditLogs` = "Audit Logs" em inglês, enquanto `nav.auditLogs` = "Logs de Auditoria" em português. Correção: "Logs de Auditoria".

**48. [`src/locales/pt-BR.json:1982`]** — P2
`systemHealth.services.dict-service.description` = "Diretório de chaves PIX (DICT API v2.9.0) e controle de taxa" cita a versão **v2.9.0**, mas a integração ativa é DICT v2.11.0 (vide CLAUDE.md). Rótulo desatualizado exibido ao usuário. Correção: atualizar para "v2.11.0".

**49. [`src/locales/pt-BR.json:2187`]** — P2
`certificates.detail.subject` = "Sujeito" é tradução literal/inadequada do campo X.509 "Subject". Em pt-br de certificado ICP-Brasil o usual é "Titular" (ou "Assunto" / "Sujeito do certificado"). "Sujeito" isolado soa errado. Correção: "Titular".

**50. [`src/locales/pt-BR.json:1043,1044`]** — P2
`operationsMonitor.live.live` = "live" e `...offline` = "offline" em inglês, num painel pt-br. Correção: "ao vivo" / "offline" (ou "desconectado"); ao menos traduzir "live".

**51. [`src/locales/pt-BR.json:1754,1755,1756,1757,1761,1782,1787`]** — P2
"Tracking Graph" / "tracking graph" mantido em inglês em vários rótulos do MED (`fundsRecovery.trackingTab`, `generateTracking`, etc.). Correção: padronizar para "Grafo de Rastreamento" (já existe "Gerar Tracking Graph" misturando os dois idiomas no mesmo botão).

**52. [`src/locales/pt-BR.json:1795`]** — P2
`med.refunds.title` = "Devoluções (Refunds)" mistura pt-br e inglês no título. Correção: "Devoluções".

**53. [`src/locales/pt-BR.json:1041`]** — P3/P2
`operationsMonitor.live.connected` = "Conectado ao live feed" mistura idiomas. Correção: "Conectado ao fluxo em tempo real".

### 2.e — Acentuação nos locales secundários (espanhol/francês)

**54. [`src/locales/es-ES.json` e `fr.json` (acentos)]** — P2
As traduções existentes em espanhol e francês vêm sem acento, ex.: es `med.hub.title` = "...Mecanismo Especial de Devolucion" (deveria "Devolución"), `accounting.title` = "Contabilidad" (ok) mas vários outros sem til; fr `med.hub.title` = "...Mecanisme Special de Retour" (deveria "Mécanisme Spécial"), `accounting.title` = "Comptabilite" (deveria "Comptabilité"), `operationsMonitor.title` = "Moniteur d'operations" (deveria "opérations"). Correção: revisar acentuação de es-ES e fr (baixa prioridade enquanto pt-BR é o idioma padrão, mas os idiomas estão selecionáveis).

---

## P3 — Polish (comentários/keys internos, não exibidos, mas registrados)

**55. [`components/common/StatusTabs.vue:8`]** — P3
Comentário "classes sao literais" sem acento (não exibido). Correção opcional: "são".

**56. [`views/monitor/OperationsMonitorView.vue:25`]** — P3
Comentário "push de novas operacoes" sem acento (não exibido). Correção opcional: "operações".

**57. [`views/transactions/TransactionDetailView.vue:64`]** — P3
Comentário "descricao BCB" sem acento (não exibido). Correção opcional: "descrição".

**58. [`views/keys/KeyCreateView.vue:35`]** — P3
Exemplo de e-mail `'usuario@email.com'` — é um placeholder de exemplo válido, não erro de acento. Sem ação (registrado para evitar falso positivo).

---

## Observações de método

- A acentuação do `pt-BR.json` é, no geral, boa: os erros se concentram em (a) demonstrativo "esta" grafado como verbo "está" (4 ocorrências), (b) "assíncrono/saída" (1 string), e (c) os 13 travessões de IA. O grosso dos erros de acento está no **texto cravado nos componentes**, principalmente em `views/balance/*`.
- Cobertura de chaves i18n estáticas: das ~900 chaves `$t(...)` estáticas usadas, **nenhuma chave faltando** que renderizaria a chave crua (boa notícia). As chaves dinâmicas (`t(\`...${x}\`)`) testadas resolvem para enums presentes no JSON. Logo, o problema não é "chave faltando", e sim "metade das telas não chama o i18n".
- Falsos positivos descartados após inspeção: `está` correto como verbo em `pt-BR.json:834` ("MFA não está ativo") e `:2069` ("Você está removendo..."); `remuneracao` em `Camt060Form.vue:24,57` e `Camt060ToolView.vue:74,259` são chaves/ids de código (valor exibido é "Remuneração", correto); `usuario@email.com` é exemplo de e-mail.

Arquivos-fonte da auditoria:
- `/Users/luizpenha/monetarie/pix/frontend/admin/src/locales/pt-BR.json` (canônico, 2374 linhas; base do merge)
- `/Users/luizpenha/monetarie/pix/frontend/admin/src/locales/{en-US,es-ES,fr,zh-HK}.json` (1212 linhas cada, 30/53 módulos)
- `/Users/luizpenha/monetarie/pix/frontend/admin/src/main.ts:39-53` (pt-BR como base, demais merge por cima → fallback pt-br)
- `/Users/luizpenha/monetarie/pix/frontend/admin/src/composables/useLocale.ts:17-22` e `/Users/luizpenha/monetarie/pix/frontend/admin/src/components/layout/Header.vue:44-48` (seletor expõe os 5 locales)
- 62 views sem i18n em `/Users/luizpenha/monetarie/pix/frontend/admin/src/views/`


---

## Tema claro/escuro (toggle)


Confirmed. The `settings.language` is a dead setting (never wired to i18n; i18n is driven by `ui.locale`/`localStorage('locale')`). I have all the evidence. Producing the audit report.

# Auditoria do toggle de tema claro/escuro — Painel ADMIN PIX (Monetarie)

## Veredito

O toggle de tema está **quebrado de ponta a ponta**. Existe um seletor visual de tema (Claro / Escuro / Sistema) na tela de Configurações, ele persiste a escolha e até adiciona/remove a classe `.dark` no `<html>`, mas **nada no app reage a essa classe**. Os motivos somam quatro falhas independentes e cada uma sozinha já bastaria para o modo escuro não funcionar:

1. O projeto usa Tailwind CSS v4 e **não declara `@custom-variant dark`**. Sem isso, as 409 ocorrências de `dark:` no código compilam para `@media (prefers-color-scheme: dark)`, ou seja, ignoram a classe `.dark` e seguem só a preferência do SO. Alternar Claro/Escuro na tela não muda nada.
2. **Não existe nenhuma regra CSS `.dark`** que troque as variáveis de marca (`--color-bg-page`, `--color-bg-card`, etc.). Todo o chrome do app (layout, header, sidebar, cards, tabelas) é pintado por essas variáveis fixas em tons claros. Mesmo que a classe funcionasse, o fundo continuaria branco.
3. A store que de fato aplica o tema (`settings.ts`) **só é instanciada dentro da própria tela de Configurações**. No boot do app o tema salvo nunca é reaplicado, então qualquer reload "perde" o tema até o usuário abrir /settings.
4. Há **duas stores de tema concorrentes e dessincronizadas** (`settings.ts` e `ui.ts`), gravando em chaves diferentes do `localStorage` e podendo se sobrescrever, sem nenhuma UI ligada à segunda.

Detalhamento por problema, com arquivo:linha.

---

## P0 — Quebrado

### 1. Variante `dark:` do Tailwind v4 não está ligada à classe `.dark`
[src/style.css:1] (arquivo inteiro) e [src/views/settings/SettingsView.vue:177] (entre outras 409 ocorrências)
O `style.css` começa com `@import "tailwindcss";` (Tailwind v4) e **não contém `@custom-variant dark (&:where(.dark, .dark *))`** nem nada equivalente (`grep custom-variant` = nenhum resultado). No Tailwind v4 o default de `dark:` é `prefers-color-scheme`, não a estratégia de classe. Logo, todas as 409 classes `dark:...` espalhadas em 8 arquivos (`SettingsView.vue`, `SystemHealthView.vue`, `StatusTabs.vue`, `RcoMonitorView.vue`, `ResponseTimeReportView.vue`, `PixSaquePixTrocoView.vue`, `GroupListView.vue`, `LegacyParametersView.vue`) **só reagem à preferência do SO** e ignoram completamente o toggle Claro/Escuro. Enquanto isso, o código liga/desliga `document.documentElement.classList.toggle('dark', ...)` em [src/stores/settings.ts:87], [src/stores/settings.ts:89] e [src/stores/ui.ts:32], uma classe que ninguém observa.
Correção: adicionar ao `style.css`, logo após o import, `@custom-variant dark (&:where(.dark, .dark *));` para que `dark:` passe a seguir a classe `.dark`. Sem isso, todo o resto é irrelevante.

### 2. Nenhuma variável de tema é redefinida no modo escuro
[src/style.css:4-23]
O bloco `:root { ... }` define as cores de marca todas em tons claros (`--color-bg-page: #f8fafc`, `--color-bg-card: #ffffff`, `--color-text-primary: #101820`, etc.) e **não existe nenhum bloco `.dark { ... }`** que sobrescreva esses valores (`grep "\.dark"` no CSS = nenhum resultado). O layout principal pinta o fundo com `bg-[var(--color-bg-page)]` em [src/components/layout/AdminLayout.vue:21], o header com `bg-white/95` em [src/components/layout/Header.vue:66], e dezenas de componentes usam `var(--color-bg-card-border)` inline. Resultado: ainda que a classe `.dark` fosse honrada, o app inteiro continuaria com fundo claro, porque as variáveis não mudam. O "dark mode" existente é só um verniz nas 8 telas que têm `dark:`, sobre um esqueleto que permanece branco.
Correção: criar `.dark { --color-bg-page: ...; --color-bg-card: ...; --color-text-primary: ...; ... }` redefinindo o conjunto completo de variáveis para a paleta escura, e migrar os `bg-white`/`bg-white/95` hardcoded do chrome para `var(--color-bg-card)` ou para utilitários com variante `dark:`.

### 3. O tema salvo não é aplicado no boot do app
[src/main.ts:55-65] e [src/stores/settings.ts:38]
A store `useSettingsStore` (única que aplica tema e acessibilidade) **só é referenciada em `SettingsView.vue`** (`grep useSettingsStore` = só em settings.ts e SettingsView.vue). O `main.ts` não a instancia, o `App.vue` ([src/App.vue:1-7]) só renderiza `<RouterView/>`, e o `AdminLayout.vue` usa apenas `useUIStore`. Como o `watch(..., { immediate: true })` de [src/stores/settings.ts:94-100] só roda quando a store é criada, e ela só nasce ao abrir /settings, **num reload normal (qualquer rota que não seja Configurações) o tema escolhido nunca é reaplicado**. O usuário escolhe "Escuro", recarrega a página e volta ao claro até navegar para Configurações.
Correção: instanciar e disparar a aplicação do tema no boot, por exemplo chamando `useSettingsStore()` em `main.ts` após `app.use(pinia)` (ou em `App.vue` no `setup`), garantindo que o `watch immediate` rode no carregamento; idealmente também um pequeno script inline no `index.html` para evitar flash.

### 4. Duas stores de tema concorrentes, dessincronizadas, gravando chaves diferentes
[src/stores/settings.ts:6] / [src/stores/settings.ts:72] vs [src/stores/ui.ts:8-10] / [src/stores/ui.ts:31]
Existem dois mecanismos de tema independentes:
- `settings.ts` guarda o tema dentro de `localStorage['user-settings']` (JSON, [src/stores/settings.ts:72]) e suporta `'light' | 'dark' | 'system'`.
- `ui.ts` guarda em `localStorage['theme']` (string crua, [src/stores/ui.ts:31]) e só suporta `'light' | 'dark'`.

Ambos manipulam a mesma classe `.dark` no `<html>` ([src/stores/settings.ts:87,89] e [src/stores/ui.ts:32,39]). O `ui.ts` tem `watch(theme, ..., { immediate: true })` em [src/stores/ui.ts:36-42] e é instanciado no boot via `AdminLayout.vue`, então **no boot quem vence é o `ui.ts`** (default `'light'`, [src/stores/ui.ts:9]), sobrescrevendo qualquer tema salvo em `user-settings`. A função `setTheme` do `ui.ts` ([src/stores/ui.ts:29]) **não é chamada por nenhum componente** (`grep setTheme` só aparece na definição), ou seja, é uma store de tema morta que ainda assim atropela a store viva no carregamento.
Correção: eliminar a duplicidade. Manter uma única fonte de verdade de tema (recomendado `settings.ts`, que já trata `system` e acessibilidade) e remover `theme`/`setTheme`/o `watch` de tema de `ui.ts`, deixando `ui.ts` só com sidebar/locale.

---

## P1 — Funcional-errado

### 5. Estado inicial padrão é `'light'` em vez de `'system'`, ignorando a preferência do SO no primeiro acesso
[src/stores/settings.ts:23] e [src/stores/ui.ts:9]
`DEFAULT_SETTINGS.theme = 'light'` e o default do `ui.ts` também é `'light'`. Há detecção de preferência do SO via `prefers-color-scheme` ([src/stores/settings.ts:86]) e até um listener de mudança ([src/stores/settings.ts:135-139]), mas eles só agem se o usuário **explicitamente** escolher "Sistema". No primeiro acesso (sem nada salvo), um usuário com SO em modo escuro recebe tema claro. A detecção existe mas está subutilizada.
Correção: definir o default como `'system'` (ao menos em `settings.ts`) para respeitar o SO no primeiro carregamento; ao consolidar as stores (item 4), garantir um único default coerente.

### 6. Tema só persiste ao clicar "Salvar Alterações"; reset/sync ficam inconsistentes
[src/views/settings/SettingsView.vue:39-46], [src/views/settings/SettingsView.vue:49-52]
`updateLocal('theme', ...)` aplica o tema imediatamente via `settingsStore.updateSetting('theme', ...)` ([src/views/settings/SettingsView.vue:43-45]), o que persiste de fato no `localStorage` pelo `watch(settings, persistSettings, {deep:true})` ([src/stores/settings.ts:79]). Porém as demais preferências só são gravadas no `handleSave` ([src/views/settings/SettingsView.vue:50]). Isso cria um comportamento inconsistente: o tema "salva sozinho" ao clicar no card, enquanto o resto exige o botão Salvar. Se o usuário troca o tema e sai sem salvar, o tema fica gravado mas as outras edições locais se perdem, sem aviso. É um modelo de estado confuso e propenso a bug.
Correção: unificar o modelo, ou tudo aplica/persiste ao vivo, ou tudo só no Salvar (com preview temporário do tema sem persistir até confirmar).

### 7. Não existe toggle rápido de tema no chrome (header/sidebar)
[src/components/layout/Header.vue:82-168]
O Header tem seletor de idioma, seletor de instituição, sino de notificações e menu de usuário, mas **nenhum botão de alternância de tema** (`grep setTheme/toggleTheme` no Header = nada). A única forma de mudar o tema é entrar em Configurações > aba Preferências. Para um painel operacional 24/7 (cabine PIX), a ausência de um toggle de um clique é uma lacuna de UX relevante.
Correção: adicionar um botão de alternância sol/lua no `Header.vue`, ligado à store única de tema (após resolver itens 1 a 4).

---

## P2 — UX / i18n

### 8. Os textos do seletor de tema estão hardcoded em português, fora do i18n
[src/views/settings/SettingsView.vue:164] ("Tema"), [src/views/settings/SettingsView.vue:167] ("Escolha como o sistema deve aparecer"), [src/views/settings/SettingsView.vue:199] ("Claro"), [src/views/settings/SettingsView.vue:232] ("Escuro"), [src/views/settings/SettingsView.vue:265] ("Sistema")
O app tem 5 locales (en-US, pt-BR, es-ES, zh-HK, fr — ver [src/main.ts:47-51]), mas o seletor de tema e os rótulos das abas ([src/views/settings/SettingsView.vue:17-21]) estão escritos diretamente como strings pt-BR, sem `t(...)`. Em qualquer idioma diferente de português, a tela aparece misturada.
Correção: mover esses textos para as chaves de tradução e usar `t('settings.theme.*')`.

### 9. Mismatch de códigos de idioma deixa o seletor de "Idioma" das Configurações totalmente desconectado do i18n
[src/stores/settings.ts:22], [src/views/settings/SettingsView.vue:62-65] vs [src/main.ts:44-52], [src/stores/ui.ts:7]
A store de settings usa `language: 'pt-br'` ([src/stores/settings.ts:22]) e o select da tela oferece apenas `'pt-br'` e `'en'` ([src/views/settings/SettingsView.vue:63-64]). Já o i18n real usa códigos `'pt-BR'`, `'en-US'`, `'es-ES'`, `'zh-HK'`, `'fr'` e é dirigido por `ui.locale`/`localStorage['locale']` ([src/main.ts:44], [src/stores/ui.ts:7]). O `settings.language` **não é consumido em lugar nenhum** que troque o i18n (`grep` confirma: aparece só como `computed` em [src/stores/settings.ts:55] e como `:value` em [src/views/settings/SettingsView.vue:151]). Ou seja, o seletor de idioma da tela de Configurações é um controle morto, e quem realmente troca idioma é o menu de bandeiras do Header ([src/components/layout/Header.vue:32-36]). Embora não seja "tema", isso está na mesma store/tela e reforça o padrão de configurações desconectadas que afeta o tema.
Correção: ou ligar `settings.language` ao i18n usando os mesmos códigos canônicos (`pt-BR`, etc.) e remover a duplicidade com `ui.locale`, ou remover o seletor de idioma da tela de Configurações para não confundir o operador.

### 10. Cores de seleção do próprio seletor de tema são azuis hardcoded, fora da paleta de marca
[src/views/settings/SettingsView.vue:177] (`border-blue-500 bg-blue-50`), [src/views/settings/SettingsView.vue:183] (`bg-blue-100`), [src/views/settings/SettingsView.vue:187,197] (`text-blue-600`/`text-blue-700`) e repetidos nos cards Escuro/Sistema
O estado "selecionado" dos cards de tema usa azul (`blue-500/600/700`), enquanto a identidade Monetarie é primário `#101820` + acento ouro `#ffc847` (ver [src/style.css:5-6]). Fora de marca e, dado o item 1, esses `dark:` adjacentes também não funcionam.
Correção: usar as variáveis de marca (`--color-accent`/`--color-primary`) no estado selecionado.

---

## P3 — Polish

### 11. `index.html` sem classe de tema e sem script pré-pintura (FOUC garantido)
[index.html:2] (`<html lang="en">`)
Não há `class` no `<html>` nem script inline que leia o tema do `localStorage` antes do Vue montar. Combinado com o item 3 (tema só aplicado quando a store nasce), há flash de tema claro a cada carregamento, mesmo depois que os itens 1 a 4 forem corrigidos. Bônus: `lang="en"` fixo num app cujo idioma padrão é pt-BR ([src/main.ts:44]).
Correção: script inline mínimo no `<head>` que aplique `.dark` a partir do tema salvo (ou de `prefers-color-scheme` quando `system`) antes do paint, e ajustar `lang` conforme o locale.

### 12. `largeText`/`highContrast`/`reduceAnimations` aplicam classes sem CSS correspondente
[src/stores/settings.ts:107-116]
A store adiciona as classes `high-contrast`, `large-text`, `reduce-motion`, `screen-reader-optimized` ao `<html>`, mas não há regras CSS para nenhuma delas no `style.css` (`grep` no CSS não encontra essas classes). São toggles de acessibilidade sem efeito visual, o mesmo padrão de "controle que não faz nada" do tema. Não é o tema em si, mas é a mesma store e o mesmo defeito estrutural.
Correção: implementar as regras CSS dessas classes ou remover os controles enquanto não houver suporte.

### 13. Listener de `prefers-color-scheme` nunca é removido (e duplica a cada HMR)
[src/stores/settings.ts:134-140]
O `addEventListener('change', ...)` é registrado no corpo da factory da store, sem `removeEventListener` correspondente. Em produção é benigno (store singleton), mas em dev com HMR e em testes acumula listeners. Menor, porém é o tipo de detalhe que indica que o fluxo de tema não foi tratado com cuidado.
Correção: guardar a referência do handler e remover no `dispose`/`unmount`, ou registrar uma única vez fora da store.

---

## Resumo executivo

Há um seletor de tema com aparência completa (3 cards Claro/Escuro/Sistema), detecção de SO e persistência, então "parece" pronto, mas **o modo escuro não pinta nada** porque: faltam `@custom-variant dark` (item 1) e o bloco `.dark` de variáveis (item 2); o tema **não é aplicado no boot** (item 3); e **duas stores concorrentes** se atropelam, com a store morta (`ui.ts`) vencendo no carregamento (item 4). Mesmo nas 8 telas que têm classes `dark:`, elas só obedecem à preferência do SO, jamais ao toggle. O caminho mínimo para funcionar é, nesta ordem: declarar `@custom-variant dark`, criar `.dark { ... }` com a paleta escura completa, instanciar/aplicar a store de tema única no boot e eliminar a store de tema duplicada em `ui.ts`.

Arquivos relevantes (todos absolutos):
- /Users/luizpenha/monetarie/pix/frontend/admin/src/style.css
- /Users/luizpenha/monetarie/pix/frontend/admin/src/stores/settings.ts
- /Users/luizpenha/monetarie/pix/frontend/admin/src/stores/ui.ts
- /Users/luizpenha/monetarie/pix/frontend/admin/src/main.ts
- /Users/luizpenha/monetarie/pix/frontend/admin/src/App.vue
- /Users/luizpenha/monetarie/pix/frontend/admin/src/index.html
- /Users/luizpenha/monetarie/pix/frontend/admin/src/components/layout/AdminLayout.vue
- /Users/luizpenha/monetarie/pix/frontend/admin/src/components/layout/Header.vue
- /Users/luizpenha/monetarie/pix/frontend/admin/src/views/settings/SettingsView.vue
- /Users/luizpenha/monetarie/pix/frontend/admin/src/composables/useLocale.ts


---

## Dados hardcoded e mock


Confirmed: the `getFieldPlaceholder` return values (including `12345678901`, `98765432100`, `12345678000190`, `12345678` ISPB, hardcoded date `202602071234`) are bound to the `:placeholder` attribute (grey hint text), while `v-model="formValues[field]"` starts empty. So they are placeholder hints, not pre-filled submittable values, which lowers severity but they still display fake CPF/CNPJ/ISPB strings to the operator.

I now have a complete picture. Writing the final audit report.

---

# Auditoria de Frontend, PIX Admin, Dados Hardcoded e Mock

Escopo auditado: `/Users/luizpenha/monetarie/pix/frontend/admin/src` (236 arquivos). Foco exclusivo: dados mock/fake, valores hardcoded que deveriam vir da API, URLs/ISPB/endpoints fixos e formularios pre-preenchidos com dados de teste. Não foram aplicadas correções, apenas auditoria. Os arquivos de teste (`services/__tests__/*.spec.ts`) usam `vi.mock` de forma legitima e foram desconsiderados.

## Resumo executivo

A boa noticia: a maioria das telas operacionais (Dashboard, Monitoring, SystemConfig, Balance Dashboard, Message Statistics, Operations Monitor, Accounting Monitor, Dict, Simulator, QR Codes, criação de transação) está corretamente ligada à API, com estado inicializado em zero e populado pela resposta. URLs, ISPB padrão e lista de ISPBs suspeitos vêm de variaveis de ambiente `VITE_*`, não de literais.

A má noticia: existem três seções inteiras da tela de detalhe do participante que são 100 por cento mock (sem nenhuma chamada de API), um mapa de e-mails de usuario hardcoded por id de seed no Dashboard, e o formulario de construção de mensagens exibe CPF/CNPJ/ISPB/data de teste como placeholders. Detalhamento abaixo.

---

## P0, quebrado, exibe dado fabricado como se fosse real

### 1. [views/participants/ResponsiblesSection.vue:17-26]
Severidade: P0.
A lista de responsaveis é um array hardcoded com uma unica linha fixa: `nome: 'System Administrator'`, `cargo: 'Administrador'`, `email: 'admin@monetarie.com.br'`, `telefone: '---'`, `principal: true`. Não há nenhuma chamada de API. O componente recebe a prop `ispb` (linha 5) e a ignora completamente, então qualquer participante aberto na tela mostra exatamente o mesmo responsavel falso. Os handlers `handleAdd` (28), `handleEdit` (31) e `handleRemove` (34) têm corpo vazio, ou seja, os botões "Adicionar Responsável", "Editar" e "Remover" não fazem nada.
Correção recomendada: criar `services/responsibles.ts` (ou estender o de participantes), buscar os responsaveis por `props.ispb` em `onMounted`, inicializar `responsaveis` como `[]`, e implementar os handlers de CRUD contra o backend. Enquanto não houver endpoint, exibir estado vazio real em vez do registro fabricado.

### 2. [views/participants/WebhooksSection.vue:16-29]
Severidade: P0.
`webhooks = ref<Webhook[]>([])` nunca é preenchido por API. Os handlers `handleAdd` (18), `handleEdit` (21) e `handleRemove` (28) têm corpo vazio; só `handleToggle` (24) altera o estado local em memoria, que se perde no refresh. A prop `ispb` (linha 5) é ignorada. O resultado é uma tela de webhooks puramente decorativa: o operador acha que cadastra webhooks por participante mas nada é persistido.
Correção recomendada: buscar webhooks por `props.ispb` em `onMounted` via service real e ligar os handlers de criar/editar/remover/ativar ao backend. Se o endpoint ainda não existe, marcar a seção como indisponivel em vez de simular CRUD.

### 3. [views/participants/LimitsSection.vue:10-17]
Severidade: P0.
Os limites do participante são valores hardcoded em centavos: `daily: 100000000` (R$ 1.000.000,00), `monthly: 5000000000` (R$ 50.000.000,00), `perTransaction: 50000000` (R$ 500.000,00), e os usados/contadores fixos em `0`. São renderizados como se fossem os limites reais daquele participante, inclusive com barras de progresso. A prop `ispb` (linha 5) é ignorada, então todo participante mostra os mesmos limites. O botão "Editar Limites" (linha 110-115) abre um modal "Funcionalidade em desenvolvimento" (linha 132), ou seja, não edita nada.
Correção recomendada: buscar limites e consumo por `props.ispb` via API em `onMounted`, inicializar tudo em zero, e implementar a edição real. Esses números (R$ 1mi/R$ 50mi/R$ 500k) não podem aparecer como verdade operacional em uma cabine PIX.

### 3b. [views/ParticipantDetailView.vue:317 e :337]
Severidade: P0 (efeito colateral do item 3).
`LimitsSection` está montado duas vezes na mesma tela (linhas 317 e 337), enquanto `ResponsiblesSection` (297) e `WebhooksSection` (357) aparecem uma vez cada. Provavel erro de copia/cola: uma das duas abas que deveria mostrar outra coisa está exibindo limites duplicados.
Correção recomendada: revisar as abas do detalhe do participante e substituir a montagem duplicada de `LimitsSection` pelo componente correto da aba.

---

## P1, funcional errado, depende de seed especifico

### 4. [views/DashboardView.vue:165-169]
Severidade: P1.
`formatAuditUser` mapeia ids de usuario para e-mails de forma hardcoded: `'1': 'admin@monetarie.com.br'`, `'2': 'operator@monetarie.com.br'`, `'3': 'viewer@monetarie.com.br'`. Isso só funciona para o banco semeado (seed). Em produção, ou com qualquer usuario de id 4 ou maior, ou se os ids de seed mudarem, o nome do autor do log de auditoria fica errado ou cai em "sistema". É um mapa de dados de teste embutido na renderização da auditoria, que é justamente uma area que precisa de precisão.
Correção recomendada: o backend deve retornar o e-mail/nome do autor junto do log (`log.user_name`/`log.actor`), e o frontend deve consumir só esses campos. Remover o `usersBySeedId`.

---

## P2, UX e i18n, dados de teste visiveis ao operador

### 5. [views/messages/MessageBuilderView.vue:196-207]
Severidade: P2.
O mapa `getFieldPlaceholder` injeta CPF/CNPJ de teste como placeholder dos campos do formulario "Construir Mensagem": `debtor_document: '12345678901'` (196), `creditor_document: '98765432100'` (197), `indirect_cnpj: '12345678000190'` (199), `director_cpf: '12345678901'` (207). Confirmado que estão ligados ao atributo `:placeholder` (linha 320), não ao `v-model` (que começa vazio na linha 318), então não são valores submetidos. Mesmo assim, exibem documentos fake ao operador, e numa cabine PIX isso induz a erro e dá aparência de ambiente de teste.
Correção recomendada: trocar por placeholders genéricos não numéricos (ex.: "Somente digitos, 11 para CPF", "Somente digitos, 14 para CNPJ") ou mascaras de formato vazias, em vez de documentos plausiveis.

### 6. [views/messages/MessageBuilderView.vue:189]
Severidade: P2.
O placeholder do `end_to_end_id` é montado com data fixa hardcoded: `` `E${sampleParticipantIspb.value}202602071234abcdefghijkl` ``. O trecho `202602071234` é uma data/hora congelada (2026-02-07 12:34) e `abcdefghijkl` é sufixo fake. Aparece como dica para o operador um E2E que parece valido mas é de uma data passada e arbitraria.
Correção recomendada: gerar o placeholder com a data/hora atual (ou apenas descrever o formato), em vez de uma data fixa. O ideal é o backend gerar o E2E e o campo nem ser editavel manualmente.

### 7. [views/messages/MessageBuilderView.vue:25]
Severidade: P2.
`sampleParticipantIspb` faz fallback para o ISPB literal `'12345678'` quando não há instituição ativa nem `VITE_DEFAULT_INSTITUTION_ISPB`. Esse `'12345678'` reaparece nos placeholders de `debtor_ispb` (186), `party_ispb` (200) e no E2E (189). ISPB inventado como dica em tela de cabine PIX.
Correção recomendada: não usar ISPB literal de fallback; se não houver instituição ativa, deixar o placeholder vazio ou com texto descritivo ("ISPB do participante, 8 digitos").

### 8. [views/messages/MessageBuilderView.vue:198 e :201]
Severidade: P2.
Placeholders de ISPB de teste: `indirect_ispb: '12345678'` (198), `target_ispb: '12345678'` (201), além de `creditor_ispb: '87654321'` (187). São ISPBs sequenciais ficticios exibidos ao operador.
Correção recomendada: substituir por texto descritivo de formato, não por ISPBs ficticios.

### 9. [services/dict.ts:25,27,34,40]
Severidade: P2 (mistura de i18n e alvo hardcoded).
Em `getIndicators`, os nomes dos indicadores estão hardcoded em ingles: `name: 'Total Keys'` (25), `'Availability'` (31), `name: 'Avg Response Time'` (34), `'SLA Compliance'` (40). Além disso, os alvos são hardcoded: `target: 300000` chaves (27), `target: 100` ms (37) e `99.95`/`99.5` por cento (31/40). Os valores em si já vêm da API corretamente (o mock de SLA foi removido, conforme comentario), mas os rotulos não são traduzidos e os alvos são fixos no codigo.
Correção recomendada: mover os nomes para chaves i18n e, idealmente, receber os alvos (`target`) do backend, já que são parametros de ANS que podem mudar.

---

## P3, polish, defaults hardcoded de baixo risco

### 10. [views/balance/BalanceParametersView.vue:16-27]
Severidade: P3.
O `formData` inicial do formulario de parametros de saldo é pre-preenchido com valores financeiros hardcoded: `minBalanceRB: 10000000`, `minBalancePI: 5000000`, `criticalBalanceRB: 5000000`, `criticalBalancePI: 2000000`, `autoRequestThreshold: 8000000`, `autoRequestAmount: 5000000`, `criticalAmountThreshold: 10000000`, `executiveApprovalAmount: 50000000`. Em `onMounted` (87-95) e no `watch` (97-107) esses defaults são sobrescritos pela resposta da API, então o impacto real é: (a) um flash de valores fabricados antes do carregamento, e (b) se a API retornar `null`/vazia, o operador pode salvar esses defaults arbitrarios como se fossem politica oficial.
Correção recomendada: inicializar `formData` vazio/zerado e só liberar o submit após o carregamento dos parametros reais; ou usar `null` e bloquear o salvamento enquanto não houver dado da API.

### 11. [views/balance/DebitRequestView.vue:32]
Severidade: P3.
`minBalanceThreshold = computed(() => balanceStore.parameters?.minBalanceRB || 10000000)`. Fallback hardcoded de R$ 100.000,00 (em centavos) usado na validação do pedido de debito quando os parametros não carregaram. O proprio codigo admite o problema no comentario da linha 30 ("Constants - These would come from API in production"). Risco baixo porque há fonte da store, mas o fallback é um valor de negocio inventado.
Correção recomendada: se `parameters` não carregou, não validar contra um limite fabricado; exibir aviso de "parametros indisponiveis" e bloquear, em vez de usar `10000000`.

### 12. [views/participants/ResponseTimeReportView.vue:357]
Severidade: P3.
`slaTarget: 500` hardcoded no objeto `summary`. É a meta ANS de 500ms. Como é mesclado com a resposta da API (o endpoint pode sobrescrever), o risco é baixo, mas se a API não retornar `slaTarget`, fica fixo em 500ms no codigo.
Correção recomendada: receber o alvo de ANS do backend, ou centralizar a constante ANS num unico lugar de configuração em vez de literal na view.

### 13. [views/keys/KeyCreateView.vue:32-34]
Severidade: P3.
Placeholders de mascara com zeros: `CPF: '000.000.000-00'`, `CNPJ: '00.000.000/0000-00'`, `PHONE: '+5511999999999'`. Os de CPF/CNPJ são mascaras neutras (aceitavel). O de telefone `+5511999999999` parece um numero plausivel de teste em vez de mascara.
Correção recomendada: trocar o placeholder de telefone por mascara neutra (ex.: "+55 (DDD) numero") em vez de um numero ficticio completo.

---

## Áreas verificadas e consideradas corretas (sem achados)

Para registro, confirmei que as seguintes telas inicializam estado em zero/vazio e populam via API, sem dado fabricado:

- `views/DashboardView.vue` cards de stats (43-76): valores vêm de `/api/v1/dashboard/stats`.
- `views/MonitoringView.vue` (17-104): `services`/`metrics`/`alerts` zerados e vindos de `/api/v1/system/health` e `/audit/logs/stats`.
- `views/SystemConfigView.vue` (36-59): parametros vêm de `/api/v1/system/config`.
- `views/balance/BalanceDashboardView.vue`: tudo via `balanceStore`.
- `views/participants/MessageStatisticsView.vue` (26-82) e `ResponseTimeReportView.vue`: summary zerado, dados via `/audit/logs/stats`.
- `views/accounting/AccountingMonitorView.vue` (53-99): cards derivados de `/api/v1/fees/schedules`.
- `views/transactions/TransactionCreateView.vue` (9-23): formulario "Nova Transação" com todos os campos vazios, ISPB do devedor lido do `localStorage` da instituição ativa, sem prefill de teste.
- `views/qrcodes/QrCodeCreateView.vue` e `QrCodeListView.vue`: via `qrcodeService`, formulario vazio.
- `views/auth/LoginView.vue` (13-21): login e senha começam vazios, sem credencial pre-preenchida.
- `stores/institution.ts`: ISPB ativo 100 por cento via API e env, sem literal.
- `stores/balance.ts`: `parameters` é `null` até a API responder, sem valor fabricado.
- URLs e ISPB padrão: todos via `VITE_API_BASE_URL`, `VITE_DEFAULT_INSTITUTION_ISPB`, `VITE_PIX_DOCS_URL`, `VITE_MONITOR_SUSPICIOUS_ISPBS` etc. (`vite-env.d.ts:15-21`), nenhum endpoint/host hardcoded encontrado.
- `composables/useMessageForms.ts:34-82`: catalogo de 3 tipos de mensagem é metadado estatico (codigos ISO + chaves i18n), comentado como "Hardcoded MVP catalog", não é dado de transação fake.

## Prioridade de correção sugerida

1. Itens 1, 2, 3, 3b (P0): as três seções mock do detalhe do participante e a montagem duplicada. São o que mais "denuncia" o front como inacabado e mostram numeros/e-mails fabricados como reais.
2. Item 4 (P1): mapa de e-mails por id de seed no Dashboard de auditoria.
3. Itens 5 a 9 (P2): limpar CPF/CNPJ/ISPB/data de teste dos placeholders do construtor de mensagens e traduzir rotulos do dict.
4. Itens 10 a 13 (P3): defaults financeiros hardcoded e placeholder de telefone.

Arquivos que exigem ação (caminhos absolutos):
- `/Users/luizpenha/monetarie/pix/frontend/admin/src/views/participants/ResponsiblesSection.vue`
- `/Users/luizpenha/monetarie/pix/frontend/admin/src/views/participants/WebhooksSection.vue`
- `/Users/luizpenha/monetarie/pix/frontend/admin/src/views/participants/LimitsSection.vue`
- `/Users/luizpenha/monetarie/pix/frontend/admin/src/views/ParticipantDetailView.vue`
- `/Users/luizpenha/monetarie/pix/frontend/admin/src/views/DashboardView.vue`
- `/Users/luizpenha/monetarie/pix/frontend/admin/src/views/messages/MessageBuilderView.vue`
- `/Users/luizpenha/monetarie/pix/frontend/admin/src/services/dict.ts`
- `/Users/luizpenha/monetarie/pix/frontend/admin/src/views/balance/BalanceParametersView.vue`
- `/Users/luizpenha/monetarie/pix/frontend/admin/src/views/balance/DebitRequestView.vue`
- `/Users/luizpenha/monetarie/pix/frontend/admin/src/views/participants/ResponseTimeReportView.vue`
- `/Users/luizpenha/monetarie/pix/frontend/admin/src/views/keys/KeyCreateView.vue`


---

## Saldos e funcionalidade (unidades, contrato, 404)


I have confirmed everything I need. The `available` column is `:decimal` storing reais (not cents), the camt060 reads it as reais (no /100), the dashboard/history/requests divide by 100. The "Solicitações Pendentes" are balance_blocks from outbound flows (pacs.004 returns, recurrences). The router has no `/balance/requests`, `/balance/requests/:id`, `/balance/parameters`, approve/reject endpoints. Statement field names mismatch (`from_date`/`to_date` vs `period_start`/`period_end`; `statement_type` vs `type`). The statement controller `generate` expects `statement_type` = "intraday"/"end_of_day" but the frontend sends `type` = "camt.052"/"camt.053".

Let me do one final verification on the `positions` endpoint to confirm there is no RB/PI field, and confirm the `getIntradayMovements` endpoint shape vs what's consumed. I've already read enough. Let me compile the audit report.

I have all the evidence needed. Here is the exhaustive audit.

---

# Auditoria de Funcionalidade, Telas de Saldo/Operacao do Painel Admin PIX (Monetarie)

Escopo: `views/balance/*`, `views/camt060/*`, `views/statements/*` no frontend, e os controllers de saldo/camt060/statement mais o contexto `Balances` no backend. Tudo abaixo foi verificado lendo o codigo, com referencia a arquivo:linha. Nada foi corrigido, apenas auditado.

## Resumo executivo (a raiz dos 5 problemas)

1. O backend guarda e devolve dinheiro em REAIS (coluna `:decimal`, ver `balances/balance.ex:16-17`; a view CAMT.060 serializa com `Decimal.to_string` em reais, `camt060.ex:70-92`). Varias telas de saldo dividem por 100 como se fosse centavos. Isso sozinho gera o "saldo 100x menor" e parte da inconsistencia entre telas.
2. O frontend chama varios endpoints de "requests"/"parameters" que NAO EXISTEM no router (`router.ex:72-99`). Toda a sub-area de solicitacoes (lista, detalhe, criar, aprovar, rejeitar, parametros) bate em 404.
3. O endpoint que de fato existe (`/balance/requests/pending`) devolve bloqueios de saldo de fluxos de saida (pacs.004 de devolucao e recorrencia), nao "solicitacoes de credito/debito". O front rotula isso como solicitacao DEBIT pendente de LSO.
4. O backend nunca produz split RB x PI; ha um unico saldo por ISPB. O front inventa "RB" e "PI" e sempre deixa RB zerado.
5. Os tres numeros divergentes (1.000.000 vs 10.000 vs 999.999,97) vem de telas lendo a mesma fonte com unidades diferentes (umas dividem por 100, CAMT.060 nao divide) somado a fontes diferentes (posicao agregada x bloqueios x retorno BACEN materializado).

---

## P0, Quebrado (rota inexistente / tela nao funciona)

**[services/balance.ts:269] e [stores/balance.ts:73-83] e [views/balance/BalanceDashboardView.vue:126] — P0**
`getRequests()` faz `GET /api/v1/balance/requests`. Esse path NAO existe no router (`router.ex:72-99` so tem `/balance/requests/pending`). A `BalanceRequestsListView` (carrega via `getRequests`, linha 123) sempre cai no `catch` e mostra "Falha ao carregar solicitacoes" ou lista vazia.
Correcao: ou criar no backend `GET /balance/requests` (index com filtros status/type/page/limit retornando o shape esperado), ou apontar o front para `/balance/requests/pending` e remover a tela de lista completa. Decidir a fonte de verdade do conceito "solicitacao".

**[services/balance.ts:275] e [views/balance/BalanceRequestDetailView.vue:99] — P0**
`getRequest(id)` faz `GET /api/v1/balance/requests/:id`. Path inexistente no router. A `BalanceRequestDetailView` sempre erra (`fetchRequest` cai no catch) e mostra "Solicitacao nao encontrada". O link do dashboard (`BalanceDashboardView.vue:69` `navigateToRequest`) leva a uma tela que nunca carrega.
Correcao: implementar `GET /balance/requests/:id` no backend, ou remover a navegacao/rota.

**[services/balance.ts:280] e [views/balance/CreditRequestView.vue:88], [DebitRequestView.vue:133], [LiquidityInjectionView.vue:125] — P0**
`createRequest()` faz `POST /api/v1/balance/requests`. Path inexistente. As tres telas de criacao (Credito, Debito, Aporte) chamam `balanceStore.createRequest` -> 404. O usuario preenche o formulario, clica Enviar, e o backend responde 404, mas o `CreditRequestView.vue:95-98` ainda assim cai no fluxo de erro (mostra erro). Pior: nenhuma solicitacao e persistida em lugar nenhum.
Correcao: criar `POST /balance/requests` no backend com o modelo de dados de solicitacao (tabela propria, fluxo LSO/RSO), ou remover essas telas ate existir backend.

**[services/balance.ts:285,292] e [views/balance/BalanceRequestDetailView.vue:109,123] — P0**
`approveRequest()`/`rejectRequest()` chamam `POST /api/v1/balance/requests/:id/approve` e `.../reject`. Paths inexistentes. Os botoes Aprovar/Rejeitar na tela de detalhe nunca funcionam (e a tela nem carrega, ver acima). Todo o fluxo de dupla assinatura LSO/RSO e fantasma.
Correcao: backend precisa expor approve/reject com regra de alcada, ou remover a UI.

**[services/balance.ts:307,312] e [stores/balance.ts:89,102] e [views/balance/BalanceParametersView.vue:88,73] — P0**
`getParameters()`/`updateParameters()` chamam `GET/PUT /api/v1/balance/parameters`. Path inexistente no router. A `BalanceParametersView` no `onMounted` (linha 88) sempre falha o GET (mostra "Falha ao carregar parametros") e o "Salvar Parametros" (linha 73, PUT) sempre da 404. A tela inteira de parametros e nao funcional; os valores que aparecem sao apenas os defaults hardcoded do front (`BalanceParametersView.vue:15-27`), nao persistem.
Correcao: implementar endpoints de parametros no backend (ou remover a tela). Hoje e uma tela 100% decorativa.

**[services/balance.ts:251] — P0 (latente)**
`getPosition(accountType)` faz `GET /api/v1/balance/position/${accountType}` passando 'RB'/'PI' como se fosse `:ispb` (a rota e `/balance/position/:ispb`, `router.ex:88`). Chamaria o backend com ispb="RB", que falha na validacao de ISPB (8 digitos). Nao esta sendo usado pelas telas atuais, mas e uma bomba: qualquer uso futuro quebra. O conceito accountType nao mapeia para o parametro ispb da rota.
Correcao: remover a funcao ou redesenhar; o backend nao tem endpoint por tipo de conta.

**[views/statements/StatementGenerateView.vue:8-13,44] vs [backend statement_controller.ex:25-36] — P0**
O form envia `{ type: 'camt.053', start_date, end_date, account_ispb }`. O backend `generate` (linha 26-27) le `statement_type` (ou `type`) e SO aceita os valores `"intraday"` ou `"end_of_day"` (linha 31-35); `"camt.053"` cai no `_ -> {:error, :invalid_statement_type}` (linha 35), retornando 400 "Invalid statement type". Alem disso o backend espera `ispb` (linha 26) e o front manda `account_ispb`; e espera `from_date`/`to_date` (linha 215,224) e o front manda `start_date`/`end_date`. Resultado: gerar extrato sempre falha (tipo invalido) e, mesmo se o tipo passasse, ISPB e datas chegariam nil.
Correcao: alinhar contrato. Mapear no front `type` camt.052->intraday e camt.053->end_of_day, renomear `account_ispb`->`ispb`, `start_date`->`from_date`, `end_date`->`to_date`. (camt.054 nao tem geracao no backend.)

---

## P1, Funcional Errado (unidade, dados inventados, semantica trocada)

**[views/balance/BalanceDashboardView.vue:45] — P1 (unidade reais x centavos)**
`formatCurrency` faz `.format(value / 100)`. O backend devolve reais (coluna `:decimal`, `balance.ex:16`; positions em `balances.ex:489-497` devolve `available` Decimal em reais). Logo um saldo real de R$ 1.000.000,00 aparece como R$ 10.000,00. Afeta TODOS os cartoes do dashboard (RB, PI, Disponivel total, Creditos/Debitos hoje, Saldo liquido).
Correcao: remover o `/100`; tratar o valor ja como reais (ou padronizar todo o backend para centavos inteiros, mas hoje ele e reais).

**[views/balance/BalanceRequestsListView.vue:50] — P1 (unidade)**
Mesmo `formatCurrency(value/100)`. O `amount` aqui vem do bloqueio (Decimal reais, `balances.ex:574` mapeia `amount: block.amount`). Valor 100x menor.
Correcao: remover `/100`.

**[views/balance/BalanceRequestDetailView.vue:30] — P1 (unidade)**
Idem `formatCurrency(value/100)` no detalhe da solicitacao.
Correcao: remover `/100`.

**[views/balance/BalanceHistoryView.vue:44] — P1 (unidade)**
Idem `formatCurrency(value/100)`. `BalanceHistory.available_before/after/change_amount` sao `:decimal` reais (`balance_history.ex:18-21`). Todas as colunas da tabela de historico, os 4 cards de resumo e o detalhe do dia ficam 100x menores.
Correcao: remover `/100`.

**[views/balance/BalanceParametersView.vue:46] — P1 (unidade)**
`formatCurrency(value/100)` e os defaults em centavos (`minBalanceRB: 10000000` comentado como cents). Inconsistente com o resto que e reais. Mas como o input usa `v-model.number` direto sem conversao (linha 177), o que o usuario digita em reais e exibido dividido por 100 em outros lugares. Tela ja e 404, mas a logica de unidade tambem esta trocada.
Correcao: padronizar unidade (reais) e remover `/100`.

**[views/balance/CreditRequestView.vue:43 vs DebitRequestView.vue:79 vs LiquidityInjectionView.vue:86] — P1 (unidades CONTRADITORIAS entre as 3 telas de criacao)**
Bug grave de inconsistencia interna:
- Credito: `v-model.number="amount"` (linha 216) liga direto o valor digitado (reais) a `amount`, e a validacao usa `amount >= 1000` (linha 35) tratando como REAIS. Mas `formatCurrency(amount/100)` (linha 43) exibe como se fosse centavos. Ou seja, usuario digita 1000 (mil reais), o resumo mostra R$ 10,00.
- Debito: `handleAmountChange` faz `parseFloat(value) * 100` (linha 79), tratando o input como reais e guardando CENTAVOS em `amount`; valida `amount < 100000` (linha 105 = R$1.000 em centavos). Convencao oposta a do Credito.
- Aporte: igual ao Debito, `* 100` (linha 86), valida `amount < 100000000` (linha 93 = R$1.000.000 em centavos).
Resultado: as tres telas usam unidades diferentes para a mesma chave `amount` do mesmo `createRequest`. Se o backend existisse, gravaria credito em reais e debito/aporte em centavos. Os limites tambem ficam inconsistentes (mensagem do Credito diz "minimo R$1.000,00" mas valida contra 1000 reais; Debito diz o mesmo mas valida 100000 centavos).
Correcao: definir UMA convencao (recomendado: reais no payload) e aplicar identica nas tres telas, alinhando validacao, `formatCurrency` e mensagens.

**[services/balance.ts:238-248] + [views/balance/BalanceDashboardView.vue:16-17,178-253] — P1 (split RB/PI inexistente; RB sempre R$0)**
`getPositions()` espera linhas com `accountType === 'RB'` e `'PI'`. O backend `list_all_positions` (`balances.ex:478-499`) devolve uma lista de posicoes POR ISPB, sem nenhum campo `accountType`/`account_type`. Logo `rb` nunca casa (`balance.ts:241`), cai em `zeroPosition('RB')` (linha 245) -> cartao "RESERVA BANCARIA" sempre R$ 0,00, badge "Alerta". O `pi` pega `rows[0]` (a posicao do primeiro/seu ISPB). O "Saldo Disponivel Total" (linha 246) soma RB(0)+PI, e a divisao por participante "RB:/PI:" (linha 249-250) e ficticia. O backend nao modela RB (reserva bancaria) separada da Conta PI; isso e invencao do front.
Correcao: ou o backend passa a retornar duas contas (RB e PI) com `accountType`, ou o front para de fingir o split e mostra apenas a Conta PI (unica que existe).

**[services/balance.ts:166,173] — P1 (campos ausentes geram valores enganosos)**
`normalizePosition` le `row.total`, `changePercent24h`, `status`, `minThreshold`, `criticalThreshold`. O backend `list_all_positions` so devolve `ispb/available/blocked/total/projected/as_of/status` (`balances.ex:489-497`). Campos `changePercent24h`, `minThreshold`, `criticalThreshold` nunca vem -> `toNumber(undefined)=0`. Entao o dashboard mostra sempre "+0.0% vs 24h atras" (`BalanceDashboardView.vue:190-192`) como se fosse dado real. O `withinLimit` (linha 173) so e true se `status !== 'critical'`, mas o backend usa status como `:healthy/:low_balance/:blocked/:high_block_ratio/:critical` (`balances.ex:594-600`), entao a logica "Normal/Alerta" do badge nao reflete os thresholds reais.
Correcao: backend expor variacao 24h e thresholds, ou o front parar de exibir esses indicadores fixos em 0.

**[services/balance.ts:300-303] + [backend balances.ex:560-585] — P1 ("Solicitacoes Pendentes" = bloqueios de saida, nao solicitacoes)**
`getPendingRequests()` consome `/balance/requests/pending`. O backend `list_pending_requests` (`balances.ex:561-585`) retorna SOMENTE balance_blocks ativos, com `type: "balance_block"`, `reason` vindo de `"outbound_pacs004_return"` (`return_processor.ex:130`) ou `"outbound_recurrence"` (`execution_worker.ex:184`), `reference_type: "return_request"/"recurrence_execution"`. Ou seja, sao reservas de fundos de mensagens de SAIDA (devolucao pacs.004 e recorrencia), nao pedidos de credito/debito. No front `normalizeRequest` (`balance.ts:179-201`): `rawType="BALANCE_BLOCK"` -> contem "BLOCK" -> vira `'DEBIT'` (linha 183), `status: :pending` -> vira `'PENDING_LSO'` (linha 181). Entao toda transacao de saida em voo aparece como "Solicitacao de DEBITO aguardando LSO". O dashboard (`BalanceDashboardView.vue:259-298`) lista isso em "Solicitacoes Pendentes". Conceito completamente trocado.
Sobre o "R$ 0,00": o `amount` do bloco e o valor reservado (deveria ser > 0, ha `validate_number(:amount, greater_than: 0)` em `balance_block.ex:42`). O R$0,00 observado vem de DOIS efeitos combinados: (a) o `/100` do dashboard reduz 100x; (b) se algum fluxo cria bloco com amount nulo/pequeno ou o pacs.008 outbound nao popula amount, exibe arredondado para R$0,00. De qualquer modo a rotulagem "solicitacao pendente" para um pacs de saida ja e o bug principal.
Correcao: separar claramente "bloqueios operacionais de saida" de "solicitacoes de saldo (credito/debito/aporte)". Nao reaproveitar `/requests/pending` (que sao blocks) como se fosse a fila de aprovacao LSO/RSO.

**[services/balance.ts:256-259] + [router.ex:80] — P1 (intraday: rota existe mas shape/filtro divergem)**
`getIntradayMovements(accountType)` faz `GET /balance/intraday` passando `params.accountType`. O backend `intraday` (`balance_controller.ex:286-298`) ignora accountType (usa ISPB do contexto) e responde `{ispb, date, movements, summary}` com movements vindos de `get_intraday_movements` (campos `hour/hour_label/credits/debits/credit_count/debit_count/closing_balance`, `balances.ex:528-541`). O front `normalizeIntraday` (`balance.ts:203-212`) le `balance ?? closing_balance ?? available_after` e `transactionCount ?? credit_count` + `debit_count`; funciona em parte, mas `credits/debits` aqui sao Decimais em reais e depois entram no `formatCurrency(/100)` do dashboard (totalCreditsToday/totalDebitsToday, `BalanceDashboardView.vue:24-30,366,373`) -> 100x menores.
Correcao: remover `/100` no dashboard; e o filtro por accountType nao tem efeito, deve ser removido para nao enganar.

**[services/balance.ts:330] + [BalanceHistoryView.vue:53-58] — P1 (historico exige ISPB de localStorage; sem isso a tela so erra)**
`getBalanceHistory(ispb, ...)` faz `GET /balance/history/:ispb`. A view le `localStorage.getItem('activeInstitutionIspb')` (linha 53) e, se vazio, nunca consulta (mostra "Selecione uma instituicao ativa"). Em homolog com um unico ISPB proprio, se esse item nao estiver setado, a tela de historico fica inutil. Alem disso o `accountType` enviado (linha 66, vira `params.accountType` na query) e ignorado pelo backend (`history_by_ispb` em `balance_controller.ex:93-106` so usa from_date/to_date/granularity).
Correcao: cair para o ISPB da instituicao logada/config quando localStorage vazio; remover o filtro accountType que nao existe no backend.

**[services/balance.ts:214-233 normalizeHistory] — P1 (credito/debito derivados errado)**
`normalizeHistory` calcula `totalCredits = change>0 ? change : ...` e `totalDebits = change<0 ? abs(change) : ...` a partir de `change_amount` (`balance.ts:216-218`). O backend `BalanceHistory` registra UMA linha por movimento com `change_amount` e `direction` (`balance_history.ex`), nao um agregado diario com totalCredits/totalDebits. Como `get_balance_history` no modo `:daily` faz `distinct` por data pegando a ultima linha (`balances.ex:191-196`), cada "dia" vira um unico movimento, e os "totais de credito/debito" do dia ficam sendo o valor de um unico lancamento, nao a soma. Os cards de resumo (`BalanceHistoryView.vue:24-37`) somam esses pseudo-totais. Numero incorreto.
Correcao: o backend precisa agregar credito/debito por dia (SUM agrupado), nao distinct da ultima linha; o front nao deve inferir totais de uma so coluna.

**[views/statements/StatementListView.vue:216-223] vs [statement_json.ex:27-43] — P1 (nomes de campo divergentes; tabela vazia/zerada)**
A `Statement` (front, `statement.ts:3-15`) e a tabela usam `type` (linha 216), `period_start`/`period_end` (linha 218), `total_credits`/`total_debits` (linha 221-222), `record_count` (linha 223), `account_ispb` (linha 220). O backend `statement_data` (`statement_json.ex:27-43`) devolve `statement_type` (nao `type`), `from_date`/`to_date` (nao `period_start/end`), `ispb` (nao `account_ispb`), `credit_count`/`debit_count` (nao `record_count`). Resultado: coluna Tipo vazia, Periodo "- a -", ISPB "-", Registros "-". Os campos `total_credits/total_debits` casam de nome, mas (ver abaixo) podem estar zerados/sem unidade.
Correcao: alinhar nomes entre `statement_json.ex` e `services/statement.ts`/`StatementListView.vue`.

**[views/statements/StatementListView.vue:77-80] — P1 (unidade nos extratos)**
Aqui `formatCurrency(value)` NAO divide por 100 (linha 79). Os demais (dashboard/history/requests) dividem. Se os totais do extrato vierem na mesma unidade do saldo (reais), o extrato fica certo e o resto errado, evidenciando a falta de padrao. Se vierem em centavos, o extrato fica 100x maior. De qualquer forma ha inconsistencia de convencao entre telas que precisa ser unificada.
Correcao: definir unidade unica em todo o dominio e aplicar a mesma regra em todas as telas.

**[views/statements/StatementListView.vue:225] vs [router.ex:144-157] — P1 (download so existe se backend gerar; status enum divergente)**
A coluna Status usa labels `completed/processing/pending/error` (linha 99-117), mas o backend nao define esse enum de status no `statement_json` (nem retorna `status` em `statement_data`, `statement_json.ex:27-43` nao inclui `status`). Logo `item.status` chega `undefined`, cai no fallback cinza, e o botao Baixar (so aparece com `status === 'completed'`, linha 232) nunca aparece. Download fica inacessivel pela lista.
Correcao: backend incluir `status` no JSON do extrato com os valores que o front espera, ou o front parar de gatear o botao por status.

---

## P2, UX / i18n

**[views/balance/BalanceDashboardView.vue:48-49] e demais formatDateTime — P2 (timezone)**
`new Date(dateStr)` + `Intl.DateTimeFormat('pt-BR', ...)` sem `timeZone: 'America/Sao_Paulo'`. Viola a regra documentada (CLAUDE.md gotcha #27): datas/horas BACEN em UTC podem exibir o dia anterior. So o CAMT.060 (`Camt060ToolView.vue:167`) usa timeZone corretamente. Inconsistente.
Correcao: padronizar `timeZone: 'America/Sao_Paulo'` em todos os formatadores de data/hora das telas de saldo, historico e extratos.

**[views/balance/BalanceDashboardView.vue:181,211] — P2 (i18n/acentuacao)**
Textos em caixa alta sem acento: "RESERVA BANCARIA" (falta acento, deveria "Reserva Bancaria"), "PIX INSTANTANEO", "SALDO DISPONIVEL TOTAL". Varias labels pelo dashboard e historico estao sem acento: "Creditos Hoje", "Debitos Hoje", "Saldo Liquido", "24h atras", "Concluida", "Variacao Liquida", "Saldo Medio Diario", "Saldo Mais Alto/Baixo", "Media por Transacao", "atualizacoes" (CreditRequestView:118). Idem em DebitRequestView ("reducao", "Concluido"), LiquidityInjectionView ("Injecao", "Captacao", "Disponibilizacao", "Urgencia", "estarao", "necessaria").
Correcao: revisar acentuacao pt-br em todos os textos visiveis.

**[views/balance/BalanceParametersView.vue:29-31] — P2 (notificacoes nao persistem e nao tem backend)**
E-mails, SMS e Slack webhook sao coletados e enviados no PUT /balance/parameters (404). Mesmo com endpoint, nao ha modelo no backend para esses campos. Tela passa a falsa impressao de configuracao salva.
Correcao: ou implementar persistencia, ou remover a secao de notificacoes ate existir backend.

**[views/balance/CreditRequestView.vue:64-82,277-315] — P2 (anexos descartados)**
Upload de anexos e implementado na UI (ate 5 arquivos, 10MB), mas `handleSubmit` (linha 88-94) NUNCA envia `attachments` no payload de `createRequest`. O `CreateRequestPayload` (`balance.ts:106-115`) nem tem campo de anexo. Usuario anexa documento de justificativa e ele e silenciosamente descartado. Mesmo em Debito (`DebitRequestView.vue:84-100,402`) e em parte do fluxo.
Correcao: remover o upload (se nao ha backend) ou implementar multipart com persistencia dos anexos.

**[views/balance/BalanceHistoryView.vue:76-94] — P2 (export "PDF" e window.print; "Excel" e CSV)**
O botao "Excel" gera CSV (linha 79-91) e o "PDF" so chama `window.print()` (linha 93). Rotulos enganam o operador. Alem disso o CSV exporta valores crus (`r.openingBalance` etc., linha 82) que estao na mesma unidade ambigua do resto.
Correcao: renomear botoes (CSV/Imprimir) ou implementar exportacao real; alinhar unidade no CSV.

**[views/balance/BalanceDashboardView.vue:334-336,LiquidityInjectionView] — P2 (rota de aporte vs label)**
O atalho leva a `/balance/request/aporte` (existe, `router.ex:522-526`), ok. Mas o texto "Aumentar saldo RB/PI" e "Reduzir saldo RB/PI" referem RB/PI que nao existem como contas separadas no backend (ver P1 do split). Mensagem confunde o operador sobre o que e RB.
Correcao: ajustar copy para a realidade (Conta PI unica) ate existir RB de verdade.

**[views/camt060/Camt060ToolView.vue:308] vs [camt060_controller.ex:3] — P2 (versao divergente na UI)**
A UI diz "BACEN Vol VI v5.12" (linha 308) e o comentario de bundle diz v5.11 (linha 6-8); o controller diz v5.11 (`camt060_controller.ex:3`). Inconsistencia de rotulo de versao exibida ao usuario.
Correcao: padronizar a versao exibida com a baseline real (v5.12 conforme handoff atual).

---

## P3, Polish

**[views/balance/BalanceRequestsListView.vue:8, BalanceParametersView.vue:8, BalanceRequestDetailView.vue (Input import)] — P3 (imports nao usados)**
`Input` e importado e nao usado em varias views (ex.: `BalanceRequestsListView.vue:8`, `BalanceParametersView.vue:8`, `DebitRequestView.vue:8`). `watch` importado e nao usado em `BalanceRequestsListView.vue:2`. Lixo que polui o bundle e o lint.
Correcao: remover imports nao utilizados.

**[views/balance/BalanceDashboardView.vue:36-37,210] — P3 (transactionCount somado com NaN risk)**
`normalizeIntraday` faz `toNumber(transactionCount ?? credit_count) + toNumber(debit_count)` (`balance.ts:210`): quando `transactionCount` existe, ainda soma `debit_count`, podendo duplicar contagem. Inconsistente.
Correcao: definir uma unica origem para a contagem.

**[views/balance/CreditRequestView.vue:35 vs 49-53] — P3 (validacao duplicada e divergente)**
`isValid` (linha 34-36) usa `amount >= 1000 && <= 100000000` e `validateForm` (linha 46-62) repete com mensagens. Duas fontes de verdade da mesma regra; risco de divergir na manutencao.
Correcao: centralizar a validacao.

**[views/balance/BalanceRequestDetailView.vue:214] — P3 (default RB hardcoded)**
`request.targetAccount || 'RB'` assume RB como conta destino padrao, reforçando o conceito RB inexistente.
Correcao: usar 'PI' como default real (ou o que o backend devolver).

**[services/balance.ts:129-145] — P3 (helpers defensivos mascarando contrato)**
`asArray`/`toNumber`/`zeroPosition` toleram qualquer shape, o que esconde os 404 e os mismatches acima (a tela "funciona" mostrando zeros em vez de erro visivel). Util para robustez, mas hoje camufla bugs reais de contrato.
Correcao: depois de alinhar contratos, reduzir o "tolerar tudo" para falhar visivelmente quando o backend muda.

---

## Tabela sintese das 5 suspeitas do dono

1. Reais x centavos (front divide por 100 valor que ja vem em reais): CONFIRMADO. `BalanceDashboardView.vue:45`, `BalanceRequestsListView.vue:50`, `BalanceRequestDetailView.vue:30`, `BalanceHistoryView.vue:44`, `BalanceParametersView.vue:46`. Backend devolve reais (`balance.ex:16-17`, `camt060.ex:90-92`). CAMT.060 (`Camt060ToolView.vue:155-158`) e StatementList (`StatementListView.vue:79`) NAO dividem, por isso divergem.

2. Rotas 404 (front chama endpoint inexistente): CONFIRMADO. `/balance/requests` (GET/POST), `/balance/requests/:id`, `/balance/requests/:id/approve|reject`, `/balance/parameters` (GET/PUT). Router so tem ate `/balance/requests/pending` (`router.ex:83`). Servico em `balance.ts:269,275,280,285,293,307,312`.

3. "Solicitacoes Pendentes" mostrando outbound a R$0,00: CONFIRMADO na origem. `/balance/requests/pending` devolve balance_blocks de saida (`balances.ex:561-585`) criados por `return_processor.ex:128-133` ("outbound_pacs004_return") e `execution_worker.ex:182-187` ("outbound_recurrence"); o front os rotula como DEBIT/PENDING_LSO (`balance.ts:179-201`) e o R$0,00 vem do `/100` somado a valor de bloco baixo/nulo.

4. Split RB/PI que o backend nao produz (RB sempre R$0): CONFIRMADO. `list_all_positions` nao tem `accountType` (`balances.ex:489-497`); `getPositions` filtra por 'RB'/'PI' e cai em `zeroPosition('RB')` (`balance.ts:241-245`).

5. Saldos inconsistentes entre telas: CONFIRMADO a fonte de cada um. Dashboard = posicao agregada do ISPB com `/100` (R$1.000.000 vira R$10.000). CAMT.060 = mesma `Balances.get_balance` SEM `/100` (mostra o valor verdadeiro, ex.: R$999.999,97 se for o saldo real materializado). Diferencas residuais (1.000.000 vs 999.999,97) vem de fontes/momentos distintos: posicao local agregada vs retorno BACEN materializado (`camt060.ex:70-97`, source "bacen"/"calculated") e de bloqueios (`blocked`) que reduzem o disponivel.

Observacao final: a area de "solicitacoes/parametros/aprovacao LSO-RSO" inteira (telas Lista, Detalhe, Credito, Debito, Aporte, Parametros) nao tem backend correspondente; e UI sem persistencia. O unico fluxo de saldo realmente funcional ponta a ponta hoje e a consulta CAMT.060 (`Camt060ToolView.vue` + `/api/v1/camt060/*`), que inclusive e a unica com unidade e timezone corretos.


---

## Telas, botões e contrato front<->back


Confirmed — the gateway forwards `list_balance_requests` and `get_balance_request` to SPI `/api/v1/balance/requests/pending` (so they return the pending list regardless of filter/id — a contract bug: detail-by-id actually returns the pending LIST, not one request), and `create`/`approve`/`reject` forward to SPI paths (`POST /balance/requests`, `PUT /balance/requests/{id}/approve`) that **do not exist in the SPI router** → 404. I have everything needed.

Below is the exhaustive audit.

---

# Auditoria de Funcionalidade, Painel ADMIN PIX (Monetarie)

## Contexto e método

O frontend admin (`/Users/luizpenha/monetarie/pix/frontend/admin/src`) fala com um único gateway, o `settlement_service` na porta 4003 (confirmado em `vite.config.ts`, proxy `/api` para `localhost:4003`, e `services/api.ts:7` com `baseURL` vazio). Toda chamada para um path que NÃO existe no `settlement_service_web/router.ex` retorna 404, mesmo que o path exista no `spi_service` ou no `dict_service` (esses routers são internos, só alcançados via proxies do gateway). Cruzei cada serviço do front (`src/services/*`, `src/stores/*`) com os três routers e os controllers reais. Abaixo, cada problema com arquivo:linha, severidade e correção.

Legenda: P0 quebrado, P1 funcional errado, P2 UX/i18n, P3 polish.

---

## 1. CHAVES (keys)

**[services/keys.ts:37-42] + [views/keys/KeyDetailView.vue:64,81] — P0**
`blockKey` chama `POST /api/v2/entries/{key}/block` e `unblockKey` chama `POST /api/v2/entries/{key}/unblock`. O `dict_service` tem essas rotas (`dict_service_web/router.ex:192-193`), mas o gateway `settlement_service_web/router.ex` (escopo `/api/v2`, linhas 329-379) NÃO proxia `block`/`unblock`. Resultado: os botões Bloquear/Desbloquear retornam 404.
Correção: adicionar no `DictProxyController` + router do gateway as rotas `post "/entries/:key/block"` e `post "/entries/:key/unblock"`.

**[views/keys/KeyDetailView.vue:162,169] — P1**
Os botões Bloquear/Desbloquear só renderizam quando `key.status === 'active'` / `=== 'blocked'` (minúsculo). Mas o backend retorna status MAIÚSCULO: `entry.ex:17` define `@statuses ~w(ACTIVE PENDING BLOCKED DELETED)` e `entry_controller.ex:311` serializa `status: entry.status` cru (ex. "ACTIVE"). Logo os botões nunca aparecem, e `statusColor`/`statusLabel` (linhas 127-142, só chaves minúsculas) mostram badge cinza com texto "ACTIVE". (O `KeyListView.vue:97,107` faz `.toUpperCase()` e funciona, gerando inconsistência entre lista e detalhe.)
Correção: normalizar status com `.toUpperCase()` no detalhe, igual à lista, ou mapear ACTIVE/BLOCKED/PENDING.

---

## 2. PARTICIPANTES (participants)

**[services/participants.ts:195-227] — P3 (código morto perigoso)**
`create`, `update`, `delete`, `approve`, `reject`, `suspend`, `activate` apontam para `OPERATED_PARTICIPANTS_PATH = '/api/v1/participants'` (POST/PUT/DELETE e `/{id}/approve|reject|suspend|activate`). Nenhuma dessas rotas existe no gateway (o router só tem `get "/operations/participants"`, `get "/bacen/pix-participants"`, `get "/entities"`). Hoje nenhuma view chama esses métodos (grep não encontrou usos), então não quebra tela, mas é uma armadilha: se alguém ligar um botão a eles, dá 404.
Correção: remover os métodos mortos ou criar as rotas reais de operação de participante no gateway.

Observação: a `ParticipantsView.vue` (lista do diretório BACEN, somente leitura) está correta e bem normalizada.

---

## 3. RECORRÊNCIAS (recurrences) — TELA QUEBRADA

**[views/RecurrenceListView.vue:223-239] vs [recurrence_controller.ex:228-258] — P1 (lista renderiza tudo vazio/errado)**
O backend `serialize_recurrence` emite **camelCase** com nomenclatura debtor/creditor: `debtorName`, `creditorName`, `debtorIspb`, `creditorIspb`, `nextExecution`, `createdAt`. O front lê `item.payer_name`, `item.receiver_name`, `item.payer_ispb`, `item.receiver_ispb`, `item.next_payment_date` (snake_case, payer/receiver). Todas as colunas de pagador, recebedor, ISPBs e próximo pagamento saem "-"/vazias.

**[views/RecurrenceListView.vue:64] — P1 (valor errado)**
`formatCurrency` faz `value / 100`, mas o backend (`format_amount`, recurrence_controller.ex) retorna `Decimal.to_string` em reais (ex. "150.00"). `"150.00" / 100` = 1.5, ou seja, valor exibido fica errado por fator 100.

**[services/recurrence.ts:25] — P1 (paginação errada)**
`total` lê `response.data.total_count || response.data.total`, mas o backend retorna `{data, meta: {total}}`. Cai no fallback `data.length`, então a paginação fica errada.

**[views/RecurrenceListView.vue:129-136] — P1 (botão morto)**
O botão "Nova Recorrência" não tem `@click`. Não faz nada (não há rota nem view de criação de recorrência registrada).
Correção: alinhar os nomes de campo (camelCase debtor/creditor), remover o `/100` (amount já vem em reais), ler `total` de `meta.total`, e ligar/remover o botão "Nova Recorrência".

---

## 4. SESSÕES (sessions)

**[views/sessions/SessionListView.vue:70-97,232-254] — P2**
As ações Abrir/Fechar/Finalizar/Cancelar executam direto no clique, SEM diálogo de confirmação. Cancelar/Finalizar uma sessão de liquidação é destrutivo e deveria confirmar.

**[views/sessions/SessionListView.vue:88] + [services/session.ts:87] — P3**
`cancelSession(id)` envia `{ reason }` mas a view não coleta motivo, então manda `reason: undefined`. Se o backend exigir motivo de cancelamento, falha; senão, perde rastreabilidade.
Correção: adicionar modal de confirmação (e captura de motivo no cancelamento).

---

## 5. ALÇADA / LIMITES DE APROVAÇÃO (Approval Limits) — CRUD QUEBRADO

**[views/participants/ApprovalLimitsView.vue:125] vs [alcada_controller.ex create_parameter] — P0 (criar falha)**
A view posta payload **plano** (`{operation_type, min_amount, max_amount, required_approvals, approver_groups, ...}`) para `/api/v1/alcada/parameters`. O gateway repassa cru (`spi_proxy_controller.ex:704-712`), mas o SPI `create_parameter` casa SOMENTE com `%{"parameter" => attrs}` (wrapper aninhado). Sem a chave `parameter`, dá FunctionClauseError → 500. Criar limite não funciona.

**[views/participants/ApprovalLimitsView.vue:123] — P0 (editar 404)**
`PUT /api/v1/alcada/parameters/{id}`: o escopo `/alcada` do gateway (`settlement_service_web/router.ex:492-499`) só tem `get parameters`, `get parameters/:id`, `post parameters`, `post check`, `get registrations`, `post registrations/:id/visto`. Não há `put`. Editar dá 404.

**[views/participants/ApprovalLimitsView.vue:140] — P0 (excluir 404)**
`DELETE /api/v1/alcada/parameters/{id}`: não existe rota de delete de alçada em nenhum router. Dá 404. Pior: a linha 141 remove a linha da lista local de forma otimista mesmo após o 404, dando falsa sensação de sucesso.

**[views/participants/ApprovalLimitsView.vue:103-104,130,143] — P1**
Erros são engolidos só com `console.error`; o usuário não vê falha alguma. A ação parece não fazer nada.
Correção: enviar payload com wrapper `{parameter: {...}}` no create; adicionar rotas `put`/`delete` de alçada no gateway+SPI; exibir erro ao usuário.

---

## 6. PARTICIPANTS STORE — endpoints inexistentes (várias subpáginas)

Cruzando `stores/participants.ts` com os routers (nenhuma destas existe no gateway, confirmado por grep em todos os routers):

**[stores/participants.ts:211,224,239,257] — P0** `/api/v1/approval-limits` (GET/POST/PUT/DELETE) → 404. (Hoje a `ApprovalLimitsView` usa `/alcada/parameters` direto e não esses, mas o método do store é uma armadilha morta — ver item 5.)
**[stores/participants.ts:298] — P0** `/api/v1/rco/operations` → 404. (A `RcoMonitorView` na verdade chama `/api/v1/returns` direto na view:476, que existe; o método do store é morto.)
**[stores/participants.ts:313] — P1** `/api/v1/reports/response-time` → 404 (rota não existe; a `ResponseTimeReportView:399` usa `/audit/logs/stats?report=response-time`, que existe mas ignora o param).
**[stores/participants.ts:272] — P1** `/api/v1/statistics/messages` → 404 (o backend só tem `/messages/statistics` e `/participants/statistics`; a `MessageStatisticsView:56` usa `/audit/logs/stats`, que existe).
**[stores/participants.ts:326,339,352,370] — P0** `/api/v1/config/legacy-parameters`, `/api/v1/config/legacy-connections`, `/api/v1/config/legacy-connections/{id}/test` → 404 (escopo `/config` do gateway só tem parameters/schedules/security/categories).
Correção: remover os métodos mortos do store que apontam para rotas inexistentes, ou criar as rotas. Eles confundem manutenção e podem ser ligados por engano.

---

## 7. PARÂMETROS LEGADOS (Legacy Parameters)

**[views/participants/LegacyParametersView.vue:494] — P0**
`POST /api/v1/health/services/{connectionId}/test` → o gateway só tem `get "/health/services"` (router.ex:68). O botão "Testar conexão" dá 404.
Correção: criar a rota de teste de conexão no gateway, ou remover o botão até existir o backend.

---

## 8. LOGS DE AUDITORIA (Audit Logs) — FILTROS E PAGINAÇÃO QUEBRADOS

**[views/AuditLogsView.vue:33-44] vs [audit_log_controller.ex:158-167 list_audit_logs] — P1 (filtros não funcionam)**
A view envia `eventType`, `result`, `startDate`, `endDate`. O `list_audit_logs` só lê `action`, `resource_type`, `user_id`, `date_from`/`date_to` (e aliases `dateFrom`/`dateTo`). Então:
- Filtro de tipo de evento (`eventType`) é ignorado (o controller espera `action`).
- Filtro de resultado (`result`) não existe no controller, é ignorado.
- Datas (`startDate`/`endDate`) são ignoradas (o controller só aceita `dateFrom`/`dateTo`).
Curiosamente o **export** (`export_audit_logs:169-178`) aceita `startDate`/`endDate`/`eventType` com fallback, então o filtro vale no CSV mas não na tela. Assimetria.

**[services/audit.ts:49] + [views/AuditLogsView.vue:48-49] — P1 (paginação zerada)**
`auditService.list` devolve `response.data` cru, e a view lê `response.total` e `response.totalPages`. Mas o backend retorna `{data, meta: result.meta}` (audit_log_controller.ex:71-73). `total`/`totalPages` vêm de `meta`, não do topo. `totalRecords` fica 0 e `totalPages` fica 1: botão "Próximo" sempre desabilitado e contador mostra 0.

**[services/audit.ts:24] vs [audit_log_controller.ex:283-304] — P1 (coluna Resultado vazia)**
O type declara `result: 'SUCCESS' | 'FAILURE'`, mas o `audit_log_to_json` não emite `result` (emite `response_status`). A coluna/realce de resultado fica sempre vazia.
Correção: na view, enviar `action`/`dateFrom`/`dateTo` (e derivar `result` de `response_status`); no service, ler `total`/`totalPages` de `response.data.meta`. Idealmente, padronizar o controller para também aceitar `eventType`/`startDate`/`endDate` na listagem como já faz no export.

---

## 9. SALDO / SOLICITAÇÕES DE SALDO (Balance Requests) — SUBSISTEMA QUEBRADO

**[services/balance.ts:280] + [stores/balance.ts:111] vs SPI router — P0 (criar 404/500)**
`createRequest` → `POST /api/v1/balance/requests`. O gateway repassa para o SPI `POST /api/v1/balance/requests` (`spi_proxy_controller.ex:389-396`), mas o SPI router só tem `get "/requests/pending"` (`spi_service_web/router.ex:83`). Não existe `POST /balance/requests`. As telas Crédito (`CreditRequestView:88`), Débito, Aporte e Injeção de Liquidez falham no envio.

**[services/balance.ts:269,275] vs [spi_proxy_controller.ex:343-367] — P1 (lista e detalhe retornam errado)**
`list_balance_requests` e `get_balance_request` no gateway encaminham para `/api/v1/balance/requests/pending` (linhas 349 e 366). Ou seja, a tela de detalhe por id (`BalanceRequestDetailView`) na verdade recebe a LISTA de pendentes, ignorando o id; e a lista geral só mostra pendentes, nunca aprovados/rejeitados.

**[services/balance.ts:285,293] vs SPI router — P0 (aprovar/rejeitar 404)**
`approveRequest`/`rejectRequest` → `POST /api/v1/balance/requests/{id}/approve|reject`. O gateway encaminha para SPI `PUT/POST /api/v1/balance/requests/{id}/approve` (spi_proxy_controller.ex:404+), rota que não existe no SPI router. 404.
Correção: implementar no SPI as rotas `POST /balance/requests`, `GET /balance/requests`, `GET /balance/requests/:id`, `.../approve`, `.../reject`, e corrigir o gateway para não mandar tudo para `/pending`.

---

## 10. POLÍTICAS DICT (Dict Policies)

**[services/dict.ts:78,83] + [views/dict/DictPoliciesView.vue:67-72,212] — P0**
`createPolicy` → `POST /api/v2/admin/institutions`, `updatePolicy` → `PUT /api/v2/admin/institutions/{id}`. O gateway só expõe `get "/institutions"` e `get "/institutions/:ispb"` (router.ex:376-377). Salvar política dá 404.
Correção: criar no gateway/DICT as rotas POST/PUT de instituições, ou desabilitar o formulário de criação/edição até existirem.

---

## 11. ESCOPOS / PERMISSÕES (security/ScopesView) — OK, com ressalva de duplicata

A `views/security/ScopesView.vue` (a rota real, importada em `router/index.ts:29`) usa `scopeService` de `services/security.ts`, que bate em `/api/v1/permissions/groups|features|groups/{id}/features`, todas existentes (router.ex:701-705). Contrato de `updateGroupFeatures` (`{features: [...]}`) confere com `permission_controller.ex update_group_features`. Funciona.

**[views/admin/ScopesView.vue] — P3 (componente órfão com endpoints 404)**
Existe uma SEGUNDA `ScopesView` em `views/admin/`, que usa `scopesService` (`services/scopes.ts`) batendo em `/api/v1/admin/groups-scopes`, `/api/v1/admin/groups/{id}/scopes` e `/preview` — nenhuma existe em router algum. Esse arquivo NÃO está roteado (o router usa o de `views/security`), então é código morto, mas é uma armadilha de 404 se for ligado.
Correção: remover `views/admin/ScopesView.vue` + `services/scopes.ts`, ou ligar às rotas reais de permissões.

---

## 12. CERTIFICADOS (admin/CertificatesView)

**[services/certificates.ts:198] — P0 (provável)**
`runSmokeTest` → `POST /api/v1/admin/bacen/smoke-test`. Essa rota não existe em nenhum router (grep não encontrou `smoke-test`). O botão "Smoke test BACEN" (`CertificatesView.vue:466,471,1168`) tende a dar 404.
Correção: implementar o endpoint de smoke-test no gateway ou ocultar o botão.

As demais operações de certificado (`/api/v1/admin/certificates*`, import, upload-pfx, activate, deactivate) existem (router.ex:625-637) e estão alinhadas.

---

## 13. TRANSAÇÕES (transactions)

A lista (`TransactionListView` → store → `/api/v1/transactions`) está alinhada: o backend retorna `{data, meta:{total}}` e o front lê corretamente (`services/transaction.ts:89-92`). Filtros `status`/`end_to_end_id`/`start_date`/`end_date`/`instrument_type` são honrados (`payment_controller.ex index`).

**[views/transactions/TransactionCreateView.vue:154] — P2**
O campo ISPB do Recebedor é marcado com `*` (obrigatório) mas, ao contrário do Pagador (linha 131, com `required`), não tem atributo `required`. A validação só ocorre no JS (linha 50) exigindo creditor_ispb OU pix_key, o que é inconsistente com o rótulo "*".

**[views/transactions/TransactionCreateView.vue:22,38-44] — P3**
Default de instrumento é `IPMF`, que não é um código de forma de iniciação BACEN padrão (DICT/MANU/QRES/QRDN/INIC). Vai para `initiation_form` no backend (`payment_controller.ex`) sem validação.

---

## 14. QR CODES (qrcodes)

Contrato de criação alinhado (`qr_code_controller.ex create_static/create_dynamic` lêem os mesmos campos).

**[views/qrcodes/QrCodeCreateView.vue:131-133] — P3**
Para QR estático o formulário permite informar valor, mas `create_static` ignora `amount` (não lê o campo). O valor digitado é silenciosamente descartado. A nota "valor é opcional" não deixa claro que será ignorado.

---

## 15. MED (med-hub, refunds, infractions, fraud-markers, funds-recoveries)

Área bem construída. `med.ts` está alinhado com os proxies do gateway (`/api/v2/funds-recoveries`, `/infraction-reports`, `/fraud-markers`, `/refund-requests` — todos em router.ex:344-372). A `RefundListView` tem modais de confirmação corretos para cancelar/fechar, e trata o caso de `recovery_id` obrigatório com empty-state explicativo. `MedHubView` é defensivo (`.catch` por contexto). Sem P0/P1 aqui.

**[views/med/MedHubView.vue:44-47] — P3**
Os KPIs usam `total` de cada listagem chamada com `per_page: 5`; se o backend devolver `total` = tamanho da página (5) em vez do total real, o número do card fica subdimensionado. Vale validar o `total` retornado por cada endpoint MED.

---

## 16. MONITOR DE OPERAÇÕES, CAMT.060, CID SYNC

`OperationsMonitorView` (`/api/v1/admin/monitor/*`, router.ex:415-418), `Camt060ToolView` (`/api/v1/camt060/*`, router.ex:471 proxia para SPI camt060) e `CidSyncView` (`/api/v1/admin/sync/cid/*`, router.ex:420-423) estão alinhados com o backend. Telas de alta qualidade, sem divergência de contrato detectada. (Observação informativa: o camt060 exige `ispb` ou cai no ISPB de config, `camt060_controller.ex run` — comportamento correto.)

---

## Resumo por severidade

P0 (quebrado, retorna 404/500 ou ação não executa):
- Chaves: block/unblock não proxiados (keys.ts:37-42)
- Alçada/Limites: criar (wrapper `parameter` faltando), editar (sem PUT no gateway), excluir (sem rota) — ApprovalLimitsView:123,125,140
- Parâmetros legados: testar conexão (LegacyParametersView:494)
- Saldo: criar/aprovar/rejeitar solicitação e detalhe por id (balance.ts:280,285,293; SPI sem as rotas)
- Políticas DICT: salvar (dict.ts:78,83)
- Certificados: smoke-test BACEN (certificates.ts:198)
- Store de participantes: 6 endpoints mortos inexistentes (participants.ts:211,272,298,313,326,339,352,370)

P1 (funcional errado, tela carrega lixo/vazio):
- Recorrências: lista inteira com campos errados + valor /100 + paginação + botão morto (RecurrenceListView)
- Logs de Auditoria: filtros eventType/result/datas ignorados, paginação zerada, coluna resultado vazia (AuditLogsView + audit.ts)
- Chave (detalhe): botões de status nunca aparecem por case-sensitivity (KeyDetailView:162,169)
- Saldo: lista/detalhe sempre retornam só "pendentes"

P2/P3: confirmações ausentes em sessões; `required` faltando no ISPB recebedor; amount ignorado em QR estático; componente `admin/ScopesView` órfão com 404; defaults de instrumento/IPMF; KPIs MED com `total` possivelmente subdimensionado.

Causa-raiz recorrente: o gateway `settlement_service` não proxia várias rotas que existem no `spi_service`/`dict_service` (block/unblock de chave, PUT/DELETE de alçada, POST/approve/reject de balance requests, POST/PUT de instituições), e há divergência sistemática de nomenclatura (camelCase debtor/creditor no backend vs snake_case payer/receiver no front em recorrências; `eventType/result/startDate` vs `action/dateFrom` em auditoria) e de envelope (`{data, meta}` lido como topo-nível).


---

## Layout, UX e design


This confirms the SettingsView exposes a "Tema" (Theme) light/dark selector to the user, using `bg-blue-50/text-blue-700/focus:ring-blue-500` (off-brand blue) and full `dark:` classes, but since Tailwind has no dark variant configured and `style.css` has no dark tokens, selecting "Escuro" does nothing visible (broken feature shown to the operator). That's a P1.

I now have comprehensive, concrete evidence. Let me write the final audit report.

---

# Auditoria de Layout / UX / Design — Painel Admin PIX (Monetarie)

Escopo: layout principal (`AdminLayout.vue`, `Sidebar.vue`, `Header.vue`), componentes compartilhados (`components/ui`, `components/common`, `components/layout`), folha de estilo global (`style.css`) e amostra ampla de views. Referência de acabamento desejado: `spb/frontend-vue` (Vuetify, nav agrupada, footer, transições, snackbars).

Convenção das severidades: P0 quebrado, P1 funcional-errado, P2 UX/i18n, P3 polish. Caminhos relativos a `/Users/luizpenha/monetarie/pix/frontend/admin/src/`.

## Resumo executivo (por que o front "confunde o operador")

O painel não tem um sistema de design aplicado. Existem tokens e classes utilitárias boas em `style.css` (`.page-header`, `.page-title`, `.card`, `.badge`, `.empty-state`, `.kpi-card`), mas a maioria das telas ignora esses tokens e reescreve o cabeçalho, os cards, os badges e as tabelas na mão, cada uma com cores, raios de borda, espaçamentos e rótulos diferentes. O resultado é exatamente o que o dono descreveu: telas parecidas que não batem entre si.

Números concretos coletados no código:
- Só 9 views usam a classe compartilhada `.page-header`; 93 views reescrevem o cabeçalho com `text-2xl font-...` próprio.
- 152 ocorrências de título de página em `text-gray-900` (cinza puro) em vez do token de marca `--color-text-primary` (#101820).
- 471 usos de cores azul/índigo/sky (`bg-blue-*`, `text-blue-*`, `text-indigo-*`, `bg-sky-*`) num produto cuja marca é navy escuro + ouro. Azul não pertence à paleta.
- 64 views não têm nenhuma chamada de i18n (`t()`): texto fixo em português. O resto da app é i18n com 5 idiomas. Trocar de idioma traduz o menu e poucas telas; o miolo continua em português (inconsistência grave e visível).
- 27 views reimplementam a própria função de rótulo/cor de status, apesar de existir `utils/statusLabel.ts` (usado só em 12).
- Raio de borda sem padrão: 790 `rounded-lg` vs 112 `rounded-md` vs 48 `rounded-xl`. Os componentes-base (`Card.vue`, `Button.vue`, `.card`, `.badge`) usam `rounded-md`; as views usam majoritariamente `rounded-lg`. Cards e botões nunca casam.
- Padding de célula de tabela em 3 padrões: `px-6 py-4` (26 views), `px-4 py-3` (45 views), `p-3` (9 views).

---

## P0 — Quebrado

**[components/common/HeroKpiCard.vue:28] Badge "blue" com cor de fundo e texto incompatíveis (ilegível).**
O mapa de cores define `blue: 'bg-blue-900/40 text-emerald-700 border border-emerald-200'`: fundo azul-marinho escuro com texto verde-escuro, contraste praticamente nulo. Além disso o nome ("blue") não corresponde ao valor (verde). As demais variantes (`green/red/amber`) usam fundos escuros `bg-*-900/40` com texto claro, pensadas para superfície escura, mas o card é `bg-white` (linha 36), então todos os badges escuros ficam sobre fundo branco. Correção: redesenhar o mapa para superfície clara (ex.: `blue: 'bg-sky-50 text-sky-700 border-sky-200'` ou, melhor, alinhar à paleta navy/ouro) e remover o badge escuro sobre card claro.

**[components/common/HeroKpiCard.vue (arquivo inteiro)] Componente de KPI "herói" é código morto.**
`HeroKpiCard` não é importado em nenhuma view (0 usos). Ou seja, o componente "oficial" de KPI nunca é usado e cada tela desenha o KPI à mão (ver Monitor linhas 316-331, BalanceDashboard linhas 362-388, Dashboard linhas 399-440), o que perpetua a divergência visual. Correção: adotar um único componente de KPI em todas as telas (depois de corrigir o bug acima) ou removê-lo e padronizar `.kpi-card`.

---

## P1 — Funcional-errado

**[views/settings/SettingsView.vue:164-197 + stores/ui.ts:32] Seletor de Tema (claro/escuro) exposto ao operador mas sem efeito.**
A tela de Configurações mostra um seletor "Tema" com opções claro/escuro e a store (`ui.ts`) faz `document.documentElement.classList.toggle('dark', ...)`. Porém: (1) não há configuração de dark mode no Tailwind (nenhum `darkMode`/`@custom-variant dark` em config/`style.css`); (2) `style.css` não define tokens para `.dark` (paleta só clara). Logo, escolher "Escuro" não muda nada visível. É uma funcionalidade mostrada ao usuário que não funciona. Correção: ou implementar dark mode de fato (variante + tokens `--color-*` no `html.dark`), ou remover o seletor de tema.

**[8 arquivos com classes `dark:` — ex. views/participants/RcoMonitorView.vue:193, views/settings/SettingsView.vue:98-197, views/monitoring/RealTimeMonitoringView.vue] Classes `dark:` espalhadas que nunca aplicam.**
Como não há variante dark configurada, todo `dark:text-white`, `dark:bg-gray-800` etc. é inerte. Mistura de telas "preparadas para dark" e telas sem isso, sem nenhum dark real. Correção: remover as classes `dark:` (ou só mantê-las se o dark mode for implementado de verdade).

**[views/transactions/TransactionListView.vue:33 e 27 views similares] Status PIX `ACSP` pintado de azul (fora da paleta) e lógica de status duplicada por tela.**
O mapa de cor de status usa `ACSP: 'bg-blue-100 text-blue-800'` (azul), enquanto a marca não tem azul; pior, a função é reescrita em 27 views (cada uma com seu mapa de cores/rótulos), então o mesmo status pode aparecer com cor diferente em telas diferentes. Já existe `utils/statusLabel.ts` (usado só em 12 views). Correção: centralizar rótulo e cor de status num único util/componente de Badge de status e usar em todas as telas; trocar azul por um tom da paleta (ex.: cinza-azulado neutro ou o ouro suave da marca para "em processamento").

**[components/layout/Header.vue:79] Header sem breadcrumb nem título de página (só um comentário "could go here").**
O cabeçalho global é praticamente vazio do lado esquerdo (só o botão de menu mobile). Não há breadcrumb nem título da rota atual. `BreadcrumbNav.vue` existe mas é usado em 1 única view (`ParticipantDetailView.vue`). O operador perde a noção de onde está, ainda mais com a sidebar de menus profundos. Correção: renderizar título/breadcrumb da rota no Header (a partir de `route.meta`), aproveitando o `BreadcrumbNav` já existente.

---

## P2 — UX / i18n

**[Sidebar.vue:54-68 + locales/pt-BR.json:168-170] Rótulos longos no menu com `truncate` em sidebar de largura fixa (os "..." que o dono viu).**
A sidebar é `w-64` fixa (Sidebar.vue:426) e cada item usa `class="truncate"` (linhas 489, 528, 553). Rótulos como `nav.operationsMonitor` = "Monitor de Operações" e `nav.camt060` = "Consulta Saldo (CAMT.060)" não cabem no espaço útil (64 menos ícone, padding e a seta de expandir), então cortam para "Monitor de Operaço..." e "Consulta Saldo (CA...". Correção: encurtar os rótulos no i18n (ex.: "Monitor de Operações" e "Saldo (CAMT.060)"), aumentar a largura da sidebar, ou usar `title` + quebra em duas linhas em vez de `truncate`. Hoje só o modo colapsado tem `title`; o modo expandido truncado não tem tooltip.

**[64 views sem i18n — lista completa abaixo] Metade das telas com texto fixo em português; troca de idioma quebra a consistência.**
Views sem nenhuma chamada `t()`, todas hardcoded em pt-BR: `camt060/Camt060ToolView.vue`, todas as 8 de `balance/*`, todas as de `accounting/*` (FeeManagement, AccountingDashboard, SettlementReport, Reconciliation, NettingCycles), todas as de `claims/*`, `keys/*`, `transactions/*`, `statements/*`, `messages/*`, `qrcodes/*`, `reports/*`, `simulator/*`, `alcada/*`, `sessions/*`, `participants/*` (vários), `sync/CidSyncView.vue`, `RecurrenceListView.vue`, `settings/SettingsView.vue`, `security/CertificateManagementView.vue`, `auth/LoginView.vue`. Como o seletor de idioma do Header oferece 5 idiomas, ao escolher inglês o menu vira inglês mas o conteúdo destas 64 telas continua em português. Correção: ou externalizar os textos destas telas para i18n, ou (decisão de produto) reduzir o seletor para pt-BR enquanto a tradução não está completa, evitando o estado meio-traduzido.

**[locales/pt-BR.json:93] Vazamento de inglês dentro do locale português: `common.pages.auditLogs` = "Audit Logs".**
No próprio arquivo pt-BR há um valor em inglês. Há também duplicidade conceitual: `nav.auditLogs` = "Logs de Auditoria" (certo) vs `common.pages.auditLogs` = "Audit Logs" (errado). E `nav.claims` = "Reivindicações" convivendo com `nav.claimsShort` = "Claims" e `common.pages.claims` = "Claims": o mesmo conceito aparece traduzido e em inglês. Correção: padronizar tudo para pt-BR ("Logs de Auditoria", "Reivindicações") e remover os termos em inglês.

**[views/balance/BalanceDashboardView.vue:181,193,211,223,241 e 11 ocorrências em balance/claims/reports] Acentuação faltando em rótulos visíveis.**
Exemplos com arquivo:linha: BalanceDashboardView "RESERVA BANCARIA" (l.181), "24h atras" (l.193 e 223), "PIX INSTANTANEO" (l.211), "SALDO DISPONIVEL TOTAL" (l.241), "Creditos Hoje" (l.365), "Debitos Hoje" (l.372), "Saldo Liquido" (l.378), "Concluida" (l.89); BalanceRequestsListView.vue:80,253,254 "Concluida"/"Rejeitada"; BalanceRequestDetailView.vue:60 "Concluida"; BalanceHistoryView.vue:80 cabeçalho CSV "Creditos,Debitos"; LiquidityInjectionView.vue:215 "Injecao de liquidez"; CustomReportView.vue:392-393 "Total Creditos"/"Total Debitos"; ClaimListView.vue:29,100 e ClaimDetailView.vue:112 "Concluida". Correção: corrigir acentos (Reserva Bancária, Instantâneo, atrás, Disponível, Créditos, Débitos, Líquido, Concluída, Rejeitada, Injeção).

**[views/camt060/Camt060ToolView.vue (todo) e demais views hardcoded] Mistura de telas i18n e telas fixas no mesmo fluxo.**
O `Camt060ToolView` (Consulta Saldo, item que o dono citou) é 100% texto fixo ("Parâmetros", "Resposta", "Consultando BACEN...", "Saldo Conta PI", "Materializado", "Aguardando retorno"), enquanto o `OperationsMonitorView` ao lado é totalmente i18n. O operador percebe telas de "qualidades" diferentes. Correção: alinhar (i18n em ambas) e reusar os tokens `.page-header`/`.page-subtitle` (que essa tela até usa parcialmente, linhas 304-309, mas o resto é fixo).

**[components/layout/InstitutionSelector.vue:81 e ~20 templates] Enum cru renderizado direto na UI (sem rótulo amigável).**
O seletor de instituição mostra `{{ inst.status }}` (valor bruto "active"/"inactive") e `{{ inst.type }}` direto. Mesmo padrão em AccountingEventsView.vue:186 (`{{ event.status }}`), AccountingMonitorView.vue:188,196, RcoMonitorView.vue:314, LegacyParametersView.vue:53, MessageDetailView.vue:265, UserDetailView.vue:193, ReturnListView.vue:115, RealTimeMonitoringView.vue:119,188,208, etc. O operador vê códigos técnicos em inglês/maiúsculas em vez de rótulos. Correção: mapear status/type para rótulo traduzido (centralizado, ver P1 de status).

**[views/balance/BalanceDashboardView.vue:285, BalanceRequestsListView.vue:290, BalanceRequestDetailView.vue:204] Badge de urgência mostra o valor cru (`{{ request.urgency }}`) enquanto tipo e status são traduzidos.**
Na mesma linha de badges, type e status passam por `getTypeLabel`/`getStatusLabel`, mas urgency é renderizado cru ("CRITICAL", "HIGH"). Inconsistência dentro do mesmo componente. Correção: aplicar `getUrgencyLabel` análogo.

**[views/balance/BalanceDashboardView.vue:272-274, views/monitor/OperationsMonitorView.vue:421-423] Estados vazios pobres (uma linha de texto cinza, sem ícone/CTA), enquanto outras telas têm empty-state rico.**
BalanceDashboard mostra só "Nenhuma solicitação pendente"; o Monitor mostra só "operationsMonitor.empty". Já o Dashboard (DashboardView.vue:401-406, 448-453) e a classe `.empty-state`/`.empty-state-icon` em `style.css` oferecem empty-state com ícone. O tratamento de vazio é desigual entre telas. Correção: usar o componente/classe `.empty-state` padrão em todas as listas.

**[views/monitor/OperationsMonitorView.vue:316-331] KPIs do Monitor com largura mínima fixa (`min-w-28`) que estoura/empurra o layout no header.**
Os 4 cartões de KPI (Total, Sucesso, Erros, p95) ficam num flex com `AutoRefreshControl` no mesmo header e cada um com `min-w-28`; em telas médias isso quebra de forma desajeitada (já existe `flex-wrap`, mas sem grid os cartões pulam de linha de forma irregular). Correção: mover os KPIs para um grid responsável dedicado (como faz `.kpi-card`/grid no Dashboard), separado da barra de auto-refresh.

---

## P3 — Polish (consistência visual)

**[style.css:160-170 vs uso real] Cards e botões com raio de borda divergente da app.**
Os tokens base (`.card`, `.kpi-card`, `.badge`, `Card.vue:14`, `Button.vue:25`) usam `rounded-md`, mas as views usam 790x `rounded-lg` e 48x `rounded-xl`. Cards "oficiais" (md) ao lado de cards à mão (lg) na mesma tela. Correção: escolher um raio único (sugiro `rounded-lg` por ser o mais usado) e alinhar os tokens base e as classes utilitárias a ele.

**[múltiplas views] Três padrões de padding de célula de tabela.**
`px-6 py-4` (accounting, 26 views), `px-4 py-3` (45 views) e `p-3` (Monitor e outras, 9 views). Tabelas com densidades diferentes entre telas. Correção: definir uma classe `.table-cell` única e aplicar.

**[views/DashboardView.vue:50-74, 296-305] Nomes de cor enganosos no Dashboard (`color: 'blue'` mapeado para sky, `'purple'` para teal).**
O array de stats declara `color: 'blue'`, `'green'`, `'yellow'`, `'purple'`, mas `colorClasses` mapeia blue→sky, purple→teal. Além de confuso para manutenção, introduz sky/teal (fora da paleta navy/ouro). Correção: usar nomes coerentes e cores da marca (ouro para o KPI principal, neutros para os demais).

**[views/DashboardView.vue:323, BalanceDashboardView.vue:146 vs camt060:306] Cabeçalho de página em três estilos diferentes.**
Dashboard usa `text-2xl font-bold text-gray-900`; BalanceDashboard usa `text-2xl font-bold text-gray-900` com subtítulo `text-gray-600`; Camt060 usa as classes corretas `.page-title`/`.page-subtitle`. Pesos (`font-bold` vs `.page-title` que é `font-semibold`) e cores de subtítulo (`text-gray-600` vs `--color-text-muted`) divergem. Correção: todas as telas adotarem `.page-header`/`.page-title`/`.page-subtitle`.

**[Sidebar.vue:42-379] Agrupamento e nomenclatura do menu desbalanceados; comparar com SPB.**
A sidebar tem 4 itens "soltos" sem seção no topo (Dashboard, Monitor de Operações, Sincronização DICT, Consulta Saldo) e depois seções grandes; algumas "seções" têm 1 item só (`sidebar.sections.automation` → só "Mensagens Agendadas"; `sidebar.sections.simulator` → só "Simulador"), o que gera cabeçalhos de seção para um único link. No SPB (referência, `spb/.../Sidebar.vue`) tudo é agrupado em `v-list-group` colapsável com ícones MDI consistentes. Aqui os ícones são paths SVG escritos à mão e repetidos (o mesmo path de "gráfico de barras" aparece em dashboard, monitoring, monitors, accounting-monitor), então itens diferentes têm o mesmo ícone. Correção: consolidar seções de 1 item, padronizar ícones (idealmente uma biblioteca como o MDI do SPB) e evitar paths duplicados para conceitos distintos.

**[Sidebar.vue:560-568] Rodapé da sidebar redundante ("Monetarie | PIX") repetindo o que o logo já diz; SPB usa o rodapé para ação útil (botão de Ajuda).**
No SPB o `template #append` traz um botão "Ajuda" (`AppLayout`/`Sidebar.vue:997-1004`). Aqui o rodapé é só um texto estático repetindo a marca. Correção: usar o rodapé para algo útil (versão do build, link de ajuda/suporte) ou remover.

**[AdminLayout.vue:41] Conteúdo sem largura máxima nem container central; SPB usa `v-container` + transição de rota.**
O `main` é só `px-4 py-5 sm:px-6 lg:px-8` sem `max-w`/`mx-auto`, então em telas largas o conteúdo estica de ponta a ponta de forma desigual entre telas densas e telas com pouco conteúdo. Não há transição entre rotas (o SPB tem `<transition name="fade">`, AppLayout.vue:37-41). Correção: opcionalmente limitar largura de leitura e adicionar transição suave de troca de rota.

**[components/layout/Header.vue:43-62] Seletor de idioma oferece 5 idiomas que, na prática, traduzem só parte da app (ver P2 i18n).**
Oferecer English/Español/繁體中文/Français dá a impressão de suporte completo, mas 64 telas ficam em português. Em homologação isso pode confundir o avaliador. Correção: alinhar a oferta de idiomas ao que está realmente traduzido.

**[components/layout/Header.vue:127-130] Avatar do usuário usa fundo navy com inicial em ouro; consistente, mas a área esquerda do header fica vazia (ver P1 breadcrumb).** Polish de equilíbrio do header: hoje todo o peso visual está à direita (idioma, instituição, sino, usuário) e a esquerda vazia. Correção: preencher a esquerda com título/breadcrumb.

---

## Comparação com o padrão desejado (SPB)

O SPB (`spb/frontend-vue`) está "mais bonito" por motivos estruturais, não só estéticos:
1. Usa um design system pronto (Vuetify 3): cards, listas, badges, drawer, snackbars e tipografia já vêm coerentes; o PIX é Tailwind cru montado tela a tela, sem componentes reaproveitados (HeroKpiCard morto, StatusTabs usado 1x, BreadcrumbNav 1x, Badge ui 30x mas concorrendo com 27 implementações próprias).
2. Nav agrupada e colapsável (`v-list-group`) com ícones MDI uniformes e rodapé com ação (Ajuda); o PIX tem seções de 1 item, ícones SVG duplicados e rodapé decorativo.
3. Layout com container, transição de rota e banner de expiração de sessão (`AppLayout.vue`); o PIX não tem container, nem transição, nem aviso de sessão.
4. Tokens de tema reais com modo claro/escuro funcionando (`--flx-*`, `.spb-sidebar` usa `--flx-sidebar-active-bg`); no PIX o dark mode é exposto mas inerte.

Caminho recomendado (sem corrigir agora, só direção): adotar de fato os tokens/classes já existentes em `style.css` em todas as telas, eliminar azul/índigo/sky em favor da paleta navy/ouro, centralizar status/badge e KPI em componentes únicos, completar (ou restringir) o i18n, e corrigir o dark mode (implementar ou remover). Isso fecha a maior parte das divergências apontadas.