# PIX admin — Full deep audit: plano completo com cobertura 100% (2026-06-25)

Auditadas **102 paginas / ~120 rotas** em 13 secoes + 4 eixos transversais. **300 achados** (90 high, 120 medium, 90 low). 30 telas quebradas/mock. Distribuicao por camada: 237 front, 34 back, 29 ambos.

Regra: BACEN nunca erra; pt-BR com acento, sem travessao de IA; cada achado tem arquivo:linha e fix.

## Matriz de cobertura (secao -> status)

| Secao | Paginas | works | partial | broken | mock |
|---|---|---|---|---|---|
| dashboard_monitor_camt | 4 | 0 | 2 | 2 | 0 |
| transactions | 4 | 0 | 3 | 1 | 0 |
| keys_qr | 5 | 0 | 5 | 0 | 0 |
| statements_extratos_sessions | 3 | 0 | 1 | 2 | 0 |
| claims_med | 10 | 8 | 2 | 0 | 0 |
| messages_recurrence | 1 | 0 | 1 | 0 | 0 |
| participants_ops | 16 | 1 | 7 | 8 | 0 |
| balance_alcada | 11 | 0 | 5 | 6 | 0 |
| accounting | 11 | 3 | 4 | 3 | 1 |
| security | 10 | 2 | 5 | 3 | 0 |
| reports | 10 | 2 | 6 | 2 | 0 |
| dict_monitors | 7 | 4 | 3 | 0 | 0 |
| simulator_system | 10 | 4 | 4 | 2 | 0 |


## Onda A — Paineis quebrados / dados (broken_data, wrong_field, backend)  (102 itens, 49 high)

### HIGH

**/monitor** _(dashboard_monitor_camt)_
- [front/broken_data] ROOT CAUSE of '0 operations despite ~1929 xml_audit_logs rows': the store default time_window is '1h'. The backend build_query filters created_at >= from where from = now-1h (monitor_controller.ex:194-196, parse_time_window:142-146). Historical/test traffic older than 1h is excluded, so operations, stats and category counts all return 0/empty even though the table is full.  
  `pix/frontend/admin/src/stores/monitor.ts:32, 108-113` -> FIX: Change the default activeFilters.time_window from '1h' to '7d' (or '24h') in BOTH the initial activeFilters (line ~32) and resetFilters() (line ~108). Backend already supports '7d'. No backend change needed.

