# Desenhos dos 4 follow-ups do STA (2026-07-21)

Documento de design (design-first). Nenhum código é alterado aqui. A implementação de cada item vem depois, com TDD (RED antes de GREEN) e revisão por commit, seguindo a regra da casa: BACEN é a verdade, ache o NOSSO defeito e prove empiricamente.

Escopo: (a) token dedicado `STA_SERVICE_TOKEN`; (b) job de snapshot do saldo da cabine que não atualiza em PRD (CA-001/CA-002); (c) MetricsView do STA ainda em mock; (d) aviso de "static manifest" no boot do sta-api.

Convenções de ambiente e deploy: raiz `/CLAUDE.md`, `core/CLAUDE.md`, `sta/CLAUDE.md`. AWS conta `990933657879`, região `sa-east-1`, profile `vulcimonetarie`.

---

## (a) Token de serviço dedicado `STA_SERVICE_TOKEN`

### Problema (evidência arquivo:linha)

Hoje o gate fail-closed do escopo `/api/v1` do STA aceita o token de serviço m2m, mas com fallback para o segredo compartilhado `STA_WEBHOOK_SECRET` (guardian `mon_sta`). Os dois lados já resolvem o token com a MESMA precedência: `STA_SERVICE_TOKEN` primeiro, `STA_WEBHOOK_SECRET` na ausência.

Lado STA (a cabine que verifica o Bearer):
- `sta/backend/config/runtime.exs:48-49`
  ```elixir
  if service_token = System.get_env("STA_SERVICE_TOKEN") || System.get_env("STA_WEBHOOK_SECRET") do
    config :sta_connector, service_token: service_token
  end
  ```
- Plug que valida: `sta/backend/lib/sta_connector_web/plugs/service_auth.ex:43-52` (lê `Application.get_env(:sta_connector, :service_token)` em `configured_token/0`, linha 81) e compara em tempo constante com `Plug.Crypto.secure_compare` (linha 47), normalizando whitespace nos dois lados (`normalize/1`, linha 57).

Lado Core (o injetor do Bearer nas chamadas ao `/api/v1` do STA):
- `core/backend/config/runtime.exs:905`
  ```elixir
  sta_service_token: System.get_env("STA_SERVICE_TOKEN") || System.get_env("STA_WEBHOOK_SECRET")
  ```
- Injetor: `core/backend/lib/monetarie/sta/client/http.ex:126-140` (`auth_headers/0` lê `Application.get_env(:monetarie, :sta_service_token)`, normaliza whitespace com `String.replace(token, ~r/\s/, "")` e monta `{"authorization", "Bearer " <> clean}`). Usado em `post_files/1` (linha 115) e `fetch_status/1` (linha 78).

Ou seja: a precedência do token dedicado JÁ EXISTE no código dos dois lados. O que falta é PROVISIONAR o segredo dedicado e referenciá-lo nos task-defs, para que a rotação do token de serviço fique INDEPENDENTE da rotação do `STA_WEBHOOK_SECRET` (que também assina os callbacks HMAC de webhook, `sta/backend/config/runtime.exs:37-38`; rotacionar o webhook secret hoje força coordenar as duas funções ao mesmo tempo).

### Causa provável

Decisão de fechar o gate SEM introduzir segredo/IAM novo (memória `monetarie-sta-v1-auth-failclosed-0721`): a execution role é uma whitelist de ARNs EXATOS, então adicionar um secret novo exige alterar a role. Para não bloquear o fechamento do gate, caiu-se no `STA_WEBHOOK_SECRET`, que os dois lados já tinham provisionado com o MESMO ARN. O upgrade (token dedicado) ficou como follow-up justamente porque depende de um passo de IAM.

### Solução proposta

Sem código novo. Trabalho de provisionamento + IAM + task-def, nos DOIS ambientes (homolog e prod).

1. Criar o secret dedicado em cada ambiente:
   - Nome: `monetarie/homolog/auth/mon_sta/service_token` e `monetarie/prod/auth/mon_sta/service_token`.
   - KMS: MESMA CMK do domínio `mon_sta` já usada pelo `STA_WEBHOOK_SECRET` (não criar key nova).
   - Valor: token aleatório forte (por exemplo 48 bytes base64url SEM quebra de linha). Regra da casa: nunca gravar o valor em Markdown, `.env` ou log. O valor vive só no Secrets Manager.
   - Observação importante: o valor NÃO deve conter whitespace interno. O `STA_WEBHOOK_SECRET` sofria com `\n` de line-wrap base64 (por isso a normalização existe nos dois lados); gerar o token dedicado sem quebra de linha evita depender dessa normalização.

