Configuração — env vars e toggles¶
Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-09-15
Toda a configuração é por variável de ambiente (no Coolify, nas envs do recurso). Não há
arquivo de configuração em produção; o .env.example na raiz do repo documenta o conjunto.
Banco de dados¶
| Variável | Obrigatória | Uso |
|---|---|---|
DATASOURCES_DEFAULT_URL |
Sim | JDBC do Postgres 18 (ex.: jdbc:postgresql://host:5432/maxsul_pied). |
DATASOURCES_DEFAULT_USERNAME |
Sim | Usuário do banco. |
DATASOURCES_DEFAULT_PASSWORD |
Sim | Senha do banco. |
As migrações (Flyway) rodam automaticamente no start — a V1__landing_pied.sql cria as três
tabelas landing. Requer Postgres 18+ (uuidv7() nativo).
PIED¶
| Variável | Default | Uso |
|---|---|---|
PIED_BASE_URL |
https://backend-pied-prod.piedadmin.com.br/api |
Base da API REST da PIED. |
PIED_TOKEN |
(vazio) | Token Bearer fornecido pela PIED. Sem ele, o poll loga aviso e não roda. |
PIED_WEBHOOK_SECRET |
(vazio) | Segredo compartilhado do webhook (header X-Pied-Secret). Vazio = aceita sem validar (MVP). |
Toggles¶
| Variável | Default | O que liga |
|---|---|---|
PIED_WEBHOOK_HABILITADO |
true |
Recepção do POST /webhook/pied. Desligado → 503. |
PIED_RETENCAO_HABILITADO |
true |
Job de limpeza do landing raw (pied_rest/pied_webhook). |
PIED_RETENCAO_DIAS |
7 |
Janela de retenção do raw — linhas mais antigas são apagadas. O dado útil vive nas tabelas normalizadas (guardam o próprio payload). |
PIED_RETENCAO_INTERVALO |
24h |
Intervalo do job de retenção (Duration). |
PIED_WATCHDOG_HABILITADO |
true |
Vigia o silêncio do webhook: alerta no GlitchTip por duas réguas, a que vier primeiro. Modo de falha: a PIED desativa o webhook após uma falha de entrega (404 ou timeout) e não volta sozinha — ver recadastro do webhook, decisão 0016 e 0023. |
PIED_WATCHDOG_LIMITE_MIN_COMERCIAL |
45 |
Régua rápida: minutos de silêncio dentro do comercial (seg–sex, faixa abaixo) antes de alertar. Calibrado no histórico real — o maior silêncio legítimo medido foi 37 min. 0 desliga só esta régua. |
PIED_WATCHDOG_HORA_INICIO |
8 |
Abertura do comercial da Maxsul (hora local, 0–23). |
PIED_WATCHDOG_HORA_FIM |
18 |
Fechamento do comercial (hora local, 1–24). |
PIED_WATCHDOG_LIMITE_HORAS |
24 |
Rede de fora do horário: limite de horas úteis (dia útil inteiro, seg–sex; sáb/dom não contam) sem webhook antes de alertar. |
PIED_WATCHDOG_INTERVALO |
5m |
Intervalo do job de verificação (Duration). Tem que ser bem menor que a régua comercial. |
PIED_GATE_PRIMEIRA_CAPTURA_DESDE |
(vazio) | Resgate da 1ª captura já paga (decisão 0023): data de ativação AAAA-MM-DD. Pedido visto já pago na primeira captura — webhook perdido num apagão, poll REST o alcança depois de pago — vai direto para NA_FILA se foi criado depois desta data. Vazio desliga (default): só o Enfileirar do console resgata. A data é o piso que impede o deploy de despejar legado. |
PIED_GATE_PRIMEIRA_CAPTURA_JANELA_DIAS |
7 |
Idade máxima do orderCreated para o pedido contar como novo. Cobre apagão que atravessa o fim de semana sem alcançar o backlog antigo da PIED. |
PIED_POLL_HABILITADO |
false |
Job agendado de poll REST (3 entidades → pied_rest). |
PIED_POLL_INTERVALO |
6h |
Intervalo entre rodadas do poll (Duration: 6h, 1h, 30m...). Produtos/clientes re-varrem tudo a cada rodada (sem cursor incremental) e o landing (pied_rest) é append-only sem dedup — intervalo curto incha o raw; 6h dá 4 backstops/dia (o webhook cobre o tempo-real). |
PIED_POLL_LIMITE_PAGINA |
50 |
Tamanho de página nas chamadas REST (máximo aceito pela PIED: 50). |
PIED_POLL_ENRIQUECIMENTO |
true |
Varre produtos/clientes na rodada periódica (re-varre tudo — sem cursor incremental). Clientes/produtos chegam por webhook (order.* + company.* que traz InscEst); com false, o poll periódico fica só nos pedidos (backstop) e para de inflar o landing. O backfill sob demanda ("Sincronizar clientes") segue funcionando. |
PIED_INTEGRACAO_HABILITADA |
false |
Fase 2 (transform → push ao Integrador). Ligado: o TransformJob normaliza, aplica o gate e (em PRODUCAO) envia; a reconciliação do retorno roda. Desligado: só captura raw. |
Fase 2 — transform + push ao Integrador¶
| Variável | Default | Uso |
|---|---|---|
PIED_INTEGRACAO_MODO |
PRODUCAO |
PRODUCAO envia automático assim que o pedido fica elegível; STAGING deixa NA_FILA até o clique no /console (decisão 0004). |
PIED_INTEGRACAO_INTERVALO |
5m |
Intervalo do job de normalização+gate (+push em PRODUCAO). Duration. |
PIED_INTEGRACAO_RECONC_INTERVALO |
5m |
Intervalo do job de reconciliação do retorno do X-Adm. Duration. |
MENSAGERIA_RELAY_ENABLED |
true |
Agendador do relay da lib (xadm-mensageria), que drena a fila de saída a cada 2 min. Desligar para a entrega ao Integrador (a fila acumula; nada se perde). Nasceu false enquanto a lib não elegia réplica e um job local fazia a guarda — histórico na decisão 0014. |
MENSAGERIA_RELAY_CLUSTER_LOCK |
true |
Eleição de réplica por tick (pg_try_advisory_lock), desde a lib 0.3.4. Desligar devolve a entrega dobrada sob as duas instâncias (jar + native) — medido, 92 PUT para 52 payloads distintos em 24h. Escotilha de diagnóstico; nunca em produção. |
INTEGRADOR_BASE_URL |
(vazio) | Base do Integrador (ex.: https://integrador.xadm.biz). Vazio com integração habilitada: log WARN no startup e cada envio/reconciliação vira ERRO tratado ("Integrador não configurado") — não crash. Configure para operar. |
INTEGRADOR_API_TOKEN |
(vazio) | Bearer estático do Integrador (Authorization: Bearer …). |
Cada rodada do job começa pelo enriquecimento sob demanda: havendo pedido NA_FILA sem invoice
(capturado só por webhook), ele busca a janela desse pedido no REST e a normalização da mesma rodada
o destrava — sem isso a espera seria a do poll (PIED_POLL_INTERVALO, 6h). Exige PIED_TOKEN; sem
preso não faz chamada nenhuma. Desenho, limites e diagnóstico do preso residual na
decisão 0017.
O gate de envio é a transição de pagamento (payment.status → received), detectada por
evento no upsert de pied_pedido (Maxsul, 2026-08-07). pied.integracao.estados-venda não governa
mais o envio — sobrou só como filtro do "Puxar da PIED" (captura sob demanda do console). O de-para de forma de
pagamento por pied.integracao.transform.class-forma (default financiamento→99). Só se ajustam via
application.yml/env estruturada; os defaults cobrem o caso comum (mapeamento,
pendências).
Como habilitar o poll¶
- Obter o token de produção da PIED e definir
PIED_TOKEN. - Definir
PIED_POLL_HABILITADO=true(e o intervalo desejado emPIED_POLL_INTERVALO). - Redeploy/restart. A primeira rodada ocorre ~1 minuto após o start; pedidos começam com carga
completa (cursor vazio) e as rodadas seguintes usam
?lastUpdateAftera partir depied_cursor. - Conferir:
SELECT entidade, count(*) FROM pied_rest GROUP BY 1;e o cursor empied_cursor.
Como habilitar a integração (fase 2)¶
- Definir
INTEGRADOR_BASE_URLeINTEGRADOR_API_TOKEN(Bearer estático do Integrador). - Confirmar o de-para e o conjunto de estados do gate com a Maxsul (pendências P1/P2); ajustar
pied.integracao.estados-venda/transform.class-formase preciso — os defaults cobrem o comum. - Recomendado no 1º deploy:
PIED_INTEGRACAO_MODO=STAGING— os pedidos elegíveis ficamNA_FILAe você confere e solta pelo/console(puxar → conferir de-para → enfileirar → enviar → atualizar) antes de deixar automático. Validado, mudar paraPRODUCAO. PIED_INTEGRACAO_HABILITADA=true+ redeploy. Não há despejo do histórico: o gate é a transição de pagamento (só entra o pedido que passa areceivednum update depois do enable; o backlog já-pago não re-transiciona).
Alerta de erro terminal do pedido (e-mail via Resend)¶
Quando um pedido é rejeitado de forma irrecuperável pelo X-Adm na reconciliação (ERRO_XADM com
codRetorno 1XX terminal), os contatos cadastrados na tela /destinatarios recebem um e-mail
via Resend (decisão 0005). Erros
reprocessáveis (ERRO de transporte, ERRO_XADM 9XX) e de sistema (esses vão ao GlitchTip) não
disparam. O envio é best-effort: se falhar, apenas loga (LOG.error → GlitchTip) e a reconciliação
segue. Como depende da reconciliação, o alerta fica dormente até a Fase 2 ser ligada.
Um e-mail por pedido e código de retorno. O último código alertado fica gravado no pedido
(pied_pedido.alerta_cod_retorno), então reiniciar o app (deploy, reboot do servidor) não reenvia os
alertas de pedidos que já estavam travados. O pedido volta a alertar quando o código muda, quando ele é
confirmado e depois trava de novo, ou quando o operador o reenfileira.
| Variável | Default | Uso |
|---|---|---|
PIED_ALERTA_HABILITADO |
false |
Liga o envio do alerta. Desligado (ou sem api-key/remetente) = no-op silencioso. |
RESEND_API_KEY |
(vazio) | Chave da API do Resend. Vazio = não envia. |
PIED_ALERTA_REMETENTE |
(vazio) | Remetente (from) do e-mail. O domínio precisa estar verificado no painel do Resend, senão o envio é recusado. |
PIED_ALERTA_BASE_URL |
(vazio) | URL pública do app p/ montar o link /pedido/{code} no corpo do e-mail (ex.: https://pied.maxsul.xadm.biz). |
Para ativar: (1) criar a conta/domínio no Resend e verificar o domínio do remetente; (2) definir
RESEND_API_KEY, PIED_ALERTA_REMETENTE e PIED_ALERTA_BASE_URL; (3) cadastrar os destinatários em
/destinatarios; (4) PIED_ALERTA_HABILITADO=true + redeploy.
Testar o envio: na tela /destinatarios, cada contato tem um botão "Enviar teste" que dispara um
e-mail de teste àquele endereço. O teste ignora o toggle PIED_ALERTA_HABILITADO (é ação manual),
mas exige RESEND_API_KEY + remetente — sem eles, a tela avisa "Configure RESEND_API_KEY…". Serve de
smoke-test pós-deploy do caminho do e-mail antes de ligar o alerta automático.
Console operador (/console)¶
Superfície única de operação da Fase 2 (absorveu o antigo /inspetor e /fila): dirige cada pedido
pela máquina de estados real (pied_pedido.status:
CAPTURADO→NA_FILA→ENVIANDO→ENVIADO→CONFIRMADO | ERRO_XADM | ERRO).
| Rota | O que faz | Modo |
|---|---|---|
GET /console[?status=] |
Lista filtrável por status, com stepper. | ambos |
GET /console/{code} |
Detalhe: bloco 1 (origem), 2 (de-para campo a campo), 3 (status+retorno). Read-only. O bloco 2 é dry-run sobre o payload atual — pode divergir do que foi efetivamente entregue, se o payload evoluiu após o envio (caso 260081441). | ambos |
POST /console/puxar-pied?n={1\|5} |
Puxa 1/5 pedidos finalizados da PIED → CAPTURADO (só elegíveis). |
STAGING |
POST /console/{code}/enfileirar |
Gate manual CAPTURADO→NA_FILA — exige pagamento recebido (payment_status='received'); recusa não-pago. Cobre o pedido que veio já-pago na 1ª captura. |
STAGING |
POST /console/{code}/enviar · POST /console/enviar-todos |
Envia 1 / todos os NA_FILA ao Integrador. |
STAGING |
POST /console/{code}/atualizar |
Reconcilia o retorno do X-Adm agora (ENVIADO→CONFIRMADO/ERRO_XADM). |
ambos |
POST /console/{code}/reenviar |
Reenvia ERRO/ERRO_XADM 9XX; barra o 1XX terminal. |
ambos |
POST /console/{code}/renormalizar |
Re-normaliza do próprio payload (sem chamar a PIED). |
ambos |
POST /console/sincronizar-clientes |
Varre v1/companies → preenche InscEst. |
ambos |
Em STAGING o console é interativo (o pedido espera NA_FILA até o clique); em PRODUCAO é
monitor (o pipeline anda sozinho; ficam só as ações de exceção — Atualizar/Reenviar).
⚠️ Aberto (sem auth, como o
/captura) — expõe PII e tem ações ao vivo (push ao X-Adm, fetch na PIED). O gate por login das rotas de ação está deferido (hardening separado; nenhuma regressão vs. o antigo/fila, que já era aberto) — proteger antes de expor fora da rede (decisão 0004, L8).
Webhook — cadastro do lado PIED (manual)¶
O registro do endpoint não é automático: é feito à mão no painel da PIED (Configurações → API e Webhooks → aba Webhooks → Adicionar):
- Endpoint:
https://pied.maxsul.xadm.biz/webhook/pied. - Gatilhos: eventos de orçamento e pedido (
budget.*,order.*) — e o que mais o painel oferecer: o serviço aceita qualquer evento. - Autenticação: definir o segredo e replicá-lo em
PIED_WEBHOOK_SECRET. Enquanto a forma exata de envio não for confirmada com a PIED, pode-se operar sem segredo (vazio) — os headers recebidos ficam registrados empied_webhook.headerspara diagnóstico. - Conferir:
SELECT evento, recebido_em FROM pied_webhook ORDER BY recebido_em DESC LIMIT 10;
Health check¶
GET /health → {"status":"UP","versao":"X.Y.Z"} (versão da release, lida do
version.properties gerado no build; servido pela lib xadm-comum-web). Usado pelo healthcheck do
container e pelo Coolify/Traefik.
Observabilidade — GlitchTip (erros)¶
Todo log ERROR — inclusive exceções não-tratadas do Micronaut — sobe para o GlitchTip via
SentryAppender no logback.xml, sem instrumentar o domínio. Configurado pelo /xadm-setup
(features.glitchtip no app.json).
| Variável | Origem | Uso |
|---|---|---|
SENTRY_DSN |
ENV do recurso Coolify no native (o alvo de deploy): o binário não tem sh nem jq no ENTRYPOINT. Só a imagem jar (Dockerfile) lê docs/app.json (features.glitchtip.dsn) via jq no entrypoint. |
DSN do projeto no GlitchTip, com o mesmo valor do app.json. Só SENTRY_DSN: a xadm-comum-web 0.10.0+ não lê GLITCHTIP_DSN e, com só ele, sobe com o error tracking desligado e WARN no boot. Vazio/ausente = no-op (dev/local). |
O DSN é client-grade (público) — por isso vive no app.json versionado, não é segredo. O
SENTRY_AUTH_TOKEN (upload de sourcemap) não se aplica a backend JVM (não há sourcemap).
Smoke-test pós-deploy¶
POST /test/glitchtip emite um erro de propósito para confirmar que o DSN chega ao GlitchTip sem
esperar um erro real:
curl -s -X POST https://pied.maxsul.xadm.biz/test/glitchtip | jq .
# → {"status":"enviado", ...} e o evento aparece no projeto maxsul-pied no GlitchTip
⚠️ Aberto (sem auth —
micronaut-securityvem transitivo da libxadm-comum-webmas está desligado viamicronaut.security.enabled=false), no mesmo espírito do/captura. Fecha na Fase 2: ligar o filtro + gate por login, ou remoção.
Diagnóstico da captura (/captura)¶
Superfície de debug do que foi capturado da PIED, sem acesso direto ao banco (que só existe dentro do
servidor Coolify) — o análogo da tabela requests do Integrador.
| Rota | O que faz |
|---|---|
GET /captura[?fonte=] |
Tela HTML: timeline unificada webhook + páginas REST (mais recente primeiro). ?fonte=rest\|webhook\|ambos filtra a fonte (default ambos; valor inválido → ambos). |
GET /captura/{tipo}/{id} |
Payload cru (pretty) de uma captura; {tipo} = webhook|rest. |
GET /captura/json |
Dump JSON agregado das 3 tabelas (pied_webhook, pied_rest, pied_cursor) com o payload aninhado, para colar de uma vez. ?limite=N por tabela (default 1000, teto 5000). |
# no browser: https://pied.maxsul.xadm.biz/captura → clica numa linha p/ ver o raw
curl -s https://pied.maxsul.xadm.biz/captura/json | jq .webhook
⚠️ Aberto e expõe PII (CNPJ, contatos) — sem auth por enquanto, diagnóstico atrás da rede. O gate por login fica deferido para um hardening separado (mesmo espírito do console; decisão 0004).
Telas e navegação (UI)¶
As telas são server-rendered (JTE — templates compilados, decisão 0025) com o kit de UI da casa
(engenharia/java-micronaut §UI): o
kit/layout.jte e o public/css/custom-theme.css são templates rastreados — vêm do repo
central e não se editam por app (a /xadm-docs re-deriva quando a identidade muda). A composição
inverte o antigo Thymeleaf: a página chama @template.kit.layout(...) passando o conteúdo (e um
navMenu opcional); o layout monta o <head>, o header com a marca e o bloco direito
(versão/modo/chip de sessão, em kit/headerRight.jte).
O que é deste app vive em src/main/jte/: kit/navMenu.jte (menu de operador + os chips
MODO/versão) e componentes/jsonCellScript.jte. O CSS de componente (timeline, tiles, buckets,
stepper) é o /css/app.css, que o layout.jte linka direto no <head>. Os chips ficam no slot do
menu porque o kit não tem slot de status no header; editá-lo por app faria o arquivo divergir do
central para sempre.
O GlobalViewModel injeta em toda tela o contrato do kit — appNome/clienteXadm/authEnabled
— mais modo/staging/versao. (authEnabled=false: sem chip de sessão, o app não tem auth.)
Duas superfícies, o mesmo layout: o painel do usuário final usa o fragment navbar (marca
sem menu, o chrome limpo que a decisão 0006 pedia — o
layout-painel.html que ela cita foi absorvido pelo kit e não existe mais); as telas internas
(operador) usam o navbarComMenu.
Painel do usuário Maxsul (decisão 0006):
| Rota | O que faz |
|---|---|
GET / |
Dashboard ("Importação de Pedidos PIED → X-Adm"): tiles de resumo por status amigável (Pedidos em aberto/Enviando ao X-Adm/Importados no X-Adm/Falha ao importar) + lista paginada de pedidos (clicável, 100/página, por last_update); ?status=<slug> filtra por bucket. |
GET /pedidos/{code} |
Detalhe: timeline do status + dados (cliente, documento, datas, status na PIED = deal_status, pagamento = payment.status, pago em) + itens do pedido. code inexistente → 404 amigável. |
POST /pedidos/{code}/importar |
Botão "Importar" (home): força o pedido em aberto já pago (CAPTURADO+received) para NA_FILA (gate manual). |
POST /pedidos/{code}/dispensar |
Botão "Dispensar" (home): marca o pedido em aberto já pago como IMPORTADO_MANUAL (já lançado à mão no X-Adm), sem enviar. |
POST /pedidos/{code}/reimportar |
Botão "Re-Importar" (detalhe), só em Falha ao importar e só com parte do pedido pendente (achada pelo diagnóstico, ou ?pendente=1 depois de um 409 do "Importado manualmente"): reenfileira; o re-arme das linhas 1XX no espelho sai junto com a entrega (PushEnviador). |
POST /pedidos/{code}/importado-manual |
Botão "Importado manualmente" (detalhe e home), só em Falha ao importar — o caminho padrão da Falha: ERRO_XADM → IMPORTADO_MANUAL depois que o operador lançou à mão no X-Adm o que faltou. Observação obrigatória (observacao, até 500 caracteres) e operador opcional (operador, até 100, lembrado no navegador; na home os dois vêm de prompts) — o painel não tem login, então eles são o rastro de o que e quem; gravados em importacao_manual_obs (operador no fim) e _em, exibidos no detalhe. Fecha antes a remessa no Integrador (POST /api/v1/integracao/remessas/resolver-manual → RESOLVIDA_MANUAL) e só com o 200 marca aqui: parte do pedido ainda pendente de entrega (409: remessa ABERTA, ou TRAVADA com filho 000 bloqueado — não lançar à mão; aguardar ou corrigir e Re-Importar) ou Integrador fora deixam o pedido em Falha, com o motivo no flash. Ver API do Integrador §5. |
Diagnóstico da falha (detalhe, só em Falha ao importar): consulta o Integrador ao vivo na
abertura da página — a remessa viva (GET /api/v1/integracao/remessas, o que foi rejeitado,
inclusive formulas) e o retorno por tabela (GET /api/v1/xadm/retorno/{tabela}, o que foi
gravado e o código do contrato no X-Adm). Mostra o tipo do pedido (Kit + componentes ×
produtos avulsos), Gravado no X-Adm (definitivo — a integração não reenvia), NÃO gravado
(com os dados do envio: quantidades, valores, frete, componentes com o nome, o produto do item do
kit, a venda a prazo) e avisos "Possível" tirados do texto do X-Adm: produto-kit não criado
(estoque 006 com mensagem de pedido e composição rejeitada), contrato em duplicidade (códigos
diferentes no estoque e no contrato — o operador escolhe qual fica antes de lançar) e item com o
valor no lugar do número do pedido. O "O que fazer" é um só — não excluir o que está em Gravado,
lançar à mão o que falta, marcar Importado manualmente —, exceto com parte pendente (linha no
espelho que não é 006 nem 1XX, o filho de um pai rejeitado): aí orienta corrigir e Re-Importar,
e só então o botão aparece. Integrador fora do ar ou sem INTEGRADOR_BASE_URL → aviso no lugar do
diagnóstico, a página não cai.
Telas internas (operador) — layout :: navbarComMenu(~{componentes :: navMenu}):
| Menu | Rotas |
|---|---|
(home) GET /homedev |
Painel de cards com a contagem de linhas por tabela + atalhos (a antiga /). |
| Operação | /console (ver acima). |
| Alertas | /destinatarios — CRUD dos contatos que recebem o e-mail de erro terminal (ver acima). |
| Banco | Browser read-only, uma tela por tabela pied_* (ver abaixo). |
| Captura | /captura (timeline) e /captura/json (dump). |
| Sistema | /health. |
Browser do banco (/dados/*)¶
Uma tela de listagem read-only por tabela — colunas escalares + célula JSON clicável (clica →
copia o payload formatado), teto ?limite=N (default 1000, teto 5000). Cobre as 7 tabelas pied_*:
| Rota | Tabela | Observação |
|---|---|---|
GET /dados/pedidos |
pied_pedido |
Link por linha abre o /console. |
GET /dados/clientes |
pied_cliente |
Expõe PII; lista todas as linhas, incl. deletadas. |
GET /dados/produtos |
pied_produto |
Lista todas, incl. deletados. |
GET /dados/webhooks |
pied_webhook |
Payload + headers como célula JSON. |
GET /dados/rest |
pied_rest |
Páginas cruas do poll. |
GET /dados/cursor |
pied_cursor |
Cursor incremental + ultima_pagina. |
GET /dados/estado |
pied_integracao_estado |
Singleton com o corte go-forward. |
⚠️ Tudo aberto (sem auth) e expõe PII — mesma postura do
/captura//console: diagnóstico atrás da rede, hardening por login deferido. É read-only (não escreve no banco); as ações de operação ficam só no/console.
Desenvolvimento local¶
./gradlew run sobe o Postgres de dev via docker-compose.dev.yml (porta 5433, para não
colidir com o Postgres de dev do integrador) e ativa o perfil dev (application-dev.yml).
Os testes não usam esse banco: provisionam o próprio Postgres via PostgresTestResource da casa
(xadm-comum-teste, Testcontainers).