**/sync/cid** _(dashboard_monitor_camt)_
- [both/backend_bug] 'TIPO CHAVE' and 'CHAVE' columns are permanently empty. The CID-event ingester only writes (id, event_type, cid, participant_ispb, details, created_at) — BACEN /cids/events delivers only the anonymized CID hash, never the underlying key/keyType (confirmed by the module's own comment at cid_sync_service.ex:494-498). But list_events SELECTs entry_key, entry_key_type and the view binds ev.entry_key_type / ev.entry_key, which are always NULL -> always render '—'.  
  `pix/backend/apps/dict_service/lib/dict_service/sync/cid_sync_service.ex:499-511 (insert); controller cid_sync_controller.ex:63-65; view CidSyncView.vue:429-432` -> FIX: Two options: (a) resolve each CID back to its key via GET /entries-by-cid/{Cid} during ingestion and populate entry_key/entry_key_type, or (b) drop the 'Tipo chave'/'Chave' columns from both the SELECT and the view and show only CID + event_type (which are real). At minimum, the frontend must stop presenting columns the data never fills.
- [both/backend_bug] Event-type enum mismatch. Ingested cid_events have event_type IN ('ADDED','REMOVED','UPDATED') (cid_sync_service.ex:507; simulator CHECK constraint 'ADDED','REMOVED','UPDATED'), but the controller whitelist @valid_event_types is ('KEY_CREATED','KEY_DELETED','KEY_OWNERSHIP_CHANGED','CLAIM_COMPLETED','all') and the frontend filter dropdown offers BOTH sets. Selecting ADDED/REMOVED/UPDATED in the UI is rejected as invalid_param (422); selecting KEY_CREATED etc. filters to zero rows. The eventClass() styling also keys off the wrong enum.  
  `pix/backend/apps/settlement_service/lib/settlement_service_web/controllers/admin/cid_sync_controller.ex:14 (@valid_event_types); view CidSyncView.vue:50-59, 158-174` -> FIX: Align all three on the real BACEN enum ADDED/REMOVED/UPDATED: fix @valid_event_types, the eventTypeOptions list, and eventClass(). Remove KEY_CREATED/KEY_DELETED/KEY_OWNERSHIP_CHANGED/CLAIM_COMPLETED unless those are actually produced.

**/camt060** _(dashboard_monitor_camt)_
- [back/backend_bug] The 7 tabs really do send the SAME message. SpiService.Camt060.query always calls send_camt060(ispb) -> spi_client().get_balance(ispb) for every action and never uses from_date/to_date/from_time/to_time/id_evento_pesquisa/tipo_relatorio/tipo_retorno when building the camt.060. The proxy forwards all form fields (spi_proxy_controller camt060: body = Map.drop(params,[...])), but query() drops them on the floor (only from_date is echoed into reference_date). So lista-lanctos, detalha-lancto, demonstrativo-tir, total-tir, demonstrativo-remuneracao all transmit an identical 'saldo atual' camt.060.  
  `pix/backend/apps/spi_service/lib/spi_service/camt060.ex:30-34, 41-68, 108-118` -> FIX: Build a per-action camt.060 (AcctRptgReq) that honors the action and params: closing balance for saldo-na-data (from_date), entry list with period+tipo_relatorio for lista-lanctos, single-entry detail for detalha-lancto (id_evento_pesquisa), TIR/remuneracao variants. Pass params into get_balance / the message builder instead of ignoring them.
- [both/broken_data] Balance never shows. Only saldo-atual/saldo-na-data are in @balance_actions, so the other 5 tabs never return a balance block at all. For the 2 balance tabs, the position materializes only from inbound camt.052/053 which is not consumed in homolog (INBOUND_SOURCE=bacen_mock_nats), so balance_state stays 'no_local_position' -> always 'Aguardando retorno' + R$ 0,00 with the amber note. End result: no tab ever shows a real saldo.  
  `pix/backend/apps/spi_service/lib/spi_service/camt060.ex:31-32, 55-67, 96-106` -> FIX: Front: render the actual async-return expectation per action instead of a blank balance for 5 tabs. Back/infra: wire inbound camt.052/053 consumption (INBOUND_SOURCE=icom_http) so a real Conta PI position materializes; until then the UI should clearly state 'saldo retorna de forma assincrona' rather than implying a failed query.

**/transactions** _(transactions)_
- [both/wrong_field] Coluna INSTRUMENTO mostra 'pacs.008'. Template usa `getInstrumentLabel(tx.instrument_type || tx.type)`. Quando o payment associado nao tem initiation_form, o backend serializa instrument_type=null (serialize_transaction: `instrument_type: get_in_payment(payment, :initiation_form)`), o front cai no fallback tx.type = message_code = 'pacs.008', que nao esta em instrumentLabels e e ecoado cru. Instrumento (DICT/Manual/Chave) e message_type sao conceitos distintos e estao sendo confundidos.  
  `frontend/admin/src/views/transactions/TransactionListView.vue:274 (template), 134-136 (getInstrumentLabel), 44-52 (instrumentLabels); backend payment_controller.ex:413` -> FIX: Front: remover o fallback `|| tx.type` na linha 274 (usar so `tx.instrument_type`), exibindo '-' quando nulo, para nunca mostrar message_type na coluna Instrumento. Back: garantir que toda criacao grave payments.initiation_form (create ja faz em payment_controller.ex:178) e backfill das linhas legadas sem initiation_form.

**/transactions/:id** _(transactions)_
- [both/wrong_field] O detalhe NAO mostra o nome da INSTITUICAO debitada/creditada, so o ISPB (numero) e o nome da PESSOA. serialize_transaction devolve debtor_ispb/creditor_ispb e debtor_name/creditor_name (este vindo de payments = titular pessoa). Nao ha lookup ISPB→razao social do participante, embora exista o diretorio (services/participants.ts e endpoint BACEN PIX participants). Por isso 'fica so o nome da pessoa'.  
  `frontend/admin/src/views/transactions/TransactionDetailView.vue:356-371 (template Pagador/Recebedor); backend payment_controller.ex:394-401` -> FIX: Back: enriquecer serialize_transaction resolvendo debtor_ispb/creditor_ispb contra Shared.Bacen.PixParticipants e devolver debtor_institution_name/creditor_institution_name. Front: adicionar uma linha 'Instituicao' nos cards Pagador/Recebedor exibindo esse nome ao lado do ISPB.
- [both/broken_data] Status 'Em Processamento' (ACSP) eterno e timeline so com 'Pendente'/vazia. A timeline (store.transactionHistory) vem de MessageHistory, que so e escrito pelos workers outbound_sender/status_updater ao processar pacs.002/pacs.004. O create nao grava linha inicial e, sem a esteira (NATS) consumindo, o status nunca avanca de PDNG/ACSP e o historico fica vazio ou com 1 entrada.  
  `frontend/admin/src/views/transactions/TransactionDetailView.vue:414-439 (timeline); backend payment_controller.ex:314-338 (history le MessageHistory)` -> FIX: Back: gravar MessageHistory inicial no create (status PDNG/origem 'api_create') e assegurar que os workers de status estejam ativos para registrar ACSP→ACCC→STLD.
- [back/broken_data] Message ID 'PIXOUT—...' com TRAVESSAO. NAO existe NENHUM gerador 'PIXOUT' nem '—' em message_id em todo o worktree: generate_msg_id (message_builder.ex:1363) e generate_message_id dos seeds (realistic_transactions_seed.exs:936) produzem 'M'+ISPB+data+seq (32 chars canonicos, sem travessao). Logo o valor 'PIXOUT—...' que aparece na tela e DADO LEGADO ja gravado em monetarie_spi.messages.message_id por um gerador antigo fora deste worktree. O front so ecoa o campo verbatim.  
  `frontend/admin/src/views/transactions/TransactionDetailView.vue:385 (`{{ store.currentTransaction.message_id }}`)` -> FIX: Back/dados: corrigir as linhas legadas na base viva (UPDATE de message_id contendo 'PIXOUT' ou U+2014 para o padrao canonico), ja que nenhum codigo atual gera esse formato. Defensivo no front: sanitizar exibicao trocando '—' por '-' caso o dado legado persista.

**keys** _(keys_qr)_
- [both/broken_data] Pagination is non-functional. Frontend sends {page, per_page:20} (loadData lines 39-42) but EntryController.index only reads limit/offset (controller lines 15-18). So per_page is ignored (backend limit default 100), and the Anterior/Proximo buttons (changePage) re-request with page=N which the backend never honors — the same first 100 rows return every time. The 'Mostrando 1-20 de total' counter is also wrong because perPage(20) never matches the 100 the backend returns.  
  `pix/frontend/admin/src/views/keys/KeyListView.vue:39-42,73-76,235-237` -> FIX: Front: send limit=perPage and offset=(currentPage-1)*perPage instead of page/per_page. (Backend already supports limit/offset.)

**keys/:key** _(keys_qr)_
- [front/backend_bug] STATUS CASING MISMATCH makes block/unblock unreachable. Backend Entry.@statuses = ~w(ACTIVE PENDING BLOCKED DELETED) (entry.ex:15) and entry_to_json returns entry.status verbatim (uppercase). But KeyDetailView statusColor/statusLabel maps use lowercase keys 'active'/'blocked'/'pending' (lines 126-142) AND the action buttons render only when key.status === 'active' (line 162) or === 'blocked' (line 169). With real uppercase 'ACTIVE', the badge always falls to the gray fallback and BOTH Bloquear and Desbloquear buttons are permanently hidden — only Excluir shows. Block/unblock is dead in the UI.  
  `pix/frontend/admin/src/views/keys/KeyDetailView.vue:126-142,162,169` -> FIX: Front: normalize with status?.toUpperCase() in statusColor/statusLabel and compare key.status?.toUpperCase() === 'ACTIVE'/'BLOCKED' (mirror KeyListView lines 96-114 which already uppercases).

**qrcodes** _(keys_qr)_
- [both/broken_data] Pagination broken + total always wrong. Frontend sends {page, per_page:20} (lines 37-39) but QRCodeController.index filters read only limit/offset (controller lines 20-23) — page/per_page ignored. Worse, the index returns %{data: [...]} with NO total field (controller line 27), so the service total falls back to data.length (qrcode.ts line 23); the 'Mostrando 1-20 de {total}' footer therefore always equals the current page size and Proximo never advances.  
  `pix/frontend/admin/src/views/qrcodes/QrCodeListView.vue:37-39,210-212` -> FIX: Front: send limit=perPage & offset=(page-1)*perPage. Back: add total_count to QRCodeController.index response so pagination is accurate.

**/statements** _(statements_extratos_sessions)_
- [back/wrong_field] SPI StatementJSON.index returns field names that do NOT match the frontend Statement interface. It returns statement_type/from_date/to_date/ispb/credit_count/debit_count/creation_datetime and omits status, record_count, file_size, created_at entirely. Frontend reads item.status/period_start/period_end/account_ispb/record_count/file_size/created_at. So columns Período (period_start/period_end), Registros (record_count), Status, Data (created_at) render '-'/blank, and item.type is undefined (backend key is statement_type).  
  `apps/spi_service/lib/spi_service_web/controllers/statement_json.ex:26-42 (statement_data)` -> FIX: In statement_data, add/rename keys to match the FE contract: type: statement.statement_type, period_start: statement.from_date, period_end: statement.to_date, account_ispb: statement.ispb, status: statement.status (add a status column to the schema/generation or derive 'completed'), record_count: (credit_count+debit_count) or a stored count, file_size: byte_size of stored export, created_at: statement.creation_datetime. Alternatively remap in services/statement.ts listStatements().

**/statements/generate** _(statements_extratos_sessions)_
- [both/backend_bug] Generate ALWAYS fails. Frontend posts {type:'camt.053', start_date, end_date, account_ispb}. SPI generate computes statement_type = params['statement_type'] || params['type'] and case-matches only 'intraday'/'end_of_day'; 'camt.053' falls into the catch-all → {:error, :invalid_statement_type} → HTTP 400 'Invalid statement type. Must be intraday or end_of_day'. So no extrato is ever generated.  
  `apps/spi_service/lib/spi_service_web/controllers/statement_controller.ex:25-35 (generate)` -> FIX: Either map camt.052->intraday and camt.053/054->end_of_day in the proxy/SPI generate, or change the FE typeOptions values to 'intraday'/'end_of_day' (views/statements/StatementGenerateView.vue:19-23).
- [both/wrong_field] Even if type were accepted, the date and ISPB params are dropped. FE sends start_date/end_date/account_ispb; SPI generate reads params['ispb'] and build_statement_opts reads params['from_date']/params['to_date'] — none of which the FE sends. So ispb=nil and no date opts → generation runs with wrong/empty period and ISPB.  
  `apps/spi_service/lib/spi_service_web/controllers/statement_controller.ex:26-27 (ispb=params['ispb']) and build_statement_opts (from_date/to_date)` -> FIX: Remap in services/statement.ts generateStatement to send ispb/from_date/to_date (and statement_type), or accept account_ispb/start_date/end_date keys in SPI generate/build_statement_opts.

**/sessions** _(statements_extratos_sessions)_
- [back/broken_data] Créditos/Débitos/Posição Líquida columns always show '-'. SessionController.index calls Sessions.list_sessions which does NOT preload :transactions. SessionJSON.data computes total_credit/total_debit/net_position from the transactions association; when not preloaded it returns nil (calculate_total_credit guards on %Ecto.Association.NotLoaded{}). So the list view's three money columns are perpetually empty even when sessions have transactions.  
  `apps/settlement_service/lib/settlement_service/sessions.ex:208-217 (list_sessions: no preload)` -> FIX: Add Repo.preload(:transactions) (or a join+aggregate) in list_sessions, OR compute totals from session.total_amount/stored aggregate columns instead of the association so the list does not need per-row transaction preload.

**participants/:ispb** _(participants_ops)_
- [front/broken_data] LimitsSection e renderizado DUAS VEZES: 'Limites Globais' e 'Limites por Transacao' apontam para o MESMO componente com conteudo identico, dando a impressao de duplicata.  
  `frontend/admin/src/views/ParticipantDetailView.vue:317,337` -> FIX: Renderizar conteudos distintos: um componente de limites globais e outro de limites por transacao (ou um unico acordeao). Remover a segunda instancia identica.

**participants/approval-limits  (= operations/approval-limits redirect)** _(participants_ops)_
- [back/backend_missing] Salvar/editar e excluir alcada chamam PUT /api/v1/alcada/parameters/:id e DELETE /api/v1/alcada/parameters/:id, que NAO EXISTEM no router (apenas GET /parameters, GET /parameters/:id e POST /parameters estao definidos). Editar resulta em 404 silencioso (erro so vai pro console).  
  `frontend/admin/src/views/participants/ApprovalLimitsView.vue:123,140` -> FIX: Adicionar put '/parameters/:id' e delete '/parameters/:id' em settlement router scope /alcada (apps/settlement_service/.../router.ex ~492-498) + handlers no SpiProxyController; ou ajustar o front para POST de update.

**participants/message-statistics  (= operations/message-statistics redirect)** _(participants_ops)_
- [both/broken_data] Chama GET /api/v1/audit/logs/stats?report=messages e le res.stats/res.hourlyVolume/res.categoryDistribution/res.summary. Mas audit_logs_stats retorna {data:{by_action,total,period_days},timestamp} e IGNORA o param report — nenhuma dessas chaves existe. Resultado: tabela sempre vazia e cards (Total/Taxa/Tempo/Sistemas) sempre 0.  
  `frontend/admin/src/views/participants/MessageStatisticsView.vue:56-76` -> FIX: Front: apontar para um endpoint real de estatisticas de mensagens (ex settlement /api/v1/messages /statistics em router:686, ou criar) e ler a forma certa. Back (audit_log_controller.ex audit_logs_stats, lines 116-139): nao implementa report=messages. Criar endpoint dedicado ou estender.

**participants/rco-monitor  (= operations/rco-monitor redirect)** _(participants_ops)_
- [front/wrong_field] View de RCO/TIR (Reserva de Contingencia) carrega GET /api/v1/returns (registros de DEVOLUCAO PIX) e os mapeia para campos de RCO inexistentes: operationType TIR/TCO/TCC/TIL, latencyMs, messageId, errorCode, e res.statistics. Registros de returns nao tem esses campos -> tabela mostra colunas undefined e os cards de statistics ficam em 0.  
  `frontend/admin/src/views/participants/RcoMonitorView.vue:476-486` -> FIX: Apontar para um endpoint real de operacoes RCO/TIR (inexistente hoje — criar no backend) com o shape RcoOperation/RcoStatistics; nao reutilizar /api/v1/returns, que e semanticamente outra coisa.

**participants/response-time  (= operations/response-time redirect)** _(participants_ops)_
- [both/broken_data] Chama GET /api/v1/audit/logs/stats?report=response-time e le res.metrics/res.summary. O endpoint retorna {data:{by_action,total,period_days}} e ignora report — nunca retorna metrics nem summary. Resultado: tabela de metricas por ISPB sempre vazia e cards sempre 0.  
  `frontend/admin/src/views/participants/ResponseTimeReportView.vue:399-411` -> FIX: Front: usar/criar um endpoint de relatorio de tempo de resposta por participante com o shape ResponseTimeMetric. Back: audit_logs_stats (audit_log_controller.ex 116-139) nao tem este relatorio.

**participants/legacy-parameters  (= operations/legacy-parameters redirect)** _(participants_ops)_
- [front/wrong_field] View 'Parametros Legados' carrega GET /api/v1/pix-saque-troco como 'parameters' (lines 449-457). Um PixSaqueOperation nao tem os campos category/key/value/system/dataType/isEncrypted que a tabela renderiza -> linhas com dados undefined / categorias vazias. Semantica totalmente trocada.  
  `frontend/admin/src/views/participants/LegacyParametersView.vue:449-457` -> FIX: Apontar para um endpoint real de parametros legados (inexistente — criar GET /api/v1/config/legacy-parameters no backend; o store ja preve essa rota) e remover o uso de /api/v1/pix-saque-troco.
- [both/backend_missing] Salvar parametro chama PUT /api/v1/pix-saque-troco/:id (so existe PUT /api/v1/pix-saque-troco sem :id) e testConnection chama POST /api/v1/health/services/:id/test (rota inexistente). Ambos 404 -> acoes nao funcionam.  
  `frontend/admin/src/views/participants/LegacyParametersView.vue:475,494` -> FIX: Criar endpoints reais de update de parametro e de teste de conexao no backend e apontar o front para eles.

**balance** _(balance_alcada)_
- [front/wrong_field] RB balance card permanently shows R$0 and PI card shows the wrong account. balanceApi.getPositions() does rows.find(r => r.accountType === 'RB' / 'PI'), but the backend list_all_positions returns rows keyed by ispb with NO accountType/account_type field at all. So RB never matches -> zeroPosition('RB'); PI falls through to rows[0] (first balance by ispb), not the PI account.  
  `pix/frontend/admin/src/services/balance.ts:238-248 (find); backend apps/spi_service/lib/spi_service/balances.ex list_all_positions ~ returns %{ispb,available,blocked,total,...} no accountType` -> FIX: Backend: have positions endpoint label each row with accountType RB/PI (Conta PI vs Reserva Bancaria) or expose distinct RB/PI rows; OR front: stop keying on accountType and map the two real accounts explicitly. Currently the dashboard's two headline numbers are wrong.

**balance/requests** _(balance_alcada)_
- [back/backend_bug] list_balance_requests proxies to /api/v1/balance/requests/pending instead of a real 'all requests' endpoint, so the list only ever contains PENDING items (and those are balance blocks, not requests). The view's stats Processando/Concluidas/Rejeitadas are computed from this and are permanently 0.  
  `pix/backend/apps/settlement_service/lib/settlement_service_web/controllers/gateway/spi_proxy_controller.ex:345-356` -> FIX: Implement a real spi GET /balance/requests (all statuses) backed by a balance_request table, and point the proxy at it. Currently no such SPI route exists.

**balance/requests/:id** _(balance_alcada)_
- [back/backend_bug] get_balance_request does NOT fetch a single request; it pulls the full pending list and .find()s by id. A completed/rejected request (or any non-pending id) returns 404 'Balance request not found'. Detail only works for currently-pending blocks.  
  `pix/backend/apps/settlement_service/lib/settlement_service_web/controllers/gateway/spi_proxy_controller.ex:363-384` -> FIX: Add a real spi GET /balance/requests/:id and proxy directly.

**balance/parameters** _(balance_alcada)_
- [front/broken_data] 100x currency unit bug. Backend returns/stores values in CENTAVOS (defaults minBalanceRB 1_000_000_00 = 100000000, criticalAmountThreshold 10_000_000_00, executiveApprovalAmount 50_000_000_00). The form binds these raw integers into R$ number inputs (v-model.number, step 0.01) with no /100 on load and no *100 on save. The user sees e.g. 'R$ 100000000' meaning R$100mi instead of R$1mi, and any value typed is saved as reais but read back as centavos.  
  `pix/frontend/admin/src/views/balance/BalanceParametersView.vue:15-27 defaults; 90-94 load (formData = parameters raw); 71-84 save (sends raw); inputs 177-344; backend defaults apps/settlement_service/.../balance_parameters_controller.ex 46-61` -> FIX: Divide centavos by 100 when loading into the form and multiply by 100 when submitting (or switch the backend contract to reais). Align with the rest of the system (centavos).

**alcada/history** _(balance_alcada)_
- [back/backend_missing] This 'history' view is non-functional as history: it calls fetchRegistrations() which hits the only registrations endpoint, which returns ONLY status=pending (Alcadas.list_pending_registrations hardcodes the where). So APPROVED/REJECTED/EXPIRED records — i.e. the actual history — never appear; the status filter and summary stats for those statuses are permanently empty/0. The view duplicates the pending view.  
  `pix/backend/apps/spi_service/lib/spi_service/alcadas.ex:86-93; frontend AlcadaHistoryView.vue onMounted 199-201 fetchRegistrations` -> FIX: Add a real list endpoint that returns resolved registrations with status/date filtering (and a settlement proxy route), then have the history view use it.

**accounting/settlement-reports** _(accounting)_
- [front/wrong_field] Tela chamada 'Relatorios de Liquidacao' mas so consome dados de TARIFAS (fee reports + fee schedules). Nao existe conciliacao com liquidacao/netting real aqui; o usuario espera relatorios de settlement e ve apenas tarifas. Pode existir confusao funcional grave.  
  `views/accounting/SettlementReportView.vue:124-126,164-227,230-309` -> FIX: Renomear a tela para 'Relatorios de Tarifas' OU ligar a um endpoint de liquidacao real (ex.: /api/v1/sessions/:id/statistics, /api/v1/reports/generated). Definir com o dono qual e a intencao.

**accounting/netting-cycles** _(accounting)_
- [front/broken_data] formatCurrency NAO divide por 100. total_amount das sessoes e inteiro em centavos no backend; aqui Intl currency recebe o valor cru -> volume total e cada linha exibem valor 100x maior. Inconsistente com Dashboard/Reconciliation/Fees que dividem por 100.  
  `views/accounting/NettingCyclesView.vue:122-125,202,294` -> FIX: Dividir por 100 em formatCurrency (Number(value||0)/100), confirmando a unidade de total_amount no SessionController.

**accounting/netting** _(accounting)_
- [front/broken_data] Herda o bug de moeda 100x da NettingCyclesView (formatCurrency sem /100).  
  `views/accounting/NettingCyclesView.vue:122-125` -> FIX: Mesma correcao: dividir por 100.

**accounting/history** _(accounting)_
- [front/broken_data] formatCurrency NAO divide por 100. journal amount/debit/credit/balance sao inteiros em centavos (journal_entry.ex field :amount :integer; Accounting.list_journal_entries seleciona debit: j.amount, credit: j.amount, balance running). Totais e cada linha aparecem 100x maiores.  
  `views/accounting/StandardHistoryView.vue:51-53,137,141,171,174,177` -> FIX: Dividir por 100 em formatCurrency.
- [both/wrong_field] Coluna COSIF fica vazia: backend envia cosif_code (snake_case) mas accountingApi.listJournalEntries retorna response.data cru sem normalizar snake->camel, enquanto o template le entry.cosifCode. Resultado: celula COSIF em branco.  
  `services/accounting.ts:531-540` -> FIX: Normalizar as linhas no service (cosifCode: row.cosif_code, account: row.account) ou alinhar o serializer/backend para camelCase; idem para date/reference.

**accounting/cost-centers** _(accounting)_
- [front/broken_data] formatCurrency NAO divide por 100. cost_center.budget e spent sao :integer em centavos no backend. Orcamento, gasto e saldo restante aparecem 100x maiores; a barra de uso (spent/budget) fica correta por ser razao, mas os valores em R$ ficam errados.  
  `views/accounting/CostCentersView.vue:48-50,191,195,203` -> FIX: Dividir por 100 em formatCurrency; e no dialog enviar budget*100 ao salvar (o input 'Budget (R$)' coleta reais mas o backend espera centavos).

**accounting/events** _(accounting)_
- [front/broken_data] formatCurrency NAO divide por 100. accounting_event.amount e :integer em centavos. Valores na tabela aparecem 100x maiores. Alem disso o dialog coleta amount em reais (step 0.01) mas o backend armazena centavos -> ao criar evento o valor sera salvo 100x menor que o digitado.  
  `views/accounting/AccountingEventsView.vue:50-52,178,222-223,72-77` -> FIX: Dividir por 100 ao exibir; multiplicar por 100 (Math.round) ao enviar no createEvent.

**accounting/monitor** _(accounting)_
- [front/broken_data] O delta '+0% vs ontem' e hardcoded (change: 0 sempre) — KPI falso. Alem disso formatCurrency nao divide por 100 (se algum amount real entrasse, ficaria 100x).  
  `views/accounting/AccountingMonitorView.vue:72,150-158,40-42,84` -> FIX: Remover o badge de variacao ou calcula-lo com dado real (comparacao dia anterior); dividir amounts por 100.

**security/users** _(security)_
- [back/wrong_field] Backend user_to_json (gateway/users_controller.ex) returns only id/username/email/full_name/is_active/is_blocked/institution_id/branch_id/last_login. It does NOT return entityName, entityCode, or groups. So the 'Instituição' column ({{ user.entityName }}, line 445) renders blank for every row and the 'Grupos' column always shows 'Sem grupos' (user.groups is always undefined, line 448).  
  `apps/settlement_service/lib/settlement_service_web/controllers/gateway/users_controller.ex:user_to_json (~340-355)` -> FIX: Join institution + groups in Users.get_user/list_users and add entity_name, entity_code, groups: [{id,code,name}] to user_to_json so the FE columns populate.

**security/users/new** _(security)_
- [back/backend_bug] create_user reads only institution_id/branch_id and NEVER handles group_id. The FE sends {login, password, name, email, entity_id, group_id} (UserFormView handleSubmit, lines 156-163) but the backend casts only username/email/full_name/institution_id/branch_id — so entity_id is ignored (backend wants institution_id) and group_id is silently discarded. The entire 'Grupos do usuário' selection card is a no-op: the user is created with no institution and no group.  
  `apps/settlement_service/lib/settlement_service_web/controllers/gateway/users_controller.ex:create_user (~136-176)` -> FIX: Map entity_id→institution_id; accept group_id and insert into the user_groups join (or call Users.assign_group). FE service also sends login not username — backend create_user reads params['username'] only, so even the username is dropped. Align field names FE↔BE (login/username, entity_id/institution_id, group_id).
- [front/wrong_field] userService.create posts the raw CreateUserRequest with key 'login' (services/security.ts create, line 58-60) but backend create_user reads params['username'] — so the username column ends up null and validate_required([:username,:email]) returns 422. New-user creation fails at the API.  
  `src/services/security.ts:58-61` -> FIX: Send username: data.login (and institution_id: data.entity_id) in the create payload to match backend, or fix backend to read 'login'.

**security/users/:id** _(security)_
- [back/wrong_field] Profile panel renders entityName/entityCode (lines 289) and groups (305-327), and Datas renders createdAt (386). Backend user_to_json returns none of entityName, entityCode, groups, createdAt-as-createdAt (it returns created_at; FE reads createdAt). So 'Instituição' shows ' ()', 'Grupos e permissões' always shows the empty 'Nenhum grupo atribuído' state, and 'Criado em' shows 'Nunca'.  
  `apps/settlement_service/lib/settlement_service_web/controllers/gateway/users_controller.ex:user_to_json` -> FIX: Add entity_name/entity_code/groups; and either return createdAt or map created_at→createdAt in the FE service getById.

**security/users/:id/edit** _(security)_
- [front/wrong_field] On edit load (onMounted, 119-135) it reads user.entityId and user.groupId to preselect entity/group, but the FE service getById never produces entityId/groupId (backend returns institution_id only, and service doesn't map it). So selectedEntity/selectedGroup are always undefined and no group is preselected when editing.  
  `src/views/security/UserFormView.vue:119-135` -> FIX: Map institution_id→entityId and the user's group→groupId in services/security.ts getById, after backend exposes them.
- [back/backend_bug] update_user normalize_user_params has NO branch for group_id (it maps fullName/institutionId/branchId/is_active/etc and then String.to_existing_atom for unknowns). The edit form submits {name, email, group_id} (handleSubmit 149-154) → group_id is dropped, so changing a user's group via edit does nothing.  
  `apps/settlement_service/lib/settlement_service_web/controllers/gateway/users_controller.ex:normalize_user_params, update_user` -> FIX: Handle group_id in update (write to user_groups join) and map name→full_name (FE sends 'name', backend casts :full_name only via fullName/full_name — 'name' is dropped, so editing the display name also silently fails).

**security/groups** _(security)_
- [back/backend_missing] 'Excluir' calls groupService.delete → DELETE /api/v1/groups/:id, and edit/save would call PUT /api/v1/groups/:id. The router only defines GET /api/v1/groups (AdminController :list_groups). There is NO POST/PUT/DELETE/GET-by-id route for groups in the gateway — every group mutation returns 404 (Phoenix no-route).  
  `apps/settlement_service/lib/settlement_service_web/router.ex:scope /api/v1, get "/groups" (~556-557)` -> FIX: Add create/show/update/delete group routes + GroupController actions, or disable the actions in the FE.

**reports/balance** _(reports)_
- [both/broken_data] View expects a per-DATE time series {date, rbBalance, piBalance, totalCredits, totalDebits, transactionCount} but /api/v1/balance/positions returns {count, data:[{ispb, available, blocked, projected, pending_credits, pending_debits, as_of, source}]} — CURRENT positions per participant, not a date history, and it ignores start_date/end_date/account_type. record.rbBalance/piBalance/date are undefined: cells render 'R$ NaN'/'Invalid Date', summary computes NaN%. Whole table, chart and summary are garbage.  
  `pix/frontend/admin/src/views/reports/BalanceReportView.vue:7-14,39-99,397-404` -> FIX: Either point at a real per-date balance-history endpoint (none currently returns rb/pi split by date — backend gap) or rewrite the view to the actual positions shape. As-is no backend returns this view's expected data.

**reports/fees** _(reports)_
- [both/broken_data] list_reports returns aggregate FeeReport rows {report_type, session_id, from_date, to_date, total_transactions, total_fees, total_transaction_volume, fees_by_type:{}} and ignores start_date/end_date. The view maps each row to {date:r.date, transactionType:r.transaction_type, transactionCount:r.transaction_count, feeAmount:r.fee_amount, exemptAmount:r.exempt_amount, exemptCount:r.exempt_count} — none of those keys exist, so every row is date:''→'Invalid Date', type:'', all numeric=0, and the entire exempt dimension is fabricated zeros. Charts/summary all zero.  
  `pix/frontend/admin/src/views/reports/FeeReportView.vue:8-16,124-134,436-448` -> FIX: No per-transaction-type fee breakdown endpoint exists; consume fees_by_type/report_type/from_date from the aggregate rows or build a real /fees/breakdown endpoint. As-is the page shows no real data.

**reports/history** _(reports)_
- [front/broken_data] Search input crashes the page: filteredReports calls report.templateName.toLowerCase(), but serialize_report never returns templateName (it returns reportName/type/format/status/generatedAt/generatedBy/rowCount). When the user types a search term, report.templateName is undefined -> TypeError and list rendering throws.  
  `pix/frontend/admin/src/views/reports/ReportHistoryView.vue:201,344-347` -> FIX: Guard with (report.templateName||'') and stop reading templateName (or add it to the backend serializer reports_controller.ex L543).

**dict/reports** _(dict_monitors)_
- [front/broken_data] Keys-report 'Growth %' and 'Status' columns are FABRICATED: getKeyReports hardcodes growthPercent: 0 and status: 'stable' for every row (services/dict.ts:100-101). The table (DictReportsView.vue:110-117) renders these as if real DICT growth/trend metrics. This is placeholder data presented as truth.  
  `frontend/admin/src/services/dict.ts:97-102` -> FIX: Either drop the Growth %/Status columns from the keys tab, or compute real period-over-period growth from a backend stats endpoint. Do not emit constant 0/'stable'.

**/monitoring** _(simulator_system)_
- [front/broken_data] The whole metrics grid (CPU, Memory, Disk, Requests/sec, Latency, Error Rate) is fed from /api/v1/system/health and /api/v1/audit/logs/stats. system/health returns {overall,services,database,redis,nats} with NO cpu/memory/disk; audit/logs/stats returns {by_action,total,period_days}. So statsData.cpu/memory/etc are undefined and every metric falls back to 0 — the grid permanently shows 0%/0ms. A correct endpoint GET /api/v1/monitoring/metrics EXISTS (monitoring_controller.ex:70-90 returns cpu, memory) but is never called.  
  `frontend/admin/src/views/MonitoringView.vue:44-71` -> FIX: Replace the audit/logs/stats call with GET /api/v1/monitoring/metrics and map metrics.cpu/memory/disk/requestsPerSec/avgLatency/errorRate from it. Keep /system/health for the services table.

**/monitoring/realtime** _(simulator_system)_
- [front/broken_data] onMounted reads the JWT from document.cookie looking for 'auth_token='. But the backend sets the JWT in an HttpOnly cookie (auth.ts:97-98,126 'JWT is now in HttpOnly cookie'), which JavaScript CANNOT read. So token is always undefined and store.connect(token) is NEVER called. The WS never connects; all 6 panels stay 'Connecting...'/'Waiting for data...' and the header shows 'Disconnected' permanently. UserSocket.connect requires a 'token' param (user_socket.ex:13).  
  `frontend/admin/src/views/monitoring/RealTimeMonitoringView.vue:52-60` -> FIX: Stop reading the HttpOnly cookie. Either (a) add a backend endpoint minting a short-lived WS token (GET /api/v1/auth/ws-token) and pass it to store.connect, or (b) authenticate the Phoenix socket from the HttpOnly cookie server-side in UserSocket.connect/3 via connect_info cookies and call store.connect() without a JS token.

### MEDIUM / LOW (condensado)

**/** _(dashboard_monitor_camt)_: [m/broken_data] All three fetches swallow errors into empty state (catch{} sets stats= (DashboardView.vue:77-80, 114-117)
**/monitor** _(dashboard_monitor_camt)_: [l/wrong_field] Service header comment claims the controller is AdminServiceWeb.Admin. (monitor.ts:3-5)
**/sync/cid** _(dashboard_monitor_camt)_: [m/broken_data] 'Sync 24h' and 'Sync 7d' counters read monetarie_dict.cid_files, but t (cid_sync_controller.ex:87-92)
**/camt060** _(dashboard_monitor_camt)_: [l/wrong_field] reqd_msg_nm_id (camt.052/camt.053 select) and tipo_retorno are defined (Camt060ToolView.vue:30-80, 89-99)
**/transactions/new** _(transactions)_: [m/backend_bug] Disparo da esteira BACEN (publish_payment_event → outbound NATS) so oc (payment_controller.ex:231-309); [l/wrong_field] create envia o pix_key como creditor_proxy mas pix_key_type nunca e pe (payment_controller.ex:163-180, 403)
**/transactions/returns** _(transactions)_: [l/wrong_field] Coluna Status exibe o codigo cru ({{ ret.status }} = ACCC/PDNG/RJCT) s (ReturnListView.vue:114-116)
**keys/new** _(keys_qr)_: [l/wrong_field] Form posts key_value via the 'key' field name? Service sends form as-i (keys.ts:30-33)
**keys/:key** _(keys_qr)_: [m/broken_data] 'Atualizado em' always shows '-'. View binds key.updated_at (line 278) (KeyDetailView.vue:276-279)
**/statements** _(statements_extratos_sessions)_: [m/broken_data] Pagination totals are wrong. SPI index returns {count, data}; the prox (statement.ts:18-23)
**/sessions** _(statements_extratos_sessions)_: [m/broken_data] Pagination totals wrong. SessionController.index renders %{data: ...}  (session_controller.ex:28-47 (index) and parse params (limit/offset only)); [l/wrong_field] FE filters send type=d0/d1/d2 and status lowercased — these match the  (session.ts:38-51 (normalizeSession))
**claims** _(claims_med)_: [l/wrong_field] Frontend filter values are lowercase ('portability','ownership','open' (ClaimListView.vue:18-30,40-41)
**med/refunds** _(claims_med)_: [m/wrong_field] Refund column mapping is semantically off. SQL stores debtor_ispb/cred (dict_proxy_controller.ex:934-945)
**med/funds-recoveries** _(claims_med)_: [l/wrong_field] List paginates with limit/offset (loadData uses limit:perPage, offset: (FundsRecoveryListView.vue:79-83,292-293)
**med/infractions** _(claims_med)_: [l/wrong_field] Column headers are payer/receiver (t('med.infractions.columns.payer'/' (InfractionListView.vue:37-42,176-177,192-193)
**participants/:ispb** _(participants_ops)_: [m/broken_data] Campos AGENCIA (card info e card Conta PI) e NUMERO DA CONTA estao har (ParticipantDetailView.vue:261,272,276)
**participants/approval-limits  (= operations/approval-limits redirect)** _(participants_ops)_: [l/wrong_field] loadData le res.data como array de ApprovalLimit, mas o front envia/le (ApprovalLimitsView.vue:97-101,114-116)
**participants/pix-saque-troco  (= operations/pix-saque-troco redirect)** _(participants_ops)_: [m/broken_data] Filtros startDate/endDate nao sao aplicados em lugar nenhum (nem envia (PixSaquePixTrocoView.vue:434-455,503)
**balance** _(balance_alcada)_: [m/broken_data] 'Solicitações Pendentes' list shows balance BLOCKS, not real credit/de (BalanceDashboardView.vue:272-298; backend apps/spi_service/lib/spi_service/balances.ex list_pending_requests 561-583)
**balance/requests/:id** _(balance_alcada)_: [m/broken_data] The LSO/RSO approval timeline binds request.lsoApprovedAt/rsoApprovedA (balance.ts:179-201 (normalizeRequest omits lso/rso/rejected fields))
**balance/parameters** _(balance_alcada)_: [l/backend_missing] notifyEmails/notifySMSNumbers/notifySlackWebhook are sent by the form  (balance_parameters_controller.ex:32-44 keys; 104-106 nil->skip)
**balance/request/credit** _(balance_alcada)_: [m/broken_data] Unit inconsistency vs Debit/Aporte: this form treats amount as REAIS ( (CreditRequestView.vue:34-36, 49-53 validation in reais; contrast DebitRequestView.vue 77-82)
**balance/request/debit** _(balance_alcada)_: [m/broken_data] Available-balance guard uses balanceStore.rbPosition?.availableBalance (DebitRequestView.vue:31-32, 64-75, 108-110, 124-127; service services/balance.ts 238-248); [m/broken_data] Amount unit mismatch: handleAmountChange multiplies input by 100 (cent (DebitRequestView.vue:77-82, 105-107, 30-32)
**balance/request/aporte** _(balance_alcada)_: [m/broken_data] Amount multiplied by 100 (centavos) and min 100000000; EXECUTIVE_APPRO (LiquidityInjectionView.vue:30, 84-95)
**alcada/parameters** _(balance_alcada)_: [m/broken_data] By default fetchParameters() sends no active_only param; spi list_para (alcada_controller.ex:15-19; frontend store fetchParameters stores/alcada.ts 43-54 (no active_only passed))
**alcada/pending** _(balance_alcada)_: [m/broken_data] The view's stats compute approved/rejected/expired counts, but spi lis (alcadas.ex:86-93 list_pending_registrations; controller alcada_controller.ex 55-58)
**alcada/history** _(balance_alcada)_: [m/broken_data] Detail modal openDetail(reg) only shows the already-loaded list row (s (AlcadaHistoryView.vue:132-134 openDetail; backend missing proxy in router.ex 491-499 and spi_proxy_controller.ex)
**accounting** _(accounting)_: [m/wrong_field] O card 'Relatorios de Tarifas Recentes' usa store.feeReports mas o lin (AccountingDashboardView.vue:204)
**accounting/chart-of-accounts** _(accounting)_: [l/wrong_field] Coluna 'Tipo' renderiza account.accountType cru (asset/liability/equit (ChartOfAccountsView.vue:134)
**accounting/history** _(accounting)_: [m/wrong_field] Cabecalho da coluna de conta usa t('accounting.parentAccount') ('Conta (StandardHistoryView.vue:113,157,168); [l/backend_missing] accountingApi.exportAccountingData chama GET /api/v1/accounting/export (accounting.ts:585-591)
**accounting/cost-centers** _(accounting)_: [m/wrong_field] Rotulos semanticamente errados: o campo 'budget' (orcamento) e exibido (CostCentersView.vue:190,194)
**accounting/events** _(accounting)_: [m/wrong_field] Coluna Status renderiza event.status cru ('processed'/'pending'/'error (AccountingEventsView.vue:186)
**accounting/monitor** _(accounting)_: [m/wrong_field] Status na tabela renderiza entry.status cru em ingles (processed/pendi (AccountingMonitorView.vue:196)
**security/users/new** _(security)_: [m/broken_data] The group cards render group.code, group.can_approve_lso, group.can_ap (admin_controller.ex:get_permission_groups (~hardcoded list))
**security/users/:id** _(security)_: [l/wrong_field] Status badge in header shows the raw enum {{ currentUser.status }} (li (UserDetailView.vue:193); [m/broken_data] handleResetPassword (122-136) calls securityStore.resetUserPassword(id (UserDetailView.vue:122-136)
**reports** _(reports)_: [l/wrong_field] REPORT_TYPE_OPTIONS dropdown offers 9 report types (PARTICIPANT, RESPO (ReportsListView.vue:18-29,40)
**reports/history** _(reports)_: [m/wrong_field] Columns 'Tamanho' and the download tooltip read report.fileSize and re (ReportHistoryView.vue:215,248,454-460)
**reports/view** _(reports)_: [m/wrong_field] report.templateName and report.fileSize are read but never returned by (ReportViewerView.vue:55,115,313,408,411,417)
**dict/indicators** _(dict_monitors)_: [m/broken_data] Percentiles (p50/p95/p99) come from /api/v1/audit/logs/stats but the r (dict.ts:160-168); [l/broken_data] store.slaMetrics is populated from data.sla_metrics on /audit/logs/sta (DictIndicatorsView.vue:140-166)
**dict/policies** _(dict_monitors)_: [m/wrong_field] createPolicy/updatePolicy POST/PUT the frontend DictPolicy shape (inst (dict.ts:61-85); [l/broken_data] currentUsage maps from inst.current_usage (services/dict.ts:71) — the  (dict.ts:61-74)
**dict/reports** _(dict_monitors)_: [m/broken_data] Key counts are derived client-side by paging /api/v2/entries with limi (dict.ts:87-103); [l/wrong_field] Refunds tab sources /api/v1/returns (SPI pacs.004 returns), not DICT M (dict.ts:105-116)
**dict/statistics** _(dict_monitors)_: [m/broken_data] Both the message-type breakdown table and the percentiles section depe (StatisticsView.vue:12-22,164-220)
**monitors/integration** _(dict_monitors)_: [m/broken_data] Events are sourced from /api/v1/audit/logs (services/monitors.ts:16).  (monitors.ts:16-30)
**monitors/tariffs** _(dict_monitors)_: [m/broken_data] createTariff/updateTariff POST/PUT the camelCase frontend shape (feeTy (monitors.ts:60-84)
**/simulator** _(simulator_system)_: [l/wrong_field] Service type ScenarioName (simulator.ts:4-12) only declares 8 scenario (simulator.ts:4-12)
**/system-config** _(simulator_system)_: [l/wrong_field] getDisplayName(key) does key.toUpperCase().replace(/_/g,'_') — the rep (SystemConfigView.vue:91-93)


## Onda B — Mocks e acoes mortas  (45 itens, 22 high)

### HIGH

**/transactions/returns** _(transactions)_
- [front/dead_action] Rota MORTA: 'transactions/returns' esta registrada DEPOIS de 'transactions/:id' no router; o Vue Router casa primeiro :id='returns' e abre o TransactionDetailView, que chama getTransaction('returns') → backend responde 400 'Invalid transaction ID'. O ReturnListView nunca e alcancado pela URL.  
  `frontend/admin/src/router/index.ts:206-215` -> FIX: Mover a definicao de 'transactions/returns' (linhas 211-215) para ANTES de 'transactions/:id' (linha 206), ou renomear o path para 'returns' sem colisao.
- [back/mock] Mesmo se a rota fosse alcancavel, ReturnController.index e um STUB: `returns = []` com comentario 'For now, return empty list - would need a Returns context'. A lista de Devolucoes sempre aparece vazia, independentemente de devolucoes reais na base.  
  `backend/apps/spi_service/lib/spi_service_web/controllers/return_controller.ex:23-27` -> FIX: Implementar o contexto Returns consultando monetarie_spi.messages (pacs.004) / tabela de devolucoes e paginar, em vez de retornar [].

**/statements** _(statements_extratos_sessions)_
- [back/dead_action] The 'Baixar' button is conditionally rendered with v-if="item.status === 'completed'". Because the backend never returns a status field, item.status is always undefined and the download button NEVER appears for any row; users only see '-'. The download endpoint itself works (SPI export produces real XML/PDF).  
  `views/statements/StatementListView.vue:231-239` -> FIX: Make the backend return a status field (see wrong_field fix) OR drop the v-if gate / default status to 'completed' in services/statement.ts when the row is downloadable.

**claims/:id** _(claims_med)_
- [both/dead_action] Confirm and Cancel buttons are effectively dead. claims.ts updateClaim sends PATCH body {status:'confirmed'} / {status:'cancelled'} (services/claims.ts:29-32). The gateway proxies the body verbatim to DICT (dict_proxy_controller.ex:172-181). dict_service ClaimController.update reads params["action"] and matches only "CONFIRM"/"CANCEL"/"COMPLETE", falling to `_ -> {:error, :invalid_action}` for the {status:...} body — so every confirm/cancel returns an error and the claim state never changes.  
  `pix/frontend/admin/src/views/claims/ClaimDetailView.vue:44-46,61-63` -> FIX: Front: change updateClaim to send {action:'CONFIRM'} / {action:'CANCEL'} (uppercase) instead of {status:...}; OR Back (dict_service claim_controller.ex:181-191): accept a status/state body and map confirmed->confirm_claim, cancelled->cancel_claim, completed->complete_claim. Pick one contract and align both layers.

**participants/:ispb** _(participants_ops)_
- [front/mock] Aba Responsaveis e 100% MOCK: array hardcoded com 1 linha (System Administrator / admin@monetarie.com.br), sem service, sem store, sem chamada de API. handleAdd/handleEdit/handleRemove sao funcoes vazias.  
  `frontend/admin/src/views/participants/ResponsiblesSection.vue:17-35` -> FIX: Criar service+store que leiam responsaveis reais do participante (backend endpoint inexistente — precisa ser criado, ex GET /api/v1/bacen/pix-participants/:ispb/responsibles) e wirar add/edit/remove; remover o array hardcoded.
- [front/mock] Aba Webhooks e 100% MOCK: webhooks=ref([]) sempre vazio (empty-state eterno), sem service/store. handleAdd/handleEdit/handleRemove vazios; handleToggle so muta local.  
  `frontend/admin/src/views/participants/WebhooksSection.vue:16-29` -> FIX: Criar endpoint de webhooks por participante no backend e wirar CRUD real; hoje nao existe rota nem chamada.
- [front/mock] Aba Limites e 100% HARDCODED: limits com constantes em centavos (daily 100000000, monthly 5000000000, perTransaction 50000000) e dailyUsed/monthlyUsed/transactionsToday sempre 0. props.ispb nunca e usado. 'Editar Limites' abre modal placeholder 'Funcionalidade em desenvolvimento'.  
  `frontend/admin/src/views/participants/LimitsSection.vue:10-17,111,132` -> FIX: Buscar limites reais do participante por ISPB de um endpoint (inexistente — criar) e implementar a edicao; remover modal placeholder.

**balance/requests/:id** _(balance_alcada)_
- [both/dead_action] Approve/Reject buttons (handleApprove/handleReject -> store.approveRequest/rejectRequest -> POST /balance/requests/:id/approve|reject) fail: the settlement gateway proxies these to spi /api/v1/balance/requests/:id/approve|reject, which DO NOT EXIST in the spi router (only GET /balance/requests/pending). Every approval/rejection 404s/503s.  
  `pix/frontend/admin/src/views/balance/BalanceRequestDetailView.vue:107-132; service services/balance.ts 284-298; backend spi_proxy_controller.ex 406-431; spi router apps/spi_service/lib/spi_service_web/router.ex 79-106 (no approve/reject route)` -> FIX: Implement spi POST /balance/requests/:id/approve and /reject with LSO/RSO dual-approval logic and a backing table; then the proxy and UI work.

**balance/request/credit** _(balance_alcada)_
- [back/dead_action] Submit fails: createRequest POSTs to /api/v1/balance/requests; settlement create_balance_request proxies to spi POST /api/v1/balance/requests, but the spi router has no such route (only GET /balance/requests/pending). Result is 404/503, request is never created.  
  `pix/backend/apps/settlement_service/lib/settlement_service_web/controllers/gateway/spi_proxy_controller.ex:391-399; spi router apps/spi_service/lib/spi_service_web/router.ex 79-106 (no POST)` -> FIX: Implement spi POST /balance/requests (+ backing table and LSO/RSO workflow) and proxy to it.

**balance/request/debit** _(balance_alcada)_
- [back/dead_action] Same dead backend as credit: createRequest -> proxy -> non-existent spi POST /balance/requests. Debit request never persists.  
  `pix/backend/apps/settlement_service/lib/settlement_service_web/controllers/gateway/spi_proxy_controller.ex:391-399` -> FIX: Implement the real spi balance-request create route.

**balance/request/aporte** _(balance_alcada)_
- [back/dead_action] Aporte submit hits the same dead create_balance_request proxy -> non-existent spi POST /balance/requests. Meanwhile spi DOES have a real liquidity inject route (POST /api/v1/admin/balance/:ispb/inject, BalanceController.inject_liquidity) that this view ignores.  
  `pix/backend/apps/settlement_service/lib/settlement_service_web/controllers/gateway/spi_proxy_controller.ex:391-399; real route spi router apps/spi_service/lib/spi_service_web/router.ex 211` -> FIX: Either implement the generic balance-request create, or point Aporte at the existing /admin/balance/:ispb/inject flow (with dual approval) so it actually injects liquidity.

**alcada/parameters** _(balance_alcada)_
- [back/dead_action] Edit and Activate/Deactivate fail. openEditModal->handleSubmit->updateParameter and handleToggleActive->toggleParameterActive both call PUT /api/v1/alcada/parameters/:id. The settlement gateway's /alcada scope does NOT proxy PUT (no update_alcada_parameter function, no route) — only GET list/show, POST create, POST check, GET registrations, POST visto. So every edit/toggle 404s. (Create works.)  
  `pix/backend/apps/settlement_service/lib/settlement_service_web/router.ex:491-499 (alcada scope, no put parameters/:id); controller spi_proxy_controller.ex has no update_alcada_parameter; frontend service services/alcada.ts 99-102` -> FIX: Add `put "/parameters/:id", SpiProxyController, :update_alcada_parameter` to the settlement /alcada scope and implement the proxy function (spi already has PUT /alcada/parameters/:id -> AlcadaController.update_parameter).

**accounting/monitor** _(accounting)_
- [front/mock] Tela inteira e simulada. So chama /api/v1/fees/schedules e fabrica: (a) um unico summary card a partir da contagem de tarifas ativas; (b) 'Lancamentos Recentes' mapeando schedules de tarifa em linhas com type:'FEE_COLLECTION', status inventado e amount cru. Para schedule PERCENTAGE, o 'amount' e na verdade um percentual exibido como R$. Nao reflete nenhum lancamento contabil real (ignora /api/v1/accounting/events e journal-entries que existem).  
  `views/accounting/AccountingMonitorView.vue:57-90` -> FIX: Trocar a fonte por /api/v1/accounting/events (ou journal-entries) para 'recent entries' e por balance-sheet/income-statement para os cards; remover a fabricacao a partir de fees.

**security/groups** _(security)_
- [back/mock] groupService.list → /api/v1/groups → AdminController.list_groups returns get_permission_groups(), a HARDCODED array of 5 groups (Administrators/LSO/RSO/Operators/Viewers) with id 1-5 and only name/description/permissions. No code, no entity_id, no can_approve_lso/rso. So GroupListView 'Código' column (line 193) is blank and LSO/RSO badges (206-207) never render. This is mock data, not the real groups table.  
  `apps/settlement_service/lib/settlement_service_web/controllers/gateway/admin_controller.ex:list_groups + get_permission_groups` -> FIX: Query the real groups/roles table and return code, entity_id, can_approve_lso, can_approve_rso, member_count.
- [front/dead_action] 'Novo Grupo' button → router.push('/security/groups/new') (line 70), row click → '/security/groups/:id' (74), 'Editar' → '/security/groups/:id/edit' (78). NONE of these routes exist in router/index.ts (only 'security/groups' is registered). They navigate to a non-existent route (blank/404).  
  `src/views/security/GroupListView.vue:69-79` -> FIX: Add the group new/detail/edit routes + views, or remove the navigation/buttons.

**reports/transactions** _(reports)_
- [front/dead_action] Export button sends templateId 'tpl-statement', but real template id is 'statement'. Controller resolve_type -> get_template('tpl-statement')=nil -> raw 'tpl-statement' -> canonical 'TPL-STATEMENT' -> {:error,:unknown_report_type} -> HTTP 422. Export never produces a file; user only sees alert('Erro ao exportar').  
  `pix/frontend/admin/src/views/reports/TransactionReportView.vue:95` -> FIX: Change templateId to 'statement' (Generator template id, backend reports/generator.ex L129).

**reports/balance** _(reports)_
- [front/dead_action] Export sends templateId 'tpl-statistics' — there is NO 'statistics' template in the backend Generator (only compliance/statement/audit/qrcode). Always 422.  
  `pix/frontend/admin/src/views/reports/BalanceReportView.vue:113` -> FIX: Remove the export or point at an existing template id; STATISTICS is not implemented.

**reports/fees** _(reports)_
- [front/dead_action] Export sends templateId 'tpl-qrcode' (real id 'qrcode') -> 422, export silently fails.  
  `pix/frontend/admin/src/views/reports/FeeReportView.vue:149` -> FIX: Change to 'qrcode' (or a fee-specific template if added).

**reports/audit** _(reports)_
- [front/dead_action] Export sends templateId 'tpl-audit' (real id 'audit') -> unknown type 'TPL-AUDIT' -> 422; export never produces a file.  
  `pix/frontend/admin/src/views/reports/AuditReportView.vue:151` -> FIX: Change templateId to 'audit'.

**reports/view** _(reports)_
- [front/mock] The 'Resumo Executivo' + 'Detalhamento' preview table is driven by tableData populated from pick('table_data','tableData'), a field the backend NEVER returns (metadata has no row data; CSV lives only at /download). tableData is therefore ALWAYS empty, so the report preview renders empty headers with zero rows for every report — a fake preview that never shows actual content.  
  `pix/frontend/admin/src/views/reports/ReportViewerView.vue:237-294,422-423` -> FIX: Fetch and parse the real CSV from /api/v1/reports/generated/:id/download to render rows, or remove the fake preview and show row_count + a download CTA.
- [front/dead_action] The 'Baixar' button handler only console.log + alert('Baixar iniciado'); it never calls reportsService.downloadReport, so the viewer's download produces no file (unlike ReportHistoryView which downloads correctly).  
  `pix/frontend/admin/src/views/reports/ReportViewerView.vue:434-443` -> FIX: Replace the stub with the real download: await reportsService.downloadReport(report.id) + blob save, as in ReportHistoryView.handleDownload.

**/settings** _(simulator_system)_
- [front/mock] SettingsView persists ONLY to localStorage via useSettingsStore (settings.ts:70-76); NO backend call. Notification toggles (email/SMS/transactionAlerts) and session-timeout/autoLogout are saved locally and never reach any server — they have no system effect. Presented as system settings but are purely cosmetic local prefs.  
  `frontend/admin/src/stores/settings.ts:70-79,143-156` -> FIX: If these must affect the system, wire updateSettings to a backend user-preferences endpoint (none exists — backend_missing). Otherwise relabel as local-only UI preferences.

### MEDIUM / LOW (condensado)

**/sync/cid** _(dashboard_monitor_camt)_: [l/mock] Events empty-state shows a hardcoded explanatory note ('BACEN homolog  (CidSyncView.vue:402-408)
**/transactions/:id** _(transactions)_: [m/dead_action] Botao 'Solicitar Devolucao' chama store.createReturn que faz POST /tra (transaction.ts:111-114 (createReturn envia {amount,reason}); backend return_controller.ex:47-53)
**keys** _(keys_qr)_: [m/dead_action] The 'Buscar' search box sends filters.search (line 43) but EntryContro (KeyListView.vue:43,151-158)
**qrcodes** _(keys_qr)_: [m/dead_action] Rows show TX ID styled as a clickable blue link (text-blue-600) but th (QrCodeListView.vue:186-193)
**participants** _(participants_ops)_: [m/dead_action] Table rows are not clickable and contain no router-link to participant (ParticipantsView.vue:211-247)
**participants/:ispb** _(participants_ops)_: [m/dead_action] Botao 'Editar participante' do menu Acoes chama handleEdit, que apenas (ParticipantDetailView.vue:65-67,168); [l/dead_action] Itens 'Enviar REDA014', 'Enviar REDA031', 'Gerar APIX001' sao divs des (ParticipantDetailView.vue:177-203)
**participants/operations  (= operations/participants redirect)** _(participants_ops)_: [m/dead_action] Botao 'Ver Detalhes' (icone olho) na coluna Acoes da tabela nao tem @c (ParticipantOperationsView.vue:492-498)
**participants/message-statistics  (= operations/message-statistics redirect)** _(participants_ops)_: [l/dead_action] Botao 'Exportar' nao tem @click — nao exporta nada. (MessageStatisticsView.vue:113-118)
**participants/legacy-parameters  (= operations/legacy-parameters redirect)** _(participants_ops)_: [m/dead_action] Botao '+ Nova Parametro' apenas seta showNewParameterModal=true; o mod (LegacyParametersView.vue:14,377,470-489)
**balance/requests** _(balance_alcada)_: [m/dead_action] The per-row 'eye' Button (view detail) has no @click handler — only th (BalanceRequestsListView.vue:299-304)
**balance/request/credit** _(balance_alcada)_: [m/dead_action] Attachment upload is a dead feature: files are collected into attachme (CreditRequestView.vue:64-82 file handlers; 84-102 handleSubmit (no attachments))
**accounting/settlement-reports** _(accounting)_: [m/dead_action] Botao 'Baixar' so aparece se report.downloadUrl existir; normalizeFeeR (SettlementReportView.vue:291-300)
**accounting/netting** _(accounting)_: [l/dead_action] Rota duplicada: /accounting/netting e /accounting/netting-cycles rende (index.ts:559-568)
**security/users** _(security)_: [m/dead_action] Pagination 'Anterior' and 'Proximo' buttons are hardcoded disabled (li (UserListView.vue:522-533); [l/dead_action] The per-row 'Redefinir senha' (key) icon button (lines 492-499) has a  (UserListView.vue:492-499)
**security/users/:id** _(security)_: [m/dead_action] 'Revogar todas' (line 341-343) and per-session 'Revogar' (362-364) but (UserDetailView.vue:341-364, 30-47)
**dict/statistics** _(dict_monitors)_: [m/dead_action] The time-range selector (1h/6h/24h/7d/30d) only mutates local timeRang (StatisticsView.vue:9-10,73-96)
**/simulator** _(simulator_system)_: [l/mock] Page renders real data from GET /api/v1/simulator/status via simulator (SimulatorDashboardView.vue:60-62,147-149)
**/simulator/scenarios** _(simulator_system)_: [l/mock] Run button is correctly disabled when !store.isEnabled (line 154); in  (ScenarioRunnerView.vue:152-165)
**/simulator/exchanges** _(simulator_system)_: [l/mock] Real GET /api/v1/simulator/exchanges wiring; in live homolog with SIMU (MessageExchangeView.vue:52-59)
**/monitoring** _(simulator_system)_: [m/dead_action] Alert dismiss button (the X on each alert card) has NO @click handler  (MonitoringView.vue:306-310)
**/settings** _(simulator_system)_: [m/dead_action] The 'Idioma' (language) select changes localSettings.language and pers (SettingsView.vue:39-46,149-158)


## Onda C — i18n (externalizar strings, pt-BR/en/es/fr)  (76 itens, 15 high)

### HIGH

**keys** _(keys_qr)_
- [front/hardcoded_i18n] View has zero i18n — no useI18n/$t import. Every string is hardcoded pt-BR: headings 'Chaves PIX', 'Gerenciamento de chaves do diretório DICT', button 'Nova Chave', filter labels 'Buscar'/'Tipo de Chave'/'Status', keyTypeOptions/statusOptions labels (lines 19-33), table headers 'Chave/Tipo/Proprietario/ISPB/Status/Data Criação' (201-206), 'Carregando...', 'Nenhuma chave PIX encontrada', 'Anterior'/'Proximo', and the keyTypeLabel/statusLabel maps (85-114).  
  `pix/frontend/admin/src/views/keys/KeyListView.vue:19-33,85-114,133-248` -> FIX: Replace all literals with t('keys.list.*') and move keyTypeOptions/statusOptions/label maps to i18n; add useI18n() like the rest of the admin.

**keys/new** _(keys_qr)_
- [front/hardcoded_i18n] Zero i18n. Hardcoded pt-BR throughout: 'Nova Chave PIX', 'Cadastrar uma nova chave no diretório DICT', section titles 'Tipo e Valor da Chave'/'Proprietario'/'Dados da Conta', all field labels, keyTypeOptions (22-28), keyPlaceholder map (30-39), validation messages 'Informe o valor da chave PIX' etc (45-56), success 'Chave PIX cadastrada com sucesso!', buttons 'Cadastrando...'/'Cadastrar Chave'/'Cancelar'.  
  `pix/frontend/admin/src/views/keys/KeyCreateView.vue:22-39,45-67,88-172` -> FIX: Move all to t('keys.create.*') and add useI18n().

**keys/:key** _(keys_qr)_
- [front/hardcoded_i18n] Zero i18n. Hardcoded pt-BR: 'Detalhes da Chave PIX', action buttons 'Bloquear'/'Desbloquear'/'Excluir', label maps keyTypeLabel/statusLabel (115-142), all card titles 'Valor da Chave'/'Proprietario'/'Dados da Conta'/'Datas', dt labels, dialog texts 'Bloquear Chave PIX'/'Excluir Chave PIX' and their warning paragraphs, toasts 'Chave bloqueada com sucesso!' etc.  
  `pix/frontend/admin/src/views/keys/KeyDetailView.vue:115-142,156-322` -> FIX: Move all to t('keys.detail.*').

**qrcodes** _(keys_qr)_
- [front/hardcoded_i18n] Zero i18n. Hardcoded pt-BR: 'QR Codes PIX', 'Gerenciamento de QR Codes estaticos e dinamicos', 'Novo QR Code', typeOptions/statusOptions labels (18-30), table headers 'TX ID/Tipo/Valor/Comerciante/Status/Data Criação/Expira em' (176-182), typeLabel/statusLabel maps (82-108), 'Carregando...', 'Nenhum QR Code encontrado', 'Anterior'/'Proximo'.  
  `pix/frontend/admin/src/views/qrcodes/QrCodeListView.vue:18-30,82-108,118-224` -> FIX: Move all to t('qrcodes.list.*').

**qrcodes/new** _(keys_qr)_
- [front/hardcoded_i18n] Zero i18n. Hardcoded pt-BR: 'Novo QR Code PIX', 'Gerar um QR Code estático ou dinâmico', radio cards 'Estatico'/'Dinamico' + descriptions, section titles 'Tipo de QR Code'/'Chave e Valor'/'Dados do Comerciante', all field labels/placeholders, validation messages (25-40), success message (58), buttons 'Gerando...'/'Gerar QR Code'/'Cancelar'.  
  `pix/frontend/admin/src/views/qrcodes/QrCodeCreateView.vue:25-40,58,85-169` -> FIX: Move all to t('qrcodes.create.*').

**claims** _(claims_med)_
- [front/hardcoded_i18n] View has NO useI18n/t(). Every UI string is hardcoded pt-BR: title 'Reivindicações DICT', subtitle 'Portabilidade e reivindicacao de posse de chaves PIX', filter labels 'Tipo'/'Status', typeOptions/statusOptions labels ('Todos','Portabilidade','Reivindicação de Posse','Aberta','Confirmada','Cancelada','Concluida'), table headers 'Claim ID'/'Tipo'/'Chave'/'Requisitante'/'Status'/'Data', empty state 'Nenhuma reivindicacao encontrada', pagination 'Anterior'/'Proximo'/'Mostrando'. Also note missing accents ('reivindicacao','Concluida').  
  `pix/frontend/admin/src/views/claims/ClaimListView.vue:18-30,77-103,113-217` -> FIX: Import useI18n, move all literals to pt-BR.json under a claims.* namespace and replace with t(). Fix accents while migrating.

**claims/:id** _(claims_med)_
- [front/hardcoded_i18n] No useI18n. All strings hardcoded pt-BR: 'Detalhes da Reivindicação', 'Confirmar'/'Cancelar'/'Processando...', success/error toasts ('Reivindicação confirmada com sucesso!','Erro ao confirmar reivindicacao'), section headers 'Chave PIX'/'Requisitante'/'Datas', field labels 'Nome'/'ISPB'/'Criado em'/'Atualizado em', type/keyType/status label maps. 'Reivindicação não encontrada'.  
  `pix/frontend/admin/src/views/claims/ClaimDetailView.vue:46-115,129-226` -> FIX: Add useI18n and migrate all literals to claims.* keys with t().

**security/settings** _(security)_
- [front/hardcoded_i18n] Only the h1 uses t('security.settings') (line 170). The ENTIRE rest of this 725-line view is hardcoded pt-BR: tab labels array (40-45), warning banners (189-194, 554-556), every UiCard title and every checkbox/input label across Password/Session/Login/MFA/IP/Audit tabs (e.g. 'Requisitos de senha' 241, 'Exigir letra maiúscula' 272, 'Configuração de sessão' 352, 'Filtro por endereço IP' 536, 'Configuração de logs de auditoria' 644), 'Salvar alterações' (177), 'Tentar novamente' (212).  
  `src/views/security/SecuritySettingsView.vue:40-45, 170-720` -> FIX: Externalize all literals to t() keys.

**security/mfa** _(security)_
- [front/hardcoded_i18n] The setup wizard (steps 1-3) and status card are properly i18n'd via t('security.mfa.*'), BUT the entire 'manage MFA when enabled' section is a mix of hardcoded pt-BR AND raw ENGLISH: 'Status' (304), 'Desativar MFA' header/body (578-581), buttons 'Confirm Disable' (653) and 'Cancel' (660) in English, 'Backup Codes' (669), 'Generate new backup codes. This will invalidate all previously generated codes.' (671), 'TOTP Code' (676), 'Regenerate' (701), 'Save these new backup codes. Previous codes have been invalidated.' (709), 'Código TOTP'/'Senha'/'Informe sua senha' (601-617). Inconsistent language in the same screen.  
  `src/views/security/MfaSetupView.vue:304, 578-709` -> FIX: Replace all literals in the enabled-MFA section with t('security.mfa.*') keys; remove the English strings.

**security/certificates** _(security)_
- [front/hardcoded_i18n] This view does NOT import useI18n at all — 100% hardcoded pt-BR: 'Certificados PIX Monetarie' (222), 'Atualizar' (227), summary cards 'Ativos/Saudaveis/Alertas/Vencidos' (242-272), 'Upload PIC / PIA' (280), 'Tipo/Formato/Arquivo/Senha do PFX' (283-343), 'Registrar certificado' (372), table headers 'Tipo/CN/Validade/Status/Ações' (393-397), statusLabel map (196-203), error strings (79, 92, 119, 172, 214), 'Uso operacional' block (453-470).  
  `src/views/security/CertificateManagementView.vue:196-474` -> FIX: Add useI18n and externalize all strings; or accept as Portuguese-only and at least fix the accent typos below.

**/simulator** _(simulator_system)_
- [front/hardcoded_i18n] ENTIRE view is hardcoded pt-BR with no useI18n/t(). Examples: 'Simulador BACEN', 'Simulador de mensagens SPI/DICT do Banco Central', 'Ativo'/'Inativo', 'Atualizar', 'Total Processado', 'Aceitas', 'Rejeitadas', 'Cenarios Executados', 'Configuracao', 'Taxa de Aceitacao', 'Acoes Rapidas', 'Executar Cenarios', 'Troca de Mensagens', 'Cenarios Disponiveis'. Also scenarioLabel() map (lines 34-58) hardcodes 21 pt-BR scenario labels.  
  `frontend/admin/src/views/simulator/SimulatorDashboardView.vue:34-58,70-210` -> FIX: Add const { t } = useI18n() and move all strings + scenarioLabel map into locales/pt-BR.json (e.g. simulator.* keys), mirroring SystemConfigView/AuditLogsView.

**/simulator/scenarios** _(simulator_system)_
- [front/hardcoded_i18n] Entire view hardcoded pt-BR, no t(): 'Cenarios de Teste', 'Execute e monitore cenarios...', 'Voltar ao Dashboard', 'Executar Cenario', 'Selecione um cenario...', 'Executar'/'Executando...', 'Simulador desabilitado...', 'Resultado', 'Tipo Mensagem', 'Tempo (ms)', 'Erro', 'Historico de Execucoes', 'Nenhuma execucao registrada', headers 'Nome/Status/Mensagens/Sucesso/Falhas/Iniciado/Acoes', 'Detalhes'. Plus duplicated scenarioLabel/getStatusLabel pt-BR maps (45-82).  
  `frontend/admin/src/views/simulator/ScenarioRunnerView.vue:45-82,122-329` -> FIX: Introduce useI18n() and move strings + status/scenario maps to locales; dedupe the scenarioLabel map shared with SimulatorDashboardView into one helper.

**/simulator/exchanges** _(simulator_system)_
- [front/hardcoded_i18n] Entire view hardcoded pt-BR, no t(): 'Troca de Mensagens', 'Historico de mensagens XML...', 'Voltar ao Dashboard', 'Todas direcoes', 'Entrada (Inbound)', 'Saida (Outbound)', placeholder 'Tipo de mensagem (ex: pacs.008)', 'Filtrar', 'Mensagens', 'Nenhuma mensagem encontrada', headers 'ID Mensagem/Tipo/Direcao/Remetente/Destinatario/Status/HTTP/Data/XML', 'Req'/'Resp', 'Pagina X de Y', 'Anterior'/'Proxima', 'Requisicao'/'Resposta', 'Fechar'. getDirectionLabel (38-40) hardcodes 'Entrada'/'Saida'.  
  `frontend/admin/src/views/simulator/MessageExchangeView.vue:38-40,84,104-258` -> FIX: Add useI18n() and externalize all strings to locales/pt-BR.json under a simulator.exchanges.* namespace.

**/monitoring/realtime** _(simulator_system)_
- [front/hardcoded_i18n] Entire view hardcoded in ENGLISH (this is a pt-BR admin): 'Live operational dashboard', 'Connected'/'Reconnecting...'/'Disconnected', 'TPS', 'error rate', 'Live Transactions', 'Waiting for transactions...', 'System Health', 'Updated:', 'Settlement', 'No recent events', 'DICT Operations', 'No recent operations', 'BACEN Channels', 'CPM (Primary)', 'CSM (Secondary)', 'Requests:', 'Failures:', 'Last check:', 'Queue Depths', 'Waiting for data...'/'Connecting...'. Only the H1 uses t('monitoring.realtime').  
  `frontend/admin/src/views/monitoring/RealTimeMonitoringView.vue:73,82-87,97,129,136,172,179,193,200,215,222,226,235-236,241,250-254,261,282,285` -> FIX: Add useI18n() and move every string to locales/pt-BR.json (and en-US) under monitoring.realtime.*.

**/settings** _(simulator_system)_
- [front/hardcoded_i18n] Entire view hardcoded pt-BR, no useI18n(): tab labels 'Preferencias/Notificacoes/Exibicao/Sessao/Acessibilidade' (16-22), 'Configuracoes' x2, 'Personalize sua experiencia...', 'Restaurar Padrao', 'Salvar Alteracoes', 'Idioma', 'Tema', 'Claro/Escuro/Sistema', 'Notificacoes por Email/SMS', 'Alertas de Transacao', 'Visualizacao Compacta', 'Itens por Pagina', 'Timeout de Sessao', 'Logout Automatico', accessibility labels, plus the showToast alert() strings.  
  `frontend/admin/src/views/settings/SettingsView.vue:16-22,51,58,92-117,147-465` -> FIX: Add useI18n() and move all strings to locales under settings.*.

### MEDIUM / LOW (condensado)

**/** _(dashboard_monitor_camt)_: [m/hardcoded_i18n] formatAuditAction/formatAuditDetail/resourceLabel/formatRelativeTime p (DashboardView.vue:135-218, 250-264); [l/hardcoded_i18n] Hardcoded admin emails mapped by seed id ('1'->admin@monetarie.com.br  (DashboardView.vue:165-169)
**/monitor** _(dashboard_monitor_camt)_: [m/hardcoded_i18n] Timeline tab is hardcoded pt-BR while the rest of the view uses t(): ' (OperationsMonitorView.vue:103-104, 737, 741, 744, 748-749, 770, 786)
**/sync/cid** _(dashboard_monitor_camt)_: [m/hardcoded_i18n] Entire view is hardcoded pt-BR with NO useI18n: title 'Sincronizacao D (CidSyncView.vue:33-59, 176-204, 218-462)
**/camt060** _(dashboard_monitor_camt)_: [m/hardcoded_i18n] Entire view is hardcoded pt-BR with NO useI18n: tab labels/description (Camt060ToolView.vue:30-99, 121-143, 171-174, 306-510)
**/transactions** _(transactions)_: [m/hardcoded_i18n] View nao usa useI18n/t(); todos os rotulos sao pt-BR hardcoded: 'Trans (TransactionListView.vue:18-82 (mapas), 158-297 (template))
**/transactions/new** _(transactions)_: [m/hardcoded_i18n] Sem i18n; tudo pt-BR hardcoded: 'Nova Transacao PIX', 'Crie uma transa (TransactionCreateView.vue:29-44 (arrays), 50-67 (validacao), 88-198 (template))
**/transactions/:id** _(transactions)_: [m/hardcoded_i18n] Sem i18n; pt-BR hardcoded: 'Detalhes da Transacao', cards (Status/Dire (TransactionDetailView.vue:18-82, 237-466)
**/transactions/returns** _(transactions)_: [m/hardcoded_i18n] Sem i18n; pt-BR hardcoded: 'Devolucoes', 'Listagem de devolucoes PIX ( (ReturnListView.vue:11-16, 63-133)
**/statements** _(statements_extratos_sessions)_: [m/hardcoded_i18n] View imports no useI18n and uses zero t() calls. All UI strings hardco (StatementListView.vue:16-21,99-117,127-138,145-262 (template))
**/statements/generate** _(statements_extratos_sessions)_: [m/hardcoded_i18n] No useI18n; all strings hardcoded pt-BR: 'Gerar Extrato','Solicitar ge (StatementGenerateView.vue:19-23,29-45,72-152)
**/sessions** _(statements_extratos_sessions)_: [m/hardcoded_i18n] No useI18n; all strings hardcoded pt-BR: 'Sessões de Liquidação','Gere (SessionListView.vue:17-32,70-131,146-276)
**participants/:ispb** _(participants_ops)_: [m/hardcoded_i18n] As 3 secoes (ResponsiblesSection, WebhooksSection, LimitsSection) nao  (ResponsiblesSection.vue, WebhooksSection.vue, LimitsSection.vue:ResponsiblesSection 45,53-58,126; WebhooksSection 58,73-77,106; LimitsSection 55,131,132,137); [l/hardcoded_i18n] Strings de erro/labels do proprio ParticipantDetailView ('Participante (ParticipantDetailView.vue:51,119,146,150,158,268)
**participants/operations  (= operations/participants redirect)** _(participants_ops)_: [m/hardcoded_i18n] View inteira sem useI18n; titulos e rotulos ('Operacoes por Participan (ParticipantOperationsView.vue:174-176,287,343,402,409-430,507)
**participants/approval-limits  (= operations/approval-limits redirect)** _(participants_ops)_: [m/hardcoded_i18n] Sem useI18n; rotulos, operationTypes labels ('Pagamento PIX', 'Devoluc (ApprovalLimitsView.vue:17-28,137,169,181,271,324)
**participants/message-statistics  (= operations/message-statistics redirect)** _(participants_ops)_: [m/hardcoded_i18n] Sem useI18n; titulos, cards, filtros e colunas ('Estatisticas de Mensa (MessageStatisticsView.vue:98,132,201-203,222,228-254)
**participants/pix-saque-troco  (= operations/pix-saque-troco redirect)** _(participants_ops)_: [m/hardcoded_i18n] Sem useI18n; titulos, cards, status labels ('Pendente','Concluido',... (PixSaquePixTrocoView.vue:6,37,109-123,157-176,488-498)
**participants/rco-monitor  (= operations/rco-monitor redirect)** _(participants_ops)_: [m/hardcoded_i18n] Sem useI18n; 'Monitor de RCO (TIR)', 'Reserva de Contingencia', cards  (RcoMonitorView.vue:6-11,43,59)
**participants/response-time  (= operations/response-time redirect)** _(participants_ops)_: [m/hardcoded_i18n] Sem useI18n; titulos, colunas e CSV labels pt-BR hardcoded. (ResponseTimeReportView.vue:15,420-433)
**participants/legacy-parameters  (= operations/legacy-parameters redirect)** _(participants_ops)_: [m/hardcoded_i18n] Sem useI18n; rotulos e badges pt-BR hardcoded ('Parametros Legados', ' (LegacyParametersView.vue:443,464,485,504)
**balance** _(balance_alcada)_: [m/hardcoded_i18n] Entire view is hardcoded pt-BR with no useI18n/t(): 'Gestão de Saldos' (BalanceDashboardView.vue:84-122 label maps; 146-388 template)
**balance/requests** _(balance_alcada)_: [m/hardcoded_i18n] All strings hardcoded pt-BR (no t()): 'Solicitações de Saldo', 'Gerenc (BalanceRequestsListView.vue:75-113 maps; 149-307 template)
**balance/requests/:id** _(balance_alcada)_: [m/hardcoded_i18n] All labels hardcoded pt-BR: 'Solicitação #', 'Detalhes da Solicitação' (BalanceRequestDetailView.vue:43-93 maps; 164-433 template)
**balance/history** _(balance_alcada)_: [m/hardcoded_i18n] All strings hardcoded pt-BR incl. CSV header 'Data,Saldo Inicial,Credi (BalanceHistoryView.vue:80 CSV header; 118-130 template)
**balance/parameters** _(balance_alcada)_: [m/hardcoded_i18n] All strings hardcoded pt-BR: 'Parâmetros de Saldo', 'Saldo Mínimo (Ale (BalanceParametersView.vue:120-540 template)
**balance/request/credit** _(balance_alcada)_: [m/hardcoded_i18n] All strings hardcoded pt-BR incl. 'Solicitar Crédito', urgency option  (CreditRequestView.vue:24-28 urgency options; 46-62 validation msgs; 105-345 template)
**balance/request/debit** _(balance_alcada)_: [m/hardcoded_i18n] All strings hardcoded pt-BR (urgency options, validation msgs, heading (DebitRequestView.vue:35-54 options; 102-119 msgs; 165+ template)
**balance/request/aporte** _(balance_alcada)_: [m/hardcoded_i18n] All strings hardcoded pt-BR: 'Solicitação de Aporte Enviada!', urgency (LiquidityInjectionView.vue:38-74 options; 90-115 msgs; 150+ template)
**alcada/parameters** _(balance_alcada)_: [m/hardcoded_i18n] All strings hardcoded pt-BR: 'Parâmetros de Alçada', table headers, mo (AlcadaParametersView.vue:60-72 priority; 179-546 template)
**alcada/pending** _(balance_alcada)_: [m/hardcoded_i18n] All strings hardcoded pt-BR: 'Aprovações Pendentes', 'Fila de operaçõe (AlcadaPendingView.vue:69-85 labels; 156-173 template)
**alcada/history** _(balance_alcada)_: [m/hardcoded_i18n] All strings hardcoded pt-BR: 'Histórico de Alçada' header area, filter (AlcadaHistoryView.vue:104-130 labels; 199+ template)
**accounting** _(accounting)_: [m/hardcoded_i18n] Toda a tela usa strings pt-BR fixas, sem t()/useI18n. Ex.: titulo, sub (AccountingDashboardView.vue:47-48,82,95,110,167,189,195-199,212)
**accounting/reconciliation** _(accounting)_: [m/hardcoded_i18n] Tela inteira hardcoded pt-BR (titulo, STATUS_OPTIONS, getStatusLabel,  (ReconciliationView.vue:19-25,65-100,152-153,193,206,221,234,418,421,430,439)
**accounting/settlement-reports** _(accounting)_: [m/hardcoded_i18n] Toda a tela hardcoded pt-BR (titulo, TRANSACTION_TYPE_OPTIONS, getRepo (SettlementReportView.vue:18-25,74-90,134,168,190-195,239-242,264-271,315)
**accounting/netting-cycles** _(accounting)_: [m/hardcoded_i18n] Tela 100% hardcoded pt-BR (titulo, typeOptions, statusOptions, statusL (NettingCyclesView.vue:22-37,127-137,174-175,181,225,270-277,302-305)
**accounting/fees** _(accounting)_: [m/hardcoded_i18n] Tela inteira hardcoded pt-BR (titulo, FEE_TYPE_OPTIONS, TRANSACTION_TY (FeeManagementView.vue:31-52,87-94,173,258,269,280,304-311,508,521)
**accounting/cost-centers** _(accounting)_: [l/hardcoded_i18n] Label 'Budget (R$)' no dialog esta hardcoded em ingles enquanto o rest (CostCentersView.vue:257)
**accounting/events** _(accounting)_: [m/hardcoded_i18n] Opcoes de tipo de evento no dialog estao hardcoded em ingles (Adjustme (AccountingEventsView.vue:211-214)
**security/users** _(security)_: [m/hardcoded_i18n] useI18n imported but only the h1/subtitle use t() (lines 318-319). Eve (UserListView.vue:70-79, 129, 301-348, 363-385, 405-421, 449, 519-529)
**security/users/new** _(security)_: [m/hardcoded_i18n] Header uses t() (193-197) but the whole form body is hardcoded pt-BR:  (UserFormView.vue:45-79, 211-396)
**security/users/:id** _(security)_: [m/hardcoded_i18n] Imports useI18n but t() is essentially unused in the template — all la (UserDetailView.vue:123-139, 167-433)
**reports** _(reports)_: [m/hardcoded_i18n] View is 100% hardcoded pt-BR (title 'Relatórios', KPI labels 'Tipos Di (ReportsListView.vue:18-29,93-303)
**reports/transactions** _(reports)_: [m/hardcoded_i18n] Entire view hardcoded pt-BR (title 'Extrato de Transações', 'Parâmetro (TransactionReportView.vue:20-30,48-69,120-303)
**reports/balance** _(reports)_: [m/hardcoded_i18n] Fully hardcoded pt-BR (title 'Histórico de Saldos', 'Parâmetros do Rel (BalanceReportView.vue:16-26,139-403)
**reports/fees** _(reports)_: [m/hardcoded_i18n] Fully hardcoded pt-BR (title 'Relatório de Tarifas', 'Tarifas por Tipo (FeeReportView.vue:18-31,97-106,174-447)
**reports/audit** _(reports)_: [m/hardcoded_i18n] Fully hardcoded pt-BR (title 'Relatório de Auditoria', ACTION_OPTIONS, (AuditReportView.vue:7-31,89-103,177-433)
**reports/custom** _(reports)_: [m/hardcoded_i18n] Fully hardcoded pt-BR (title 'Relatório Personalizado', 'Salvar e Exec (CustomReportView.vue:6-280,319-416)
**reports/history** _(reports)_: [m/hardcoded_i18n] Fully hardcoded pt-BR plus a LOCAL re-implementation of formatReportTy (ReportHistoryView.vue:6-19,162-186,294-314,428-452)
**reports/scheduler** _(reports)_: [m/hardcoded_i18n] Fully hardcoded pt-BR (title 'Agendamento de Relatórios', 'Novo Agenda (ReportSchedulerView.vue:6-30,149-330,353-368,493-524)
**reports/view** _(reports)_: [m/hardcoded_i18n] Fully hardcoded pt-BR plus local re-implementations of formatReportTyp (ReportViewerView.vue:27-31,101,151,238,298,469-493)
**dict/indicators** _(dict_monitors)_: [l/hardcoded_i18n] Hardcoded pt-BR strings bypass t(): 'Meta:' (line 129), 'Sem dados' (l (DictIndicatorsView.vue:9-30,129-132)
**dict/policies** _(dict_monitors)_: [l/hardcoded_i18n] Hardcoded pt-BR: '+ Nova politica' (107), table headers 'Instituição'/ (DictPoliciesView.vue:41-47,107,121-127,162-215)
**dict/reports** _(dict_monitors)_: [m/hardcoded_i18n] Pervasive hardcoded ENGLISH table headers in a pt-BR admin: 'Type','Co (DictReportsView.vue:95-204)
**dict/statistics** _(dict_monitors)_: [l/hardcoded_i18n] Hardcoded pt-BR: 'Taxa de sucesso' (60), 'Quebra por tipo de mensagem' (StatisticsView.vue:60,124-132,217-219)
**monitors/files** _(dict_monitors)_: [l/hardcoded_i18n] Hardcoded pt-BR: 'Total de arquivos' (91), 'Tamanho total' (95), 'Todo (FileMonitorView.vue:36-47,91-130,152,185)
**monitors/tariffs** _(dict_monitors)_: [l/hardcoded_i18n] Hardcoded pt-BR: feeType label map and status map (65-82), summary car (TariffMonitorView.vue:64-82,108,114-246)
**/system-config** _(simulator_system)_: [l/hardcoded_i18n] readonlyKeys list and a console.error ('Falha ao carregar configuracoe (SystemConfigView.vue:39,54)
**/monitoring** _(simulator_system)_: [m/hardcoded_i18n] Mixed i18n: titles use t() but several strings hardcoded pt-BR: header (MonitoringView.vue:79-82,255-260,318)
**/audit-logs** _(simulator_system)_: [m/hardcoded_i18n] Mostly i18n but several hardcoded pt-BR strings remain: 'Historico de  (AuditLogsView.vue:186,193,266,277,318-319,375-376)
**/system/health** _(simulator_system)_: [l/hardcoded_i18n] View uses i18n extensively (good), but SERVICE_DETAILS descriptions ar (systemHealth.ts:33,38,43,48,53)


## Onda D — Purga de travessao (—)  (15 itens, 1 high)

### HIGH

**security/api-clients** _(security)_
- [front/em_dash] Rendered AI em-dash: formatDate returns '—' (U+2014) for empty dates (line 56), which appears in the 'Último uso' column of the table. The owner forbids the em-dash in UI output.  
  `src/views/security/ApiClientsView.vue:56` -> FIX: Return '-' (hyphen) instead of '—'.

### MEDIUM / LOW (condensado)

**/** _(dashboard_monitor_camt)_: [l/em_dash] U+2014 em-dash in code comments (not user-facing, but owner forbids th (DashboardView.vue:78, 115)
**/monitor** _(dashboard_monitor_camt)_: [l/em_dash] 22 U+2014 occurrences; most are '—' empty-value placeholders (e.g. htt (OperationsMonitorView.vue:95, 98, 192, 197, 204, 537, 561, 770, 772, 776)
**/sync/cid** _(dashboard_monitor_camt)_: [l/em_dash] 9 U+2014: placeholder '—' returns in formatDate/formatRelative/formatD (CidSyncView.vue:177, 182, 195, 332, 347, 429, 431, 434, 436)
**/camt060** _(dashboard_monitor_camt)_: [m/em_dash] User-facing U+2014 in headings/copy: page title 'CAMT.060 — Consulta d (Camt060ToolView.vue:6, 34, 62, 161, 306, 308, 502)
**/transactions** _(transactions)_: [l/em_dash] Travessao U+2014 em comentario de codigo (nao user-facing, mas o dono  (TransactionListView.vue:17)
**/transactions/:id** _(transactions)_: [l/em_dash] Travessao U+2014 em comentarios de codigo. (TransactionDetailView.vue:17, 64, 149, 154)
**med/fraud-markers** _(claims_med)_: [m/em_dash] Literal em-dash U+2014 used as the tax_id empty placeholder in the tab (FraudMarkerListView.vue:296)
**med/fraud-markers/:id** _(claims_med)_: [m/em_dash] Literal em-dash U+2014 as tax_id empty placeholder: `{{ marker.tax_id_ (FraudMarkerDetailView.vue:167)
**med/refunds** _(claims_med)_: [m/em_dash] Multiple literal em-dashes U+2014 used as empty placeholders in the ta (RefundListView.vue:318,327,328,345)
**accounting/chart-of-accounts** _(accounting)_: [l/em_dash] Travessao U+2014 presente em comentario de codigo (regra do dono proib (ChartOfAccountsView.vue:38)
**security/api-clients** _(security)_: [l/em_dash] Em-dash in a code comment (not rendered): '// older browsers: noop — u (ApiClientsView.vue:157)
**reports/compliance** _(reports)_: [l/em_dash] Em-dash U+2014 present in two JS source comments (not rendered). Owner (ComplianceReportView.vue:8,12)
**reports/scheduler** _(reports)_: [l/em_dash] Em-dash U+2014 in a JS source comment (not rendered). (ReportSchedulerView.vue:347)
**/simulator/scenarios** _(simulator_system)_: [m/em_dash] AI em-dash U+2014 in the scenario <option> label: '{{ scenarioLabel(s. (ScenarioRunnerView.vue:149)


## Onda E — UX e ajustes restantes  (62 itens, 3 high)

### HIGH

**qrcodes/new** _(keys_qr)_
- [front/ux] Created QR result is discarded. createStatic/createDynamic return qr_code_data including payload (EMV copia-e-cola) and txid, but handleSubmit ignores the response and just routes back to /qrcodes after a toast (lines 53-61). The user never sees the generated QR image / copia-e-cola string — the whole point of generating a QR. Combined with the missing detail view, a created dynamic QR's payload is unreachable anywhere in the admin.  
  `pix/frontend/admin/src/views/qrcodes/QrCodeCreateView.vue:43-61` -> FIX: Capture the returned qr_code, show the payload + a rendered QR (qrcode lib) on a success step or navigate to a detail view instead of silently redirecting.

**med/refunds** _(claims_med)_
- [front/ux] loadData() early-returns when recoveryIdFilter is blank (refunds=[]; total=0; NO API call), so the default /med/refunds view always shows the 'enterRecovery' empty state. The backend list_refund_requests does NOT require recovery_id — it pages all rows filtered only by status (dict_proxy_controller.ex:372-399), and the MED hub lists refunds globally without recovery_id. The mandatory-recovery_id UX is a false constraint that hides all refunds.  
  `pix/frontend/admin/src/views/med/RefundListView.vue:58-65,287-293` -> FIX: Remove the `if (!recoveryIdFilter.value) return` gate (and the required asterisk / scoped-listing banner). Make recovery_id an optional filter like status, and call listRefunds with whatever filters are set.

**reports/custom** _(reports)_
- [front/ux] The entire 'Campos Disponíveis' + 'Campos Selecionados' + 'Filtros' builder (SOURCE_TABLES, AVAILABLE_FIELDS, aggregations, operators) is cosmetic: selections fold into params.fields/params.filters, but the backend Generator produces output purely from report_type and ignores fields/filters (the view's own comment admits it). Users build a detailed definition with zero effect on the CSV — misleading UI.  
  `pix/frontend/admin/src/views/reports/CustomReportView.vue:108-283,486-510` -> FIX: Implement field/filter-driven generation in the backend Generator, or disable the builder and keep only name+type+description.

### MEDIUM / LOW (condensado)

**/** _(dashboard_monitor_camt)_: [l/ux] SPB balance widget is a permanent hardcoded 'Em breve' placeholder (de (DashboardView.vue:378-395)
**/transactions** _(transactions)_: [l/ux] Filtro de instrumento oferece IPMF/MANU/DICT/QRES/QRDN, mas se o backe (TransactionListView.vue:75-82)
**/transactions/new** _(transactions)_: [l/ux] ISPB do recebedor tem '*' no label mas o input nao tem `required` (so  (TransactionCreateView.vue:153-154)
**keys** _(keys_qr)_: [l/other] pt-BR accent/spelling: option labels 'Chave Aleatoria (EVP)' (should b (KeyListView.vue:25,203,248)
**keys/new** _(keys_qr)_: [l/other] Accent/spelling in copy: 'Chave Aleatoria (EVP)', 'Chaves EVP sao gera (KeyCreateView.vue:27,121,128,150)
**keys/:key** _(keys_qr)_: [l/other] pt-BR errors in dialog copy: 'Tem certeza que deseja bloquear está cha (KeyDetailView.vue:288,314,319,236,254)
**qrcodes** _(keys_qr)_: [l/other] Accent/spelling: 'estaticos e dinamicos' (estáticos/dinâmicos), 'Estat (QrCodeListView.vue:119,20-21,82-88,224)
**qrcodes/new** _(keys_qr)_: [l/other] Accent/spelling: radio labels 'Estatico'/'Dinamico' (estático/dinâmico (QrCodeCreateView.vue:108,116)
**/statements** _(statements_extratos_sessions)_: [l/ux] Type filter mismatch: frontend filter sends type=camt.052/053/054 to S (statement_controller.ex:build_filters / maybe_put_statement_type)
**/sessions** _(statements_extratos_sessions)_: [l/ux] cancelSession is called without a reason from the list (sessionService (SessionListView.vue:136 (canCancel), 249-253 (button))
**med** _(claims_med)_: [l/ux] KPI totals come from each list endpoint's total. listRefunds here is c (MedHubView.vue:43-58)
**med/funds-recoveries/:id** _(claims_med)_: [l/ux] The 'Gerar rastreamento' button is disabled unless recovery.status === (FundsRecoveryDetailView.vue:204,210-212)
**med/infractions/:id** _(claims_med)_: [l/ux] Action buttons are gated on exact status strings: acknowledge shows fo (InfractionDetailView.vue:149,157,164)
**participants/pix-saque-troco  (= operations/pix-saque-troco redirect)** _(participants_ops)_: [l/ux] Esta view usa classes dark: (dark mode) enquanto as demais views da se (PixSaquePixTrocoView.vue:6,14,31)
**operations/response-time** _(participants_ops)_: [l/other] Rota e apenas um redirect para /participants/response-time; herda todo (index.ts:416-419)
**operations/pix-saque-troco** _(participants_ops)_: [l/other] Redirect para /participants/pix-saque-troco; herda os problemas dessa  (index.ts:420-423)
**operations/rco-monitor** _(participants_ops)_: [l/other] Redirect para /participants/rco-monitor; herda o mapeamento semantico  (index.ts:424-427)
**operations/message-statistics** _(participants_ops)_: [l/other] Redirect para /participants/message-statistics; herda o endpoint incom (index.ts:428-431)
**operations/participants** _(participants_ops)_: [l/other] Redirect para /participants/operations; herda o botao 'Ver Detalhes' m (index.ts:432-435)
**operations/legacy-parameters** _(participants_ops)_: [l/other] Redirect para /participants/legacy-parameters; herda a semantica troca (index.ts:436-439)
**operations/approval-limits** _(participants_ops)_: [l/other] Redirect para /participants/approval-limits; herda os PUT/DELETE /api/ (index.ts:440-443)
**balance/history** _(balance_alcada)_: [m/ux] Depends on localStorage 'activeInstitutionIspb'. If unset, the view ne (BalanceHistoryView.vue:52-58); [l/ux] PDF export is just window.print() (prints the whole page chrome), not  (BalanceHistoryView.vue:92-94)
**balance/request/debit** _(balance_alcada)_: [l/other] Unused import: Input component imported but not used (also in Requests (DebitRequestView.vue:8)
**accounting** _(accounting)_: [m/ux] Tema escuro quebrado: cores fixas text-gray-900/text-gray-600/bg-white (AccountingDashboardView.vue:47,48,81,178,200,204)
**accounting/reconciliation** _(accounting)_: [l/ux] Acentuacao ausente em varios rotulos (Concluido, Excecoes, Corresponde (ReconciliationView.vue:23,69,193,206,221,234,255,314,320); [m/ux] Tema escuro quebrado: bg-white/text-gray-900 fixos no header, cards, m (ReconciliationView.vue:152,192,294,417,418)
**accounting/settlement-reports** _(accounting)_: [m/ux] Tema escuro quebrado (bg-white/text-gray-900 fixos). (SettlementReportView.vue:134,168,199,275,314)
**accounting/netting-cycles** _(accounting)_: [m/ux] Tema escuro quebrado: usa classe 'card' generica mas tambem text-gray- (NettingCyclesView.vue:174,190,268,281); [l/ux] Coluna 'Ciclo' mostra cycle.type cru (d0/d1/d2 minusculo) ao lado do i (NettingCyclesView.vue:283)
**accounting/fees** _(accounting)_: [l/ux] Aba 'Tabelas de Tarifas' (schedules) e card de subtitulo usam termos s (FeeManagementView.vue:40-52,258,269); [m/ux] Tema escuro quebrado (bg-white/text-gray-900 fixos no header, modal, t (FeeManagementView.vue:173,300,315,472,507)
**accounting/chart-of-accounts** _(accounting)_: [m/ux] Tema escuro quebrado: text-gray-900/bg-gray-50/bg-white fixos; sem dar (ChartOfAccountsView.vue:58,113,124,127)
**accounting/history** _(accounting)_: [m/ux] Tema escuro quebrado (bg-gray-50/bg-white/text-gray-900 fixos). (StandardHistoryView.vue:86,150,153,166)
**accounting/cost-centers** _(accounting)_: [m/ux] Tema escuro quebrado (text-gray-900/bg-white fixos nos cards e modal). (CostCentersView.vue:125,174,239,257)
**accounting/events** _(accounting)_: [m/ux] Tema escuro quebrado (bg-gray-50/bg-white/text-gray-900 fixos). (AccountingEventsView.vue:98,150,153,205)
**accounting/monitor** _(accounting)_: [m/ux] Tema escuro quebrado e mensagem de erro hardcoded pt-BR ('Falha ao car (AccountingMonitorView.vue:92-93,106,133,165,187)
**security/users** _(security)_: [m/other] Corrupted XML entity in escapeXml: line 279 emits '&após;' instead of  (UserListView.vue:279); [l/ux] 'Proximo' is missing its accent (should be 'Próximo'). (UserListView.vue:529)
**security/users/new** _(security)_: [l/ux] Typo: 'Nenhum grupo disponível para está instituição' should be 'esta  (UserFormView.vue:306)
**security/settings** _(security)_: [m/ux] handleSave only persists the ACTIVE tab (switch on activeTab, 71-90).  (SecuritySettingsView.vue:64-97, 220-233)
**security/groups** _(security)_: [l/ux] This view IS correctly using t() throughout (security.groups.* keys ex (GroupListView.vue:92-234)
**security/scopes (and /scopes)** _(security)_: [l/other] Two routes ('security/scopes' and bare 'scopes') both mount the same S (index.ts:390-398); [l/other] Well-implemented: real data, full i18n via t('scopeManagement.*') (key (ScopesView.vue:118, 195)
**security/api-clients** _(security)_: [l/other] Otherwise solid: full t('apiClients.*') i18n (keys exist), real CRUD + (apiClients.ts:37)
**security/mfa** _(security)_: [l/other] Setup/verify/disable/backup-codes flows correctly target /api/v1/auth/ (mfa.ts:29-61)
**security/certificates** _(security)_: [l/ux] Accent typos in hardcoded labels: 'Saudaveis' (should be 'Saudáveis',  (CertificateManagementView.vue:200, 214, 251); [l/other] Fully wired to the real PIX certificate backend (mTLS PIC/CPIC + XMLDS (certificates.ts:136-207)
**reports/audit** _(reports)_: [l/ux] Data is aggregated by period; for groupBy=MONTH the period key is 'YYY (AuditReportView.vue:84-87,416)
**reports/scheduler** _(reports)_: [l/ux] Run-now and toggle feedback uses native alert()/confirm() rather than  (ReportSchedulerView.vue:493,504,507)
**reports/view** _(reports)_: [m/other] totalPages is hardcoded to 5 (const totalPages = 5). The pager shows ' (ReportViewerView.vue:366,203,299)
**monitors/integration** _(dict_monitors)_: [l/ux] direction/status filters are duplicated: the view filters client-side  (IntegrationMonitorView.vue:10-11,19-28)
**monitors/files** _(dict_monitors)_: [l/ux] The fileType filter is applied client-side (filteredFiles, FileMonitor (FileMonitorView.vue:9-23)
**/monitoring** _(simulator_system)_: [m/other] Fake/hardcoded target captions under metric cards: 'Meta: 1,500 TPS' ( (MonitoringView.vue:224,233,242)
**/audit-logs** _(simulator_system)_: [l/ux] Client-side search (filteredLogs) only filters the CURRENT page of ser (AuditLogsView.vue:119-140,375)
**/settings** _(simulator_system)_: [l/ux] Uses native alert() for save/reset feedback (showToast -> alert), bloc (SettingsView.vue:7-11,51,58)
**/system/health** _(simulator_system)_: [m/other] SERVICE_DETAILS in systemHealth.ts is hardcoded reference data (ports, (systemHealth.ts:31-57)
**/tools/xml-viewer** _(simulator_system)_: [l/other] Pure client-side tool: xmlService.parseXml/searchXml parse in-browser  (xml.ts:8-18); [l/other] Regex-based XML parser (/<(\w+)[^>]*>([^<]+)<\/\1>/g) only captures si (xml.ts:3-31)


## Eixos transversais

### i18n audit of Monetarie PIX admin frontend (Vue 3 + vue-i18n) — config, language switcher, hardcoded pt-BR views, missing keys
i18n IS wired and the header language switcher mechanically works (it sets vue-i18n's reactive `locale.value`, so any string rendered via t() re-renders on switch). The real defect is COVERAGE, not the switcher: 62 of 105 views (59%) never import useI18n and hardcode pt-BR directly in their <template>, so switching language is a no-op for the bulk of the product. Five locales exist (pt-BR, en-US, es-ES, zh-HK, fr) but only pt-BR is complete (2374 lines / 1848 keys); the other four are ~half-size (1212 lines / ~1050 keys) and main.ts deep-merges each over pt-BR as fallback — so even the 43 i18n-aware views silently show pt-BR text wherever a non-pt locale lacks the key. Missing-key risk is low for STATIC keys: of 937 statically-used t() keys, all resolve in pt-BR (the single 'monitors.' flag is a false positive from a `t('monitors.'+x)` dynamic prefix; the `monitors` namespace exists). The gap is the OTHER FOUR locales missing ~half the keys, not pt-BR missing keys. Separately, 81 em-dash (U+2014) occurrences across 25 .vue files: most are <script> comments, but several are USER-VISIBLE labels/titles (e.g. Camt060ToolView title, RecurrenceListView subtitle, ScenarioRunnerView) which the owner forbids; the `|| '—'` empty-value placeholders are cosmetic but also U+2014 and should be normalized. NOTE: this run inspected i18n wiring and template strings; it did not open every Pinia store/service/backend controller per view (that is the per-section data-vs-mock mandate) — findings here are scoped to the i18n question asked.

- i18n configuration location and locales. vue-i18n is created in main.ts (not a plugins/i18n* file). Five locale JSON files exist under src/locales/: pt-BR, en-US, es-ES, zh-HK, fr. legacy:false (Composition API), default locale from localStorage or 'pt-BR', fallbackLocale 'pt-BR'. pt-BR is loaded as baseMessages and the other 4 are deep-merged OVER pt-BR via mergeLocaleMessages — so any key missing in en/es/zh/fr silently falls back to Portuguese.  
  `/Users/luizpenha/.config/superpowers/worktrees/monetarie/fix-camt060-verifier/pix/frontend/admin/src/main.ts:3,9-13,39-53` -> FIX: No bug in the wiring itself. Document the 5-locale set. The fallback-merge is the mechanism that masks the half-empty non-pt locales (see locale-size finding). Optionally switch to a glob/auto-import of locales so new namespaces are picked up uniformly.
- Locale completeness gap (the real 'missing keys' problem). pt-BR.json = 2374 lines / 1848 flattened keys. en-US, es-ES, fr, zh-HK = 1212 lines / ~1050 keys each — roughly HALF the keys. Because main.ts merges each over pt-BR, switching to English/Spanish/Chinese/French shows translated text only where those files have the key and Portuguese everywhere else. So 'switch to English' produces a bilingual screen, not a fully English one.  
  `/Users/luizpenha/.config/superpowers/worktrees/monetarie/fix-camt060-verifier/pix/frontend/admin/src/locales/en-US.json:whole file (1212 vs pt-BR 2374)` -> FIX: Backfill en-US/es-ES/zh-HK/fr to key-parity with pt-BR.json. Concrete strategy: (1) flatten pt-BR keyset as source of truth; (2) for each other locale compute pt_keys minus locale_keys; (3) generate the missing subtree (translate, do NOT copy pt-BR text); (4) add a CI guard that fails when any locale's keyset != pt-BR keyset. Same flatten script used in this audit can be reused.
- Header language switcher works mechanically. Header.vue setLocale() assigns locale.value (vue-i18n reactive ref) and mirrors into uiStore. useLocale.ts composable does the same. Therefore views that render via t() DO re-render on language change. The switcher is NOT the bug.  
  `/Users/luizpenha/.config/superpowers/worktrees/monetarie/fix-camt060-verifier/pix/frontend/admin/src/components/layout/Header.vue:32-36,90,107` -> FIX: No change needed to the switcher. Two minor robustness notes: (a) Header.vue duplicates the languages[] array that already exists in useLocale.ts availableLocales — dedupe by importing the composable; (b) selected locale is persisted to localStorage by main.ts read but Header/useLocale write through uiStore.setLocale — confirm uiStore persists to localStorage('locale') so the choice survives reload.
- 62 of 105 views do NOT import useI18n and hardcode pt-BR in <template> — switching language does nothing for them. This is the core i18n defect. Full list captured via comm of all views vs views containing useI18n. Entire feature areas are affected: ALL of balance/ (10 views), ALL of reports/ (10 views), ALL of alcada/ (3), most of participants/ (11), all of simulator/ (3), statements/ (2), transactions/ (4), messages/ (4), keys/ (3), claims/ (2), qrcodes/ (2), plus auth/LoginView, settings/SettingsView, sessions, sync/CidSyncView, camt060.  
  `/Users/luizpenha/.config/superpowers/worktrees/monetarie/fix-camt060-verifier/pix/frontend/admin/src/views/balance/LiquidityInjectionView.vue:171,191,213,240,273-274,347,413,439,443-457 (e.g. 'Solicitar Aporte de Liquidez','Aprovação Executiva Necessária','Resumo da Solicitação', placeholders 'Ex: Conta Tesouraria...')` -> FIX: Front-bug. Per view: add `import { useI18n } from 'vue-i18n'` + `const { t } = useI18n()`, extract every hardcoded <template> string into a namespaced key under that feature's namespace in pt-BR.json (e.g. balance.liquidity.*), replace with t('...'), then add the same keys to the other 4 locales. Prioritize by user-visibility and offender count (see next finding).
- Worst hardcoded-pt-BR offenders (by count of accented/label strings in template), all WITHOUT useI18n. Use as remediation priority order.  
  `/Users/luizpenha/.config/superpowers/worktrees/monetarie/fix-camt060-verifier/pix/frontend/admin/src/views:LiquidityInjectionView(35); reports/CustomReportView(33); balance/DebitRequestView(33); balance/BalanceParametersView(31); transactions/TransactionDetailView(30); balance/CreditRequestView(30); camt060/Camt060ToolView(29); participants/RcoMonitorView(26); alcada/AlcadaParametersView(26); balance/BalanceDashboardView(25); reports/ReportHistoryView(24); participants/ParticipantOperationsView(24); participants/ApprovalLimitsView(24); settings/SettingsView(23); messages/MessageBuilderView(23)` -> FIX: Tackle these 15 first (they carry the densest hardcoded UI). Each needs useI18n import + key extraction as described above. Group keys by feature namespace to keep pt-BR.json organized and make the 4-locale backfill mechanical.  (x62)
- Static t() keys are essentially fully defined in pt-BR — there is NO meaningful pt-BR missing-key problem. 937 distinct static t('...') keys are used across vue/ts; all resolve against pt-BR's 1848 keys. The lone flag 'monitors.' is a false positive: IntegrationMonitorView builds keys dynamically with t('monitors.'+x) and the `monitors` namespace exists (keys title/integration/files/tariffs/status...).  
  `/Users/luizpenha/.config/superpowers/worktrees/monetarie/fix-camt060-verifier/pix/frontend/admin/src/views/monitors/IntegrationMonitorView.vue:14-16,44-45,51,81-82,101,107` -> FIX: No pt-BR keys to add for static usage. Caveat: 22 dynamic t(`...`) template-literal sites (AuditLogsView, ParticipantsView, MonitoringView, SystemConfigView, participants/ApprovalLimitsView, participants/LegacyParametersView, admin/CertificatesView, admin/ScopesView, balance/DebitRequestView, balance/CreditRequestView, ...) cannot be statically verified — spot-check that every runtime-composed key (status/type enums) has a pt-BR entry to avoid raw-key flashes.
- Em-dash U+2014 in USER-VISIBLE labels/titles (forbidden by owner). These render to users, not comments. Examples: Camt060ToolView page-title 'CAMT.060 — Consulta de Saldo e Lançamentos' and subtitle 'BACEN Vol VI v5.12 — 7 ações...'; RecurrenceListView 'PIX Automático — Pagamentos recorrentes'; ScenarioRunnerView option label '... passos) — {description}'.  
  `/Users/luizpenha/.config/superpowers/worktrees/monetarie/fix-camt060-verifier/pix/frontend/admin/src/views/camt060/Camt060ToolView.vue:306,308 (Camt060ToolView); RecurrenceListView.vue:127; simulator/ScenarioRunnerView.vue:149` -> FIX: Replace every U+2014 in user-visible strings with ' - ' (hyphen+spaces) or restructure the sentence. When these strings move into i18n keys, ensure the key VALUES contain no em-dash either.
- Em-dash U+2014 used as empty-value placeholder `|| '—'` in many table cells/detail fields. Cosmetic 'no value' marker but still U+2014, which the owner forbids project-wide.  
  `/Users/luizpenha/.config/superpowers/worktrees/monetarie/fix-camt060-verifier/pix/frontend/admin/src/views/sync/CidSyncView.vue:332,347,429,431,434,436; also med/RefundListView.vue:318,327,328,345; med/FraudMarkerListView.vue:296; med/FraudMarkerDetailView.vue:167; automation/ScheduledMessagesView.vue:298,307; components/admin/BacenSmokeTestDialog.vue:208` -> FIX: Define ONE i18n placeholder key (e.g. common.empty = '-') and replace all `|| '—'` with `|| t('common.empty')`, or at minimum normalize the literal to a hyphen '-'. Total em-dash footprint = 81 occurrences across 25 .vue files; the remainder are <script> comments (e.g. StatusTabs.vue:2, AutoRefreshControl.vue:6, DashboardView.vue:78,115) which are not user-facing but should still be cleaned to satisfy the no-em-dash rule.  (x81)

### AI em-dash (U+2014) audit across PIX admin frontend + backend; PIXOUT id-generator check
EXHAUSTIVE em-dash (U+2014 "—") inventory of the worktree. Frontend tree raw count = 144 lines; backend raw count is in the hundreds, but the overwhelming majority are JS/Elixir comments, @doc/@typedoc docstrings, test `describe` labels and migration docstrings — none of those reach a user. I separated genuinely USER-VISIBLE occurrences (rendered text, hardcoded labels/headings, i18n strings, persisted ledger/reconciliation `description:` fields shown in admin views) from inert comments.

KEY NEGATIVE FINDING the owner specifically asked about: there is NO "PIXOUT—" id generator anywhere in this worktree. grep -rn "PIXOUT" over backend AND frontend/admin/src = ZERO hits. The real id generators (end_to_end_id `E`+ISPB+..., NATS msg_id, message_id) do NOT embed an em-dash — verified by grepping every id/msg_id/end_to_end pattern: the only em-dashes near those are inside Logger strings, never inside the produced id. So no id-generator fix is required in THIS worktree; the "PIXOUT—" pattern reported by the owner does not exist here (it may live on the deployed image, a different branch, or the Core/SPB tree, which is out of scope).

USER-VISIBLE em-dash that the owner forbids and MUST be replaced:
FRONTEND hardcoded labels/headings: RecurrenceListView.vue:127; Camt060ToolView.vue:34/62/306/308; ScenarioRunnerView.vue:149.
FRONTEND placeholder glyphs ('—' as empty-value display): 40 occurrences across OperationsMonitorView, CidSyncView, RefundListView, FraudMarker views, ScheduledMessagesView, ApiClientsView, Camt060ToolView, BacenSmokeTestDialog — display fallbacks for null fields; replace with '-' or a shared empty constant / t('common.empty').
BACKEND user-visible persisted text shown in admin statements/reconciliation/accounting: reconciliation.ex:451 (discrepancy description); status_updater.ex:567/655, outbound_sender.ex:407, inbound_processor.ex:1952 (ledger `description:` memos written to accounting and rendered in statement/audit views).
BACKEND operator logs: many Logger.* lines contain '—' (log readability, lower priority, not in customer UI). The Portuguese owner-reply path in core_event_processor.ex is CLEAN (the em-dash is in the Logger.warning at :219, not in the user reply at :226).
i18n locale JSON (pt-BR/en-US/es-ES/fr/zh-HK) are CLEAN of em-dash.

FIX: replace U+2014 with hyphen '-' (or restructure) in the user-visible spots above; for the 40 placeholder glyphs replace '—' with '-' or t('common.empty'); no id-generator change needed here because no em-dash-embedding id generator exists in this worktree.

- OWNER'S SPECIFIC ASK - NO 'PIXOUT-em-dash' id generator exists. grep -rn PIXOUT over backend AND frontend/admin/src = ZERO hits. The real id/msg_id/end_to_end_id generators do not embed an em-dash (only nearby Logger strings do). No id-generator fix required in THIS worktree; the PIXOUT-em-dash pattern is not present here (likely deployed image / Core / SPB tree, out of scope).  
  `backend + frontend/admin/src (whole worktree):grep result: 0 matches for PIXOUT` -> FIX: No change here. If the live 'PIXOUT-...' id with an em-dash is observed in homolog, the generator lives outside this worktree (Core/SPB or the deployed pix image); locate it there and change the literal em-dash to a hyphen, e.g. "PIXOUT-" <> id.
- Hardcoded em-dash in user-visible subtitle: 'PIX Automatico — Pagamentos recorrentes' (template literal, not t()). Also hardcoded pt-BR (no i18n).  
  `frontend/admin/src/views/RecurrenceListView.vue:127` -> FIX: Replace '—' with '-' and move to i18n t('recurrence.subtitle'). e.g. 'PIX Automatico - Pagamentos recorrentes'.  (x1)
- Hardcoded em-dash in CAMT.060 tool: two action descriptions, the page heading and the subtitle. 'CSA — Saldo da Conta PI...', 'Termo de Identificacao de Recolhimento — detalhamento...', '<h1>CAMT.060 — Consulta de Saldo e Lancamentos</h1>', 'BACEN Vol VI v5.12 — 7 acoes de consulta...'. All hardcoded pt-BR, no t().  
  `frontend/admin/src/views/camt060/Camt060ToolView.vue:34, 62, 306, 308` -> FIX: Replace each '—' with '-' (or ':' / restructure); ideally move to i18n keys. e.g. 'CAMT.060 - Consulta de Saldo e Lancamentos'.  (x4)
- Hardcoded em-dash in scenario <option> label: '{{ scenarioLabel(s.name) }} ({{ s.steps }} passos) — {{ s.description }}'. User-visible.  
  `frontend/admin/src/views/simulator/ScenarioRunnerView.vue:149` -> FIX: Replace ' — ' with ' - ' (or ': '). e.g. '...passos) - {{ s.description }}'.  (x1)
- Placeholder em-dash glyph used as empty-value fallback in rendered cells/fields (computed return '—', detail rows ?? '—', table cells, signature/PI/event fields).  
  `frontend/admin/src/views/monitor/OperationsMonitorView.vue:95, 98, 192, 193, 197, 204, 537, 541, 561, 663, 667, 671, 710, 714, 718, 722, 726, 730, 772, 776` -> FIX: Owner forbids U+2014. Replace each '—' with '-' or 'N/D', or introduce a shared EMPTY constant (or t('common.empty')) and use it everywhere. 20 occurrences in this file.  (x20)
- Placeholder em-dash glyph as empty-value fallback in CID Sync view (formatters returning '—' and table cells).  
  `frontend/admin/src/views/sync/CidSyncView.vue:177, 182, 195, 332, 347, 429, 431, 434, 436` -> FIX: Replace '—' with '-' / t('common.empty'). 9 occurrences.  (x9)
- Placeholder em-dash glyph in Refund list cells (end_to_end_id, contestant_ispb, funds_recovery_id, terminal-status dash).  
  `frontend/admin/src/views/med/RefundListView.vue:318, 327, 328, 345` -> FIX: Replace '—' with '-' / shared empty constant. 4 occurrences.  (x4)
- Placeholder em-dash glyph in ScheduledMessages view (formatDate fallback + table cells).  
  `frontend/admin/src/views/automation/ScheduledMessagesView.vue:189, 298, 307` -> FIX: Replace '—' with '-' / t('common.empty'). 3 occurrences.  (x3)
- Placeholder em-dash glyph: balance.source empty fallback and a date/value formatter fallback (distinct from the label hits at 34/62/306/308).  
  `frontend/admin/src/views/camt060/Camt060ToolView.vue:161, 502` -> FIX: Replace '—' with '-'. 2 occurrences.  (x2)
- Placeholder em-dash glyph: tax_id_number empty fallback in Fraud Marker detail and list.  
  `frontend/admin/src/views/med/FraudMarkerDetailView.vue + frontend/admin/src/views/med/FraudMarkerListView.vue:FraudMarkerDetailView.vue:167; FraudMarkerListView.vue:296` -> FIX: Replace '—' with '-'. 2 occurrences (1 each).  (x2)
- Placeholder em-dash glyph: ApiClients secret-mask fallback returns '—'.  
  `frontend/admin/src/views/security/ApiClientsView.vue:56` -> FIX: Replace '—' with '-'. 1 occurrence.  (x1)
- Placeholder em-dash glyph: raw request_xml empty fallback in BACEN smoke-test dialog.  
  `frontend/admin/src/components/admin/BacenSmokeTestDialog.vue:208` -> FIX: Replace '—' with '-'. 1 occurrence.  (x1)
- BACKEND user-visible persisted text: reconciliation discrepancy `description:` 'No BACEN camt.053 statement available for reconciliation — cannot proceed without real data'. Surfaced in reconciliation/discrepancy admin views.  
  `backend/apps/settlement_service/lib/settlement_service/reconciliation.ex:451` -> FIX: Replace ' — ' with ' - ' (or split into two sentences). e.g. '...for reconciliation - cannot proceed without real data'.  (x1)
- BACKEND user-visible persisted ledger memos: accounting/TigerBeetle transfer `description:` strings written to the ledger and rendered in statement/audit/accounting admin views. 'PIX rejection credit-back — tx ...', 'PIX return credit — tx ...', 'PIX outbound debit — {message_type} {message_id}', 'PIX inbound/return — {message_id}'.  
  `backend/apps/spi_service/lib/spi_service/workers/status_updater.ex + outbound_sender.ex + inbound_processor.ex:status_updater.ex:567, status_updater.ex:655, outbound_sender.ex:407, inbound_processor.ex:1952` -> FIX: Replace ' — ' with ' - ' in each `description:` literal (user-visible ledger memos). e.g. 'PIX outbound debit - #{message_type} #{message_id}'. 4 occurrences.  (x4)
- BACKEND operator-log em-dash (NOT in customer UI, lower priority, but owner forbids the char in generated text): Logger.warning/info/error and a few raise strings across spi_service workers and shared (dict_simulator, partition_manager, jetstream, base_worker, circuit_breaker, channel_router), internal_client, scheduler, schedule_worker, chain_blocker, message_builder. NOTE core_event_processor.ex:219 is a Logger line; the user REPLY at :226 is CLEAN.  
  `backend/apps/* (Logger.* and raise string literals):internal_client.ex:91,124; scheduler.ex:362; outbound_sender.ex:132,141,149,160,173,291,427,436; inbound_processor.ex:629,632; core_event_processor.ex:219; schedule_worker.ex:65; chain_blocker.ex:280; dict_simulator.ex:144,233,310,325,390,1484; partition_manager.ex:56,132; jetstream.ex:307; base_worker.ex:123,611; circuit_breaker.ex:135; channel_router.ex:294; client.ex:121; message_builder.ex:1383; entry_controller.ex:96` -> FIX: Replace ' — ' with ' - ' in these Logger/raise string literals to comply with the no-em-dash rule. Dozens of lines; a scoped replacement over backend/apps/**/*.ex string literals (excluding test/ and comment lines) is appropriate. ~30+ occurrences in runtime .ex code.  (x30)

### PIX admin — Participant detail (Responsáveis/Webhooks/Limites) mock & dead-action audit + cross-front mock/em-dash inventory
The Participant DETAIL screen is the worst offender in the PIX admin: the header card reads a REAL participant from the BACEN directory endpoint (GET /api/v1/bacen/pix-participants?ispb=…, served by settlement_service BacenPixParticipantController), but ALL THREE accordion sections (Responsáveis, Limites, Webhooks) are 100% frontend mock/placeholder with NO backend behind them. Verified empirically: grep over the entire backend (apps/*/lib/*_web/router.ex) shows NO route for participant webhooks, NO route for participant responsibles/contacts, and NO route for per-participant limits. So these are NOT "front-bug, wire the API" — the endpoints do not exist; they must be built back-to-front or the sections removed. ResponsiblesSection ships a hardcoded fake row ("System Administrator / admin@monetarie.com.br") and all 3 buttons (Add/Edit/Remove) are empty no-op functions. WebhooksSection is empty-state-forever (webhooks=[] hardcoded, Add/Edit/Remove/Toggle all no-op; Toggle only flips local state). LimitsSection hardcodes fake limits (R$1M diário / R$50M mensal / R$500k por transação, "utilizado"=0 always) and "Editar Limites" opens a "Funcionalidade em desenvolvimento" placeholder modal. ParticipantDetailView itself: the "Ações" dropdown has 3 greyed dead items (Enviar REDA014/REDA031/Gerar APIX001, "Disponível em versão futura") and "Editar participante" only closes the dropdown (no edit screen, no API). The Info card "AGENCIA" and the entire "Conta PI" card (Agência + Número da Conta) are hardcoded "---" — never populated. The 3 section components also have ZERO i18n (all pt-BR strings hardcoded) and the parent only uses t() for the breadcrumb. Separately, a systematic owner-rule violation: 9 views render the AI em-dash U+2014 ("—") as the empty/null placeholder (41 occurrences) — OperationsMonitorView, CidSyncView, Camt060ToolView, the 3 MED views, ApiClientsView, ScheduledMessagesView, BacenSmokeTestDialog. The participants section itself correctly uses "---" (three hyphens), but the broader front leaks em-dashes the owner forbids. Note: I read only the worktree; ParticipantOperationsView/RcoMonitor/etc. consume a real Pinia store, so the mock is concentrated in the detail sub-components.

- ResponsiblesSection is 100% MOCK: hardcoded data row (no backend). A fake responsible 'System Administrator / Administrador / admin@monetarie.com.br / principal:true' is rendered for EVERY participant regardless of ispb. props.ispb is accepted but never used. This is a back-bug (no endpoint exists) compounded by a front mock.  
  `pix/frontend/admin/src/views/participants/ResponsiblesSection.vue:17-26` -> FIX: Backend: create a participant_responsibles resource (e.g. GET/POST/PUT/DELETE /api/v1/participants/:ispb/responsibles) in spi_service or settlement_service — NONE exists today (grep of all routers returns nothing for responsavel/responsible/contact). Frontend: add participantsService.listResponsibles(ispb)/create/update/remove in services/participants.ts, replace the hardcoded ref([...]) with onMounted load keyed by props.ispb. If the feature is not in scope for homolog, delete the fake row and show the real empty state ('Nenhum responsável cadastrado').  (x1)
- ResponsiblesSection dead actions: handleAdd, handleEdit, handleRemove are all empty no-op function bodies. The '+ Adicionar Responsável', edit (pencil) and remove (trash) buttons are wired to @click but do nothing.  
  `pix/frontend/admin/src/views/participants/ResponsiblesSection.vue:28-35 (handlers); buttons at 96, 105, 123` -> FIX: Wire each handler to the responsibles endpoint above (open a form modal for add/edit, confirm+DELETE for remove). Until the backend exists, remove the buttons rather than ship dead clicks.  (x3)
- WebhooksSection is empty-state-forever: webhooks is hardcoded ref<Webhook[]>([]) and never loaded from any API; props.ispb unused. The table/eventColors/formatDate logic is dead because the array is always empty. handleAdd/handleEdit/handleRemove are empty no-ops; handleToggle only flips local w.ativo with no persistence. No backend webhook endpoint exists for participants (grep returns only an unrelated DICT comment).  
  `pix/frontend/admin/src/views/participants/WebhooksSection.vue:16 (empty array), 18-29 (no-op handlers), 24-26 (local-only toggle)` -> FIX: Backend: build participant webhooks endpoints (GET/POST/PUT/DELETE/toggle /api/v1/participants/:ispb/webhooks) — they do not exist. Frontend: add service methods, onMounted load by props.ispb, wire the 4 handlers. If out of scope, keep the genuine empty state but remove the dead Add/Edit/Remove/Toggle buttons so nothing renders a fake control.  (x1)
- LimitsSection is MOCK: limits object hardcoded (daily=R$1.000.000, monthly=R$50.000.000, perTransaction=R$500.000, all *Used/transactionsToday=0). props.ispb unused — every participant shows the same fake limits and 0% utilização. No per-participant limit endpoint exists in the backend.  
  `pix/frontend/admin/src/views/participants/LimitsSection.vue:10-17` -> FIX: Backend: expose participant limits (GET /api/v1/participants/:ispb/limits with configured + used) — absent today. Frontend: add participantsService.getLimits(ispb), load on mount, drop the hardcoded object. If limits are not modeled, remove the section instead of showing fabricated R$ values that look real to the owner.  (x1)
- LimitsSection 'Editar Limites' is a placeholder action: clicking it opens a modal whose body literally reads 'Funcionalidade em desenvolvimento'. No form, no API call.  
  `pix/frontend/admin/src/views/participants/LimitsSection.vue:110-115 (button), 118-142 (placeholder modal), 132 ('Funcionalidade em desenvolvimento')` -> FIX: Either build a real edit form posting to the (not-yet-existing) limits endpoint, or remove the button and the placeholder modal entirely. Do not ship a 'em desenvolvimento' modal in a client-facing admin.  (x1)
- ParticipantDetailView 'Editar participante' is a dead action: handleEdit() only sets actionsOpen=false. No navigation to an edit form, no API. There is no participant edit route/screen wired.  
  `pix/frontend/admin/src/views/ParticipantDetailView.vue:65-67 (handleEdit), 167-175 (menu item)` -> FIX: Route to a participant edit form and call participantsService.update(ispb, …) (the service.update PUT /api/v1/participants/:id already exists). Until then, remove the menu item so it is not a dead click.  (x1)
- ParticipantDetailView 'Ações' dropdown has 3 permanently-disabled dead items: 'Enviar REDA014', 'Enviar REDA031', 'Gerar APIX001', each cursor-not-allowed with title 'Disponível em versão futura'. They render as greyed non-functional rows.  
  `pix/frontend/admin/src/views/ParticipantDetailView.vue:176-203 (177-185 REDA014, 186-194 REDA031, 195-203 APIX001)` -> FIX: Remove these placeholder menu items until the REDA/APIX flows exist (REDA send flows live in messages, not here), or wire them to the real message-builder routes. Shipping 3 'versão futura' items in a client admin is a placeholder the owner rejects.  (x3)
- ParticipantDetailView hardcodes placeholders for data that is never fetched: Info-card 'AGENCIA' = '---' and the entire 'Conta PI' card ('AGENCIA' + 'NÚMERO DA CONTA') = '---'. These fields have no source in the Participant type or the BACEN directory payload, so the card is decorative.  
  `pix/frontend/admin/src/views/ParticipantDetailView.vue:261 (Agencia '---'), 267-279 (Conta PI card both '---')` -> FIX: Either populate agência/conta PI from a real backend field (none exists in services/participants.ts Participant interface) or remove the empty Conta PI card and the Agência field so the screen does not display permanently blank labeled boxes.  (x1)
- i18n gap: the three detail sub-components use ZERO i18n — all UI strings are hardcoded pt-BR. ParticipantDetailView itself only uses t() for the breadcrumb and hardcodes everything else.  
  `pix/frontend/admin/src/views/participants/ResponsiblesSection.vue, WebhooksSection.vue, LimitsSection.vue, ../ParticipantDetailView.vue:Responsibles 45,53-58,126; Webhooks 58,73-77,106,157; Limits 55,59,77,81,97,103,131,132; DetailView 119,146,150,158,174,184,193,202,215-275 + statusLabel 86-92 + typeLabel 96-103` -> FIX: Wrap all rendered strings in t() and add keys under a participants.* namespace in locales/pt-BR.json (and en). Hardcoded strings include: 'Carregando participante...', 'Visualizar detalhes do participante', 'Visualizar Lista', 'Ações', 'Editar participante', 'Responsaveis', 'Limites Globais', 'Limites por Transação', 'Webhooks', 'Conta PI', 'NÚMERO DA CONTA', 'Nenhum responsável cadastrado', 'Nenhum webhook configurado', '+ Adicionar Responsável/Webhook', 'Editar Limites', 'Funcionalidade em desenvolvimento', column headers (NOME/CARGO/EMAIL/TELEFONE/PRINCIPAL/AÇÕES, URL/EVENTOS/STATUS/ÚLTIMO DISPARO), and the status/type label maps.  (x4)
- AI em-dash (U+2014 '—') rendered as the empty/null placeholder across 9 views, 41 occurrences — owner forbids the AI em-dash in user-facing text. The participants detail section correctly uses '---' (three hyphens); these other views leak '—'.  
  `pix/frontend/admin/src/views/monitor/OperationsMonitorView.vue, sync/CidSyncView.vue, camt060/Camt060ToolView.vue, med/FraudMarkerDetailView.vue, med/FraudMarkerListView.vue, med/RefundListView.vue, security/ApiClientsView.vue, automation/ScheduledMessagesView.vue, components/admin/BacenSmokeTestDialog.vue:OperationsMonitor 95,98,192-204,537,541,561,663-776; CidSync 177,182,195,332,347,429-436; Camt060Tool 34,62,161,306,502; ScheduledMessages 189,298,307; MED Refund 318,327,328,345; FraudMarkerList 296; FraudMarkerDetail 167; ApiClients 56; BacenSmokeTestDialog 208` -> FIX: Replace every quoted '—'/"—" placeholder with '-' or '---' (match the participants convention), and replace prose em-dashes in labels (e.g. Camt060 'CSA — Saldo…', RecurrenceListView 127 'PIX Automático — Pagamentos recorrentes') with a hyphen or restructured text. A single sweep: grep -rl "'—'\|\"—\"" src and substitute.  (x41)

### Monitor de Operações (/monitor) — root cause: Total 0 / 0 operações despite ~1929 rows in monetarie_audit.xml_audit_logs
CHAIN (all verified, same DB/table/prefix): /monitor route -> OperationsMonitorView.vue -> store monitor.ts -> service monitor.ts (GET /api/v1/admin/monitor/{operations,categories,stats}) -> SettlementServiceWeb.Admin.MonitorController (settlement_service) -> Shared.Repo -> Shared.Schemas.Audit.XmlAuditLog (@schema_prefix "monetarie_audit", table "xml_audit_logs"). The schema prefix, table name and Repo all match the 1929 rows exactly — it is NOT a wrong table, wrong schema prefix, or a different DB. RBAC is also not the cause: the request returns 200 with empty data (the table shows the empty-state string and the KPI cards render 0), whereas a RequirePermission failure would be a 403.

ROOT CAUSE (back + front, same bug on both layers): a DEFAULT 1-HOUR TIME WINDOW. build_query ALWAYS applies `where x.created_at >= ^filters.from`. When the client sends no explicit window (or sends the store default), `filters.from = DateTime.utc_now() - 3600s`. created_at is the row inserted_at, set to DateTime.utc_now() at write time by BacenLogger (timestamps inserted_at: :created_at). The ~1929 audit rows were written during earlier homolog test bursts, all more than 1h before the screen was opened, so `created_at >= now()-1h` matches ZERO rows. Every endpoint (stats.total, list operations, categories) is constrained by this same window, so all show 0. The frontend Pinia store hard-defaults to time_window:'1h' too, so even the very first load is clamped to the last hour. There is NO archived_at/soft-delete filter excluding rows and NO null-column filter — the only filter applied on a fresh, unmodified load is the time window. To prove/fix instantly: select '7d' (or 24h) in the "janela" dropdown -> the rows reappear (assuming they are within 7d); if even 7d is empty, the rows are simply older than 7 days and the largest selectable window (7d) cannot reach them.

EXACT FIX: (back) settlement_service/.../controllers/admin/monitor_controller.ex parse_time_window/1 line ~148-151: change the `true ->` (no params) default from `now - 3600` to a wider default (e.g. 24h = -86_400) so a bare GET is not clamped to 1h; and consider an "all"/no-window option. (front) stores/monitor.ts line 31 (and resetFilters line ~109): change `time_window: '1h'` default to '24h' (or add a '30d'/'all' option to the timeWindows list in OperationsMonitorView.vue lines 69-76 and the controller @valid_time_windows map lines 17-24) so historical homolog data is visible by default. The owner-facing symptom "Total 0 / Sucesso 0 / 0 operações" is purely the 1h clamp, not missing/mis-mapped data.

SECONDARY FINDINGS on this screen: 23 AI em-dash U+2014 occurrences (owner forbids); timeline tab fully hardcoded pt-BR (no i18n); message_type column is correctly labeled (no wrong-column bug here). No dead buttons found — all actions wire to real API calls.

- ROOT CAUSE (back). parse_time_window/1 default branch returns now-3600s (1h) when no time_window/from/to is sent, and build_query unconditionally applies `where x.created_at >= ^filters.from`. The ~1929 rows in monetarie_audit.xml_audit_logs were written (created_at = inserted_at = utc_now at write time) during earlier test bursts, all >1h old, so the 1h window matches 0 rows. stats.total, list_operations and categories ALL inherit this window => Total 0 / 0 operações. Same DB, same table, same @schema_prefix 'monetarie_audit' — not a wrong-table/prefix/DB bug.  
  `pix/backend/apps/settlement_service/lib/settlement_service_web/controllers/admin/monitor_controller.ex:148-151 (default 1h), 196 (where created_at >= from), 134-152 (parse_time_window), 17-24 (@valid_time_windows)` -> FIX: In parse_time_window/1 the `true ->` (no-params) branch, widen the implicit default from `DateTime.add(now,-3600,:second)` to e.g. -86_400 (24h) or -7*24*3600; and/or add an 'all'/no-lower-bound window so a bare GET is not clamped. This is the precise reason the screen shows 0 while 1929 rows exist.  (x1)
- ROOT CAUSE (front). Pinia store hard-defaults the filter to time_window:'1h' in BOTH the initial activeFilters and resetFilters(). On first load (and after Clear) the store sends time_window=1h, so even before the user touches anything the request is clamped to the last hour and returns empty — reproducing Total 0 client-side independently of the backend default.  
  `pix/frontend/admin/src/stores/monitor.ts:29-36 (initial activeFilters time_window:'1h'), 105-114 (resetFilters time_window:'1h')` -> FIX: Change default time_window from '1h' to '24h' (or add '30d'/'all'); apply same change in resetFilters. Pair with adding the wider option to timeWindows in the view and @valid_time_windows in the controller.  (x2)
- Time-window options too narrow: max selectable window is 7d. If homolog test data is older than 7 days, NO dropdown choice can surface it — user has no way to see historical audit rows from the UI. Backend @valid_time_windows also caps at 7d.  
  `pix/frontend/admin/src/views/monitor/OperationsMonitorView.vue:69-76 (timeWindows array, max '7d')` -> FIX: Add '30d' and/or 'all' entries to timeWindows in the view (lines 69-76), to @valid_time_windows in the controller (lines 17-24), and add an i18n label key operationsMonitor.timeWindows.30d/all. For 'all', backend must skip the created_at lower-bound.  (x1)
- AI em-dash U+2014 occurrences in the view — owner forbids '—'. 2 are in code comments (lines 26, 29); the rest are em-dash glyphs used as empty/null display fallbacks in computed fields, table cells and detail panel.  
  `pix/frontend/admin/src/views/monitor/OperationsMonitorView.vue:26, 29 (comments); 95, 98, 192, 193, 197, 204, 537, 541, 561, 663, 667, 671, 710, 714, 718, 722, 726, 730, 772, 776 (display fallbacks)` -> FIX: Replace the '—' fallback glyph with a hyphen '-' or an i18n placeholder (e.g. t('common.notAvailable')); rewrite the two comments without em-dash.  (x23)
- Timeline tab is entirely hardcoded pt-BR with no i18n. The tab label and all timeline UI strings bypass t()/useI18n while the rest of the view uses keys. There is no operationsMonitor.detail.tabs.timeline key in pt-BR.json (only summary/xml/headers exist).  
  `pix/frontend/admin/src/views/monitor/OperationsMonitorView.vue:103 (label:'Linha do tempo' hardcoded), 737 ('Linha do tempo da operação (ida e retorno)'), 741 ('Carregando linha do tempo…'), 744 ('Operação sem identificador de correlação…'), 748-749 ('Nenhuma mensagem correlacionada…'), 770 ('← Recebido'/'→ Enviado'), 786 ('após a 1ª mensagem')` -> FIX: Add operationsMonitor.detail.tabs.timeline and operationsMonitor.timeline.* keys to all 5 locale files and replace the hardcoded literals with t(). Confirmed missing: pt-BR.json operationsMonitor.detail.tabs has only {summary,xml,headers}.  (x7)
- NOT a bug (ruled out per the prompt's checklist): the 'Tipo de mensagem' column correctly renders op.message_type (i18n key fields.messageType='Tipo de mensagem') — it is not showing an instrument label where message_type belongs, nor vice versa. RBAC (RequirePermission SYSTEM:view) is not the cause: empty result is a 200 with [] (empty-state text 'Nenhuma operação encontrada…' renders), not a 403. No dead buttons: Apply/Clear/category tabs/pagination/detail tabs all call real store/API methods.  
  `pix/backend/apps/settlement_service/lib/settlement_service_web/controllers/admin/monitor_controller.ex:8 (RBAC plug), 305/541 (message_type mapping), 496-498 (empty-state render)` -> FIX: No change needed; documented to close out the prompt's checklist items.  (x1)