2. IAM (execution role): adicionar o ARN do novo secret à whitelist inline de `secretsmanager:GetSecretValue` das execution roles `monetarie-ecs-execution-prod` e o equivalente de homolog, para AS DUAS famílias de task que consomem o token: a do Core (`monetarie-core-api-*`) e a do STA (`monetarie-sta-api-*`). Sem esse passo, a task com o secret referenciado falha ao materializar o env e não sobe.
   - AVALIAR mas NÃO EXECUTAR nesta tarefa: preferir Terraform (fonte da verdade da infra) versus CLI cirúrgico. Regra da casa: HML nunca sofre `terraform apply` e drift de HML nunca vira problema (memória `monetarie-terraform-drift-regra`), então em HML o caminho natural é CLI cirúrgico no inline policy; em PROD, decidir entre Terraform (se a role está no IaC) ou CLI cirúrgico com registro. Qualquer alteração de IAM/infra é decisão do dono e NÃO deve ser feita neste passo de design.

3. Task-defs: adicionar o secret como `secrets` (nunca `environment`), com `name: STA_SERVICE_TOKEN` e `valueFrom: <arn do secret novo>`, em:
   - `monetarie-core-api-<env>` (o Core passa a mandar o token dedicado).
   - `monetarie-sta-api-<env>` (a cabine passa a exigir/aceitar o token dedicado).

4. Nada muda em `runtime.exs` nem no plug: a precedência `STA_SERVICE_TOKEN || STA_WEBHOOK_SECRET` já cobre a transição. Enquanto só o Core tiver o token dedicado, a cabine ainda aceita porque compara com o valor que ELA resolveu; por isso a ordem de deploy importa (ver abaixo).

### Arquivos a tocar

- Nenhum arquivo de código Elixir. A precedência já está em `sta/backend/config/runtime.exs`, `sta/backend/lib/sta_connector_web/plugs/service_auth.ex`, `core/backend/config/runtime.exs` e `core/backend/lib/monetarie/sta/client/http.ex`.
- Infra/provisionamento (fora do repo de aplicação, ou no módulo Terraform de secrets/roles se optar por IaC):
  - Secrets Manager: 2 secrets novos (homolog + prod).
  - Execution roles: inline policy das roles do Core e do STA.
  - Task-defs `monetarie-core-api-<env>` e `monetarie-sta-api-<env>`: bloco `secrets` com `STA_SERVICE_TOKEN`.

### Riscos

- Ordem de deploy errada quebra o gate: se o STA passar a resolver `STA_SERVICE_TOKEN` (token dedicado) ANTES de o Core mandar o mesmo valor, o Core (que ainda manda `STA_WEBHOOK_SECRET`) leva 401 em todo `/api/v1` (upload CADOC/CCS/AMES/SISBAJUD + fetch_status). Mitiga-se com a ordem Core-primeiro (ver deploy).
- Token com whitespace interno: se o valor gerado tiver `\n`, a normalização salva os dois lados hoje, mas é melhor gerar limpo. Provar o valor limpo antes de referenciar.
- IAM incompleto: se o ARN não entrar na whitelist da execution role da FAMÍLIA correta, a task não sobe (falha ao buscar o secret). Verificar as DUAS famílias (Core e STA).
- Divergência de valor entre ambientes: o secret do Core e o do STA no MESMO ambiente têm que carregar o MESMO valor (é um segredo compartilhado m2m). Um único secret por ambiente referenciado pelas duas task-defs elimina o risco de divergência (preferir isso a dois secrets iguais).

### Passos de deploy

Ordem Core-primeiro, STA-depois, sem migration, sem janela quebrada (a precedência garante retrocompatibilidade):

1. Criar os 2 secrets (homolog, prod) com valor limpo. Provar o valor materializado sem whitespace.
2. IAM: adicionar o ARN às execution roles do Core e do STA nos dois ambientes (decisão do dono; Terraform vs CLI conforme item 2 acima).
3. Deploy do Core com a task-def referenciando `STA_SERVICE_TOKEN`. O Core passa a mandar o token dedicado. A cabine antiga ainda aceita porque ela resolve o mesmo valor (o Core manda o dedicado; a cabine, sem o env dedicado ainda, resolve o webhook secret; o gate compara e... aqui está o ponto de atenção). Para NÃO depender de coincidência, o correto é: no MESMO passo, garantir que o valor do secret dedicado seja IGUAL nos dois lados desde já, OU manter o Core mandando o webhook secret até a cabine ganhar o dedicado. A forma mais segura: referenciar o MESMO secret dedicado nas duas task-defs e deployar Core primeiro; enquanto a cabine não tem o env, ela cai no webhook secret e o Core cai no dedicado, e aí os valores DIVERGEM. Portanto a recomendação firme é o passo 4 imediatamente após o 3, na mesma janela.
4. Deploy do STA com a task-def referenciando `STA_SERVICE_TOKEN`. A partir daqui os dois lados resolvem o dedicado e ficam idênticos. Rotação futura do token de serviço passa a ser independente do webhook secret.
5. Prova viva (rede real, de dentro do `core-api`, via rpc, espelhando a memória `monetarie-sta-v1-auth-failclosed-0721`): `Monetarie.Sta.Client.Http.fetch_status/1` para um id qualquer deve responder 200/404 (não 401), confirmando que o Bearer dedicado é aceito. Um request SEM token continua 401. Fazer nos 2 ambientes.

Rollback: reverter as task-defs para a revisão anterior (sem o `STA_SERVICE_TOKEN`); a precedência volta a cair no `STA_WEBHOOK_SECRET` que segue provisionado. O secret novo e o ARN na role podem ficar (inertes) ou serem removidos depois.

---

## (b) Snapshot do saldo da cabine que não atualiza em PRD (CA-001/CA-002)

### Problema (evidência arquivo:linha)

A tela de Liquidação/Tesouraria abre com "Source: unavailable" (ou snapshot preso) e só fica fresca depois do clique manual em "Atualizar agora"/"Reconciliar agora". A causa é estrutural: NÃO EXISTE job periódico que produza o snapshot; o único caminho que faz round-trip à cabine e grava snapshot é o POST manual.

- O GET que alimenta a tela é READ-ONLY por design e devolve 404 quando não há snapshot:
  - `core/backend/lib/monetarie_web/controllers/admin/coreproviders_parity_controller.ex:1362-1372` (`treasury_reconciliation/2`): se `CabinReconciliation.latest_summary() == nil`, responde 404 "Nenhuma reconciliação executada ainda." O comentário 1356-1361 é explícito: o auto-poll do front (a cada 5 min por aba) é read-only e NÃO amplifica escrita; a execução real só ocorre no POST.
  - `core/backend/lib/monetarie_web/controllers/admin/coreproviders_parity_controller.ex:1377-1385` (`settlement_balance_history/2`): lê `CabinReconciliation.history(...)`, ou seja, os snapshots já gravados. Sem snapshot, histórico vazio.
- O ÚNICO caminho que grava snapshot:
  - `core/backend/lib/monetarie_web/controllers/admin/coreproviders_parity_controller.ex:1389-1395` (`refresh_settlement_balance/2`): chama `CabinReconciliation.run_and_record(...)`. Rota POST em `core/backend/lib/monetarie_web/router.ex:2996-2998` (`/treasury/settlement-balance/refresh`).
- `run_and_record/1` (a produção do snapshot):
  - `core/backend/lib/monetarie/use_cases/treasury/cabin_reconciliation.ex:69-94`: faz o round-trip (`cabin_side/2` chama `CabinTreasury.settlement_balance/0`, `core/backend/lib/monetarie/services/pix_providers/in_house/cabin_treasury.ex:71-79`) e grava com `record_snapshot!` na tabela `treasury_reconciliation_snapshots`.
- Prova de que NÃO há chamador agendado: `grep run_and_record` retorna SÓ o controller manual; não há entrada no crontab de `core/backend/config/config.exs` (bloco Oban.Plugins.Cron ~linhas 90-205) nem no espelho de `core/backend/config/runtime.exs:602-` para treasury/reconciliation/settlement.

Fonte única confirmada: `settlement_balance_history` e `treasury_reconciliation` leem do MESMO acervo de snapshots (`ReconciliationSnapshot`), então um único job periódico refresca ao mesmo tempo a tela de Liquidação (CA-001/CA-002) e a de Reconciliação de Tesouraria (CA-011/CA-012).

### Causa provável

O GET foi deliberadamente tornado read-only (evitar que o auto-poll do front amplificasse carga/escrita na cabine), com a intenção de que a produção do snapshot ocorresse por um gatilho separado. Esse gatilho separado ficou SÓ no botão manual: nunca foi adicionado o cron que roda `run_and_record` periodicamente. Em PRD, sem clique, `latest_summary()` fica `nil`, o GET responde 404 e a tela mostra "unavailable". O snapshot só é fresco logo após um clique manual.

### Solução proposta

Adicionar um Oban worker de cron que chama `CabinReconciliation.run_and_record/1` numa cadência fixa, deixando o snapshot fresco sem clique. `run_and_record` já é seguro para rodar periodicamente:

- É READ-ONLY money-path (moduledoc em `cabin_reconciliation.ex:6-9`): reporta diferenças, NUNCA escreve no TigerBeetle, nunca ajusta saldo; a única escrita é o snapshot de observabilidade.
- É fail-closed (moduledoc `cabin_reconciliation.ex:28-33`): qualquer falha da cabine (config/auth/HTTP/timeout/janela grande) produz `status: "unavailable"` com o erro exposto, e AINDA ASSIM grava um snapshot. Ou seja, o job não deixa a tela sem dado; ele grava um snapshot honesto de indisponibilidade.
- Retenção já é podada inline (retention 90 dias, `cabin_reconciliation.ex:57-58`), então o cron não faz a tabela crescer sem limite.

Desenho do worker:

- Novo módulo `Monetarie.Workers.Treasury.SettlementSnapshotWorker`, `use Oban.Worker, queue: :default, max_attempts: 1` (mesmo padrão de `Monetarie.Workers.Reconciliation.TbPgReconciliationJob`, `core/backend/lib/monetarie/workers/reconciliation/tb_pg_reconciliation_job.ex:21`, e de `StuckOutboundChecker`, `.../stuck_outbound_checker.ex:47`). `max_attempts: 1` porque o próximo tick é a retentativa natural (mesmo racional dos pollers de STA no crontab).
- `perform/1` chama `CabinReconciliation.run_and_record(window_days: 1)` (janela default do módulo). NÃO envolver em `rescue` que engula erro: se `run_and_record` lançar (não deveria, é fail-closed), o Oban registra a falha de forma visível (o oposto do erro silencioso). O worker retorna `:ok` no caminho normal (inclusive quando o payload vem `status: "unavailable"`, porque isso É o resultado correto e já está persistido como snapshot).
- Cadência: `*/15 * * * *` (a cada 15 min), alinhada às demais reconciliações periódicas (`TbPgReconciliationJob`, `PixStatusReconciliation`, `SpbStatusReconciliation`, todas `*/15`). 15 min mantém a tela fresca sem martelar a cabine. Ajustável.
- Idempotência/anti-empilhamento: janela `unique` do Oban (por exemplo `unique: [period: 60, states: [:available, :scheduled, :executing]]`) para o tick não empilhar se um run anterior ainda estiver em voo (round-trip lento à cabine).

Registrar o cron nos DOIS lugares (config.exs E runtime.exs) por causa da paridade obrigatória:
- `core/backend/config/config.exs` (bloco Oban.Plugins.Cron, ~linha 90-205).
- `core/backend/config/runtime.exs` (bloco espelho, `crontab:` a partir de ~linha 602), guardado por `test/monetarie/oban_cron_parity_test.exs`.

Garantir cobertura da queue: o worker usa `:default`, que já está no `queues:` de `core/backend/config/config.exs:248`. Ainda assim, rodar `test/monetarie/oban_queue_coverage_test.exs` para provar que a queue do worker novo está registrada no runtime.exs (essa é a classe de falha que o teste trava: worker com queue ausente do runtime.exs empilha job preso em silêncio, exatamente o padrão do gotcha da fila `:pix_out_retry`). Como é `:default`, deve passar sem mudança de queues.

### Overlap com o item de "fail-proof" (erro silencioso)

Apontado explicitamente: um snapshot com `status: "unavailable"` é gravado e a tela mostra "unavailable", mas HOJE ninguém é alertado quando a cabine fica indisponível repetidamente. Com o cron ligado, isso vira uma sequência de snapshots "unavailable" a cada 15 min sem sinal para o operador. O tratamento fail-proof (a desenhar no item próprio) deve: detectar N snapshots "unavailable" consecutivos e disparar `Monetarie.Alerts.Operational` (o mesmo mecanismo usado por `StuckOutboundChecker`/`DlqMonitor`, memória `monetarie-arco-c-r6-ports5-8-0716`). O cron deste item (b) é a PRÉ-CONDIÇÃO para esse alerta existir (sem execução periódica, não há sinal contínuo). Manter os dois desenhos coordenados: aqui a gente liga a cadência; no fail-proof a gente liga o alarme sobre o resultado.

### Arquivos a tocar

- Novo: `core/backend/lib/monetarie/workers/treasury/settlement_snapshot_worker.ex`.
- `core/backend/config/config.exs` (adicionar a entrada de cron no bloco Oban.Plugins.Cron).
- `core/backend/config/runtime.exs` (adicionar a MESMA entrada no crontab espelho, por causa do `ObanCronParityTest`).
- Testes (TDD): novo `test/monetarie/workers/treasury/settlement_snapshot_worker_test.exs` (perform grava snapshot; caminho "unavailable" também grava e retorna :ok; idempotência/unique). Rodar `oban_cron_parity_test.exs` e `oban_queue_coverage_test.exs` como gates.
- Nenhuma migration (a tabela `treasury_reconciliation_snapshots` e o schema `ReconciliationSnapshot` já existem).

### Riscos

- Carga na cabine: um round-trip a cada 15 min por ambiente. Baixo (é 1 GET de saldo + listagem paginada de settled na janela de 1 dia). A janela default de 1 dia limita o volume; `@max_pages`/`@max_window_days` já protegem contra janela grande (`cabin_treasury.ex:44`, `cabin_reconciliation.ex:51-52`).
- Paridade cron/queue: esquecer o espelho no runtime.exs quebra o `ObanCronParityTest`; esquecer a cobertura de queue empilha job preso. Os dois testes citados são os gates.
- Sobreposição de runs (round-trip lento): mitigar com `unique` no worker para não empilhar.
- Ligar o cron sem o alerta do fail-proof deixa a indisponibilidade repetida silenciosa (só na tela). Aceitável como incremento, mas registrar o follow-up de alerta (overlap acima) para não ficar meia-fechado.

### Passos de deploy

1. Implementar worker + cron (config.exs + runtime.exs) + testes, com gates de paridade verdes.
2. Deploy do `core-api` (HML primeiro, PROD depois com OK do dono). Sem migration.
3. Prova viva: após o primeiro tick (até 15 min), `GET /treasury/reconciliation` responde 200 (não mais 404) e `GET /treasury/settlement-balance/history` traz linha nova; a tela de Liquidação mostra o saldo fresco com `source: pix_cabin` SEM clique. Fazer nos 2 ambientes. Lembrete canônico: STA e cabine só operam contra o BACEN em PROD; mas o saldo de liquidação da cabine PIX é lido em ambos (o teste vale nos dois, com o dado que cada ambiente tiver).
4. Observabilidade: acompanhar a fila `default` e o log do worker por alguns ciclos para confirmar que não há erro engolido nem job preso.

Rollback: remover as duas entradas de cron (config.exs + runtime.exs) e redeployar; o botão manual segue funcionando como antes.

---

## (c) MetricsView do STA ainda em mock

### Problema (evidência arquivo:linha)

A `MetricsView` do frontend oficial do STA renderiza dados de MOCK (mesma classe da tela de Logs que já foi para dado real). Há duas evidências independentes: um fallback de mock no próprio componente E um desencontro de contrato entre o que a view consome e o que o canal do backend entrega.

Fallback de mock no componente:
- `sta/frontend/src/views/MetricsView.vue:123-222` (`loadMockData`): monta `rates`, `latencies`, `errors`, `system`, `counters` e 60 pontos de histórico sintético com `Math.random()`/`Math.sin()`.
- `sta/frontend/src/views/MetricsView.vue:83-88`: no `onError` do canal, chama `loadMockData()`.
- `sta/frontend/src/views/MetricsView.vue:305-311`: `setTimeout(..., 3000)` chama `loadMockData()` se ainda estiver `loading` após 3 s.

Desencontro de contrato (o motivo de fundo, mais grave que o fallback):
- A view define `MetricsData` com as chaves `rates`, `latencies` (com `sta_request`/`route_delivery`/`file_processing`/`inbound_poll`, cada uma com p50/p95/p99), `errors` (`error_rate_percent`), `system` (com `run_queue`, `io`, `uptime_formatted`) e `counters`: `sta/frontend/src/views/MetricsView.vue:17-52`. E `HistoryData` com `minute`/`hour`/`day`: linhas 54-58. A view se conecta ao canal `metrics:dashboard`: linha 72.
- O canal `metrics:dashboard` do backend entrega um payload com chaves COMPLETAMENTE DIFERENTES: `inbound`, `outbound`, `rate_limit`, `poller`, `activity_data`, `system_stats`, `timestamp`: `sta/backend/lib/sta_connector_web/channels/metrics_channel.ex:229-239` (`collect_dashboard_metrics/0`). Nenhuma das chaves que a view lê (`rates`, `latencies`, `errors`, `system`, `counters`) existe nesse payload.
- Resultado prático: mesmo quando o canal conecta e faz `push "metrics_snapshot"` (`metrics_channel.ex:104-109`), a view seta `currentMetrics` com um objeto cujas chaves ela não usa; os componentes filhos (`MetricsOverview`, `ThroughputChart`, `LatencyChart`, `SystemResourceChart`) recebem `undefined` em todas as props (`rates`/`latencies`/`system`/`counters`) e ficam VAZIOS. A única forma de a tela mostrar algo com forma é o `loadMockData()`.
- Pior: a view faz `push('get_history', ...)` (`MetricsView.vue:235-245`), mas o canal NÃO tem handler para `get_history` (só `get_metrics`, `get_system_stats`, `trigger_poll`, `set_interval`: `metrics_channel.ex:133-178`) nem honra `include_history` no join (`metrics_channel.ex:79-101` ignora o payload). O seletor de período (Última hora/24h/30 dias) fica sem dado.

Existe backend REAL com a forma certa, porém DESCONECTADO da view:
- `StaConnector.Metrics.Aggregator` produz EXATAMENTE o contrato da view: chaves de rate `files_per_minute`/`bytes_per_second`/`inbound_per_minute`/`outbound_per_minute`/`sta_requests_per_minute`/`errors_per_minute` (`sta/backend/lib/sta_connector/metrics/aggregator.ex:389`), tipos de latência `sta_request`/`route_delivery`/`file_processing`/`inbound_poll` (linha 403), `error_rate_percent` (linha 458), e histórico em minute/hour/day (`get_minute_data`/`get_hourly_data`/`get_daily_data`, `get_latest`, `get_history`: linhas 67-137). O Aggregator (e o Collector, e o Metrics GenServer) estão de fato no supervisor: `sta/backend/lib/sta_connector/application.ex:52-54`.
- Ou seja: os dados reais que a view quer JÁ SÃO coletados; ninguém os serve para a view. O canal a que a view se conecta serve outra coisa.

### Causa provável

Duas trilhas de métricas evoluíram em paralelo e nunca foram reconciliadas: o `MetricsChannel` (voltado a inbound/outbound/rate_limit/poller, dado de negócio a partir do DB) e o `Metrics.Aggregator` (voltado a rates/latencies/errors/system, telemetria em ETS com histórico). A `MetricsView` foi escrita contra o contrato do Aggregator, mas ligada ao canal errado, e ganhou um `loadMockData()` como muleta para não ficar em branco. A tela de Logs já foi migrada para dado real (memória `monetarie-deploy-stavue-sisbajud-ccs-0720`); a de Metrics ficou para trás.

### Solução proposta

Ligar a view ao Aggregator (a fonte real que já tem a forma certa) e remover o mock. Caminho de menor atrito, porque a view já fala o protocolo de canal (eventos `metrics_snapshot`/`metrics_update`/`metrics_history`, pushes `get_metrics`/`get_history`): fazer o `MetricsChannel` servir o Aggregator, em vez de reescrever a view.

Backend (`sta/backend/lib/sta_connector_web/channels/metrics_channel.ex`):
1. Trocar a fonte do snapshot/update para o Aggregator: `collect_dashboard_metrics/0` passa a devolver `StaConnector.Metrics.Aggregator.get_latest()` (a forma que a view consome). Manter fail-closed: se `get_latest()` vier `nil` (sem snapshot ainda), devolver um snapshot vazio bem-tipado (rates/latencies/errors/system/counters com zeros) em vez de `nil`, para a view renderizar zeros honestos e não cair no mock.
2. Honrar `include_history` no join (`do_join`, linhas 79-101): quando `payload["include_history"]`, após o `after_join` fazer `push "metrics_history"` com `%{minute: Aggregator.get_minute_data(...), hour: Aggregator.get_hourly_data(...), day: Aggregator.get_daily_data(...)}` (o formato de `HistoryData` da view, `MetricsView.vue:54-58, 113-115`).
3. Adicionar `handle_in("get_history", %{"period" => p, "count" => n}, socket)` devolvendo `%{period: p, count: n, data: Aggregator.get_history(period: ..., count: ...)}` (o shape que `requestHistory` espera, `MetricsView.vue:235-245`).
4. `get_metrics` (linha 133) passa a devolver `Aggregator.get_latest()` (mesmo shape do snapshot).
5. As séries do canal que ainda forem úteis (poller/rate_limit/inbound/outbound de negócio) podem virar um tópico/campo separado se a tela quiser exibi-las depois; para fechar este item, o foco é entregar o contrato que a view JÁ consome.

Frontend (`sta/frontend/src/views/MetricsView.vue`):
1. Remover `loadMockData()` (123-222) e as duas chamadas a ele (83-88 no onError e 305-311 no setTimeout).
2. No `onError`, em vez de mock, mostrar um estado honesto de "métricas indisponíveis" (mesma disciplina da regra da casa: não fabricar dado; erro é recusa visível, não mock).
3. Sanidade: conferir se `sta/frontend/src/stores/metrics.ts` e `sta/frontend/src/composables/useMetrics.ts` são fontes paralelas usadas ou código morto; se a view usa só `useChannel`/`useSocket`, alinhar ou remover o que estiver órfão (evitar duas verdades).

Alternativa considerada e descartada: servir por REST em `/api/admin/metrics`. Esse endpoint devolve `StaConnector.Metrics.get_all()` (`sta/backend/lib/sta_connector_web/controllers/api/admin/metrics_controller.ex:74-79`), cuja forma NÃO bate com a `MetricsData` da view (é `system`/`sta`/`files`/`by_system`/`latencies`, outro contrato). Reaproveitar o canal + Aggregator é mais limpo e mantém o streaming a cada 15 s que a view já espera.

### Arquivos a tocar

- Backend: `sta/backend/lib/sta_connector_web/channels/metrics_channel.ex` (fonte = Aggregator; `include_history`; handler `get_history`; snapshot vazio tipado quando nil).
- Frontend: `sta/frontend/src/views/MetricsView.vue` (remover mock + fallback honesto). Revisar `sta/frontend/src/stores/metrics.ts` e `sta/frontend/src/composables/useMetrics.ts`.
- Testes: canal (`test` do sta backend) provando shape do snapshot/update = contrato da view, `get_history` respondendo, e `include_history` empurrando histórico. Frontend: teste de que a ausência de dado mostra estado honesto (sem mock). `vue-tsc` deve continuar em 0 erros (o contrato TS `MetricsData` já existe; alinhar o backend a ele).

### Riscos

- Aggregator sem histórico logo após um restart do sta-api: `get_latest()`/`get_minute_data` podem vir vazios até o primeiro snapshot (o Aggregator tira snapshot periódico). O snapshot vazio bem-tipado (item 1) evita a tela quebrar; a tela mostra zeros até o primeiro ciclo. Documentar isso para não confundir com "quebrado".
- Regressão de quem consumia o payload antigo do canal (inbound/outbound/poller): confirmar que só a `MetricsView` consome `metrics:dashboard`. Se o Dashboard também consome, preservar as séries de negócio num campo/tópico à parte para não quebrar a outra tela.
- Canônico STA: a cabine só tem tráfego real de BACEN em PROD; em HML as métricas de negócio podem ser magras. Isso é esperado e não é defeito. A prova de que "não é mais mock" é o dado real (ainda que pequeno) fluindo, não números bonitos.

### Passos de deploy

Deploy conjunto (contrato compartilhado): sta-api (canal) e sta-admin-ui (frontend Vue, build de `sta/frontend/`).
1. Implementar backend + frontend com testes; `vue-tsc` 0 erros; suíte do canal verde.
2. Deploy `sta-api` (canal servindo Aggregator) e depois `sta-admin-ui` (view sem mock). Sem migration.
3. Prova viva (regra #11, screenshot): abrir a MetricsView em HML e em PROD, confirmar que os valores vêm do Aggregator (bater com `/api/admin/metrics` ou com `Aggregator.get_latest()` via rpc), que o seletor de período traz histórico real, e que derrubar o socket mostra "indisponível" em vez de mock. Capturar screenshot + metadados; não declarar tela validada sem isso.

Rollback: reverter as revisões de sta-api e sta-admin-ui. A view volta a ter mock (estado atual), o que é o comportamento anterior conhecido.

---

## (d) Aviso de "static manifest" no boot do sta-api

### Problema (evidência arquivo:linha)

O sta-api é uma API sem assets estáticos (o frontend Vue é servido por um nginx separado, `sta/frontend`), mas no boot o Phoenix loga um aviso de manifesto de estáticos ausente. Isso ocorre porque o endpoint declara `cache_static_manifest` apontando para um arquivo que não é gerado no build.

- `sta/backend/config/prod.exs:8-9`:
  ```elixir
  config :sta_connector, StaConnectorWeb.Endpoint,
    cache_static_manifest: "priv/static/cache_manifest.json"
  ```
- O arquivo NÃO existe no build: `sta/backend/priv/static/cache_manifest.json` está AUSENTE (só há `assets/`, `index.html`, favicons, `vite.svg`), e o `sta/backend/Dockerfile` NÃO roda `mix assets.deploy`/`mix phx.digest` (nenhum passo de assets). Sem o manifesto, o `Phoenix.Endpoint` emite no boot o aviso "Could not find static manifest at ...".
- O endpoint ainda monta `Plug.Static` (`sta/backend/lib/sta_connector_web/endpoint.ex:26-30`) com `static_paths/0 = ~w(assets fonts images favicon.ico robots.txt)` (`sta/backend/lib/sta_connector_web.ex:20`), resquício de quando o backend servia a SPA.
- Este é o MESMO gotcha já resolvido no Core: `core/CLAUDE.md` gotcha #6 diz literalmente "Phoenix `cache_static_manifest`: Remover de `prod.exs` para apps API-only (sem assets estáticos)". O Core removeu; o STA ficou para trás.

### Causa provável

Config herdada do gerador do Phoenix (que assume app com assets) que nunca foi limpa no STA. O aviso é cosmético (não afeta o `/api/v1` nem os callbacks), mas polui o boot e o próprio Phoenix recomenda no texto do aviso rodar `mix phx.digest` OU remover a configuração. Como o sta-api é API-only, a remoção é o caminho certo.

### Solução proposta

Fix mínimo (espelha a resolução do Core, menor risco):
- Remover as duas linhas de `cache_static_manifest` de `sta/backend/config/prod.exs:8-9`. Isso elimina o aviso no boot. `Plug.Static` sem manifesto continua funcionando (serve arquivos sem digest), então nada mais quebra.

Limpeza completa (opcional, maior alcance, deixar explícito o "API-only"):
- Além de remover o `cache_static_manifest`, remover o `plug Plug.Static` de `sta/backend/lib/sta_connector_web/endpoint.ex:26-30` e enxugar `static_paths/0`/`statics` em `sta/backend/lib/sta_connector_web.ex:20,54`, já que a API não serve estáticos. Opcionalmente remover o `priv/static/` do backend (assets órfãos).
- Tradeoff: a limpeza completa toca mais superfície (endpoint + web.ex) e exige provar que nenhuma rota depende de `Plug.Static` (favicon.ico/robots.txt são inofensivos, mas confirmar). Por isso a recomendação é entregar o fix MÍNIMO como principal (resolve o aviso, risco baixo) e tratar a limpeza completa como incremento separado, se o dono quiser.

Recomendação: fix mínimo agora; limpeza completa como follow-up de higiene.

### Arquivos a tocar

- Fix mínimo: `sta/backend/config/prod.exs` (remover as 2 linhas de `cache_static_manifest`).
- Limpeza completa (opcional): `sta/backend/lib/sta_connector_web/endpoint.ex` (remover `Plug.Static`), `sta/backend/lib/sta_connector_web.ex` (`static_paths`/`statics`), e possivelmente `sta/backend/priv/static/` (remover assets órfãos).

### Riscos

- Se, ao contrário do esperado, alguma coisa em PROD depende de um estático servido pelo backend (por exemplo um favicon acessado direto no host da API), a limpeza completa poderia deixar 404. O fix mínimo NÃO tem esse risco (mantém o `Plug.Static`); por isso é o principal.
- Nenhum risco de money-path (é config de boot de app API-only).

### Passos de deploy

1. Aplicar o fix mínimo (remover `cache_static_manifest` do prod.exs). Sem migration.
2. Deploy `sta-api` (HML primeiro, PROD depois com OK do dono).
3. Prova viva: acompanhar o log de boot do sta-api (CloudWatch) e confirmar que o aviso "Could not find static manifest" NÃO aparece mais, e que `/health` segue 200 e o `/api/v1` segue fail-closed (401 sem token, 200/404 com token). Fazer nos 2 ambientes.

Rollback: reverter a task-def para a revisão anterior; o aviso volta (inócuo).

---

## Resumo dos 4 desenhos

- (a) Token dedicado `STA_SERVICE_TOKEN`: a precedência `STA_SERVICE_TOKEN || STA_WEBHOOK_SECRET` JÁ EXISTE nos dois lados (`sta/backend/config/runtime.exs:48`, `sta/backend/lib/sta_connector_web/plugs/service_auth.ex:81`, `core/backend/config/runtime.exs:905`, `core/backend/lib/monetarie/sta/client/http.ex:127`). Falta só provisionar um secret dedicado por ambiente (mesma KMS do mon_sta), adicionar o ARN à execution role do Core e do STA (passo de IAM, avaliar Terraform vs CLI, NÃO executar no design) e referenciar `STA_SERVICE_TOKEN` nas duas task-defs. Deploy Core-primeiro/STA-depois na mesma janela; sem código, sem migration. Ganho: rotação do token de serviço independente do webhook secret.

- (b) Snapshot do saldo da cabine (CA-001/CA-002): `CabinReconciliation.run_and_record/1` (`core/backend/lib/monetarie/use_cases/treasury/cabin_reconciliation.ex:69`) só é chamado pelo POST manual "Atualizar agora"; NÃO há cron. O GET é read-only e responde 404 sem snapshot (`coreproviders_parity_controller.ex:1362`), por isso a tela abre "unavailable". Fix: novo `Monetarie.Workers.Treasury.SettlementSnapshotWorker` (queue `:default`, `*/15`) chamando `run_and_record`, registrado em config.exs E runtime.exs (ObanCronParityTest) com cobertura de queue (ObanQueueCoverageTest). `run_and_record` já é read-only e fail-closed, seguro para rodar periódico. Overlap com o fail-proof: com o cron, snapshots "unavailable" repetidos devem disparar `Alerts.Operational` (desenho do item fail-proof). Sem migration.

- (c) MetricsView em mock: a view consome o contrato do Aggregator (rates/latencies/errors/system/counters + histórico minute/hour/day, `MetricsView.vue:17-58`), mas se conecta ao canal `metrics:dashboard` que entrega OUTRA forma (inbound/outbound/rate_limit/poller, `metrics_channel.ex:229`), então cai no `loadMockData()` (`MetricsView.vue:123`). O Aggregator real com a forma certa existe e está no supervisor (`aggregator.ex:389,403,458`; `application.ex:52-54`) mas está desconectado. Fix: `MetricsChannel` passa a servir `Aggregator.get_latest()`/`get_history` (+ honrar `include_history` e handler `get_history`); remover o mock do frontend e mostrar estado honesto de indisponibilidade. Deploy conjunto sta-api + sta-admin-ui; sem migration; prova por screenshot (regra #11).

- (d) Aviso "static manifest" no boot: `sta/backend/config/prod.exs:8-9` declara `cache_static_manifest` para um arquivo que o build não gera (`priv/static/cache_manifest.json` ausente; Dockerfile sem `phx.digest`), então o Phoenix loga o aviso no boot. É o mesmo gotcha #6 já resolvido no Core. Fix mínimo: remover as 2 linhas do prod.exs (API-only, `Plug.Static` segue intacto). Limpeza completa opcional (remover `Plug.Static` + `static_paths`) como follow-up de higiene. Sem migration.

Documento salvo em: `/Users/luizpenha/monetarie/docs/plans/2026-07-21-desenhos-sta-followups.md`
