@seedgrid/ai-toolkit 0.3.2 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/docs/formato.md +1 -1
  2. package/package.json +1 -1
  3. package/seedgridKB/SeedGrid/Extensions/Catalogo-de-propriedades.md +3 -2
  4. package/seedgridKB/SeedGrid/Extensions/Indice.md +1 -0
  5. package/seedgridKB/SeedGrid/Extensions/ai-pricing/Configuracao.md +11 -18
  6. package/seedgridKB/SeedGrid/Extensions/ai-pricing/Exemplos.md +2 -2
  7. package/seedgridKB/SeedGrid/Extensions/ai-pricing/Visao-geral.md +2 -2
  8. package/seedgridKB/SeedGrid/Extensions/business-days/Configuracao.md +36 -0
  9. package/seedgridKB/SeedGrid/Extensions/business-days/Exemplos.md +37 -0
  10. package/seedgridKB/SeedGrid/Extensions/business-days/Visao-geral.md +48 -0
  11. package/seedgridKB/SeedGrid/Extensions/exchange/Configuracao.md +10 -16
  12. package/seedgridKB/SeedGrid/Extensions/exchange/Exemplos.md +2 -2
  13. package/seedgridKB/SeedGrid/Extensions/exchange/Visao-geral.md +2 -2
  14. package/seedgridKB/SeedGrid/Extensions/login-tenancy/Configuracao.md +4 -5
  15. package/seedgridKB/SeedGrid/Extensions/login-tenancy/Visao-geral.md +1 -1
  16. package/seedgridKB/SeedGrid/Extensions/subdomain-tenancy/Configuracao.md +3 -2
  17. package/seedgridKB/SeedGrid/Extensions/subdomain-tenancy/Visao-geral.md +2 -2
  18. package/seedgridKB/SeedGrid/Mapa-dos-repositorios.md +9 -0
  19. package/seedgridKB/SeedGrid/Produtos/Pulse/Autenticacao-e-chaves.md +89 -0
  20. package/seedgridKB/SeedGrid/Produtos/Pulse/Exemplos.md +44 -0
  21. package/seedgridKB/SeedGrid/Produtos/Pulse/Protocolo-e-API.md +191 -0
  22. package/seedgridKB/SeedGrid/Produtos/Pulse/SDKs.md +287 -0
  23. package/seedgridKB/SeedGrid/Produtos/Pulse/Servidor/Configuracao.md +128 -0
  24. package/seedgridKB/SeedGrid/Produtos/Pulse/Servidor/Instalacao.md +105 -0
  25. package/seedgridKB/SeedGrid/Produtos/Pulse/Servidor/Operacao.md +86 -0
  26. package/seedgridKB/SeedGrid/Produtos/Pulse/Servidor/Providers.md +121 -0
  27. package/seedgridKB/SeedGrid/Produtos/Pulse/UI/Configuracao.md +61 -0
  28. package/seedgridKB/SeedGrid/Produtos/Pulse/UI/Visao-geral.md +74 -0
  29. package/seedgridKB/SeedGrid/Produtos/Pulse/Visao-geral.md +96 -0
  30. package/seedgridKB/SeedGrid/Toolkit/rules/backend-java-seedgrid.md +25 -1
  31. package/seedgridKB/SeedGrid/Toolkit/skills/sg-code-review/SKILL.md +1 -1
  32. package/seedgridKB/SeedGrid/Toolkit/skills/sg-crud-back/SKILL.md +1 -1
  33. package/seedgridKB/SeedGrid/Toolkit/skills/sg-crud-front/SKILL.md +3 -3
  34. package/seedgridKB/SeedGrid/Toolkit/skills/sg-new-screen/SKILL.md +2 -2
  35. package/seedgridKB/SeedGrid/Toolkit/skills/sg-new-skill/SKILL.md +20 -5
  36. package/seedgridKB/SeedGrid/Visao-geral.md +7 -0
  37. package/seedgridKB/manifest.json +7 -7
  38. /package/seedgridKB/SeedGrid/Toolkit/skills/sg-code-review/references/{checklist.md → code-review-checklist.md} +0 -0
  39. /package/seedgridKB/SeedGrid/Toolkit/skills/sg-crud-front/references/{checklist.md → crud-front-checklist.md} +0 -0
  40. /package/seedgridKB/SeedGrid/Toolkit/skills/sg-crud-front/references/{patterns.md → crud-front-patterns.md} +0 -0
  41. /package/seedgridKB/SeedGrid/Toolkit/skills/sg-crud-front/references/{routing.md → crud-front-routing.md} +0 -0
  42. /package/seedgridKB/SeedGrid/Toolkit/skills/sg-new-screen/references/{checklist.md → new-screen-checklist.md} +0 -0
  43. /package/seedgridKB/SeedGrid/Toolkit/skills/sg-new-screen/references/{steps.md → new-screen-steps.md} +0 -0
@@ -0,0 +1,191 @@
1
+ ---
2
+ tags: [seedgrid, produto, pulse, protocolo, api, websocket]
3
+ fonte: seedgrid-products/seedgrid-pulse (backend/PROTOCOL.md, backend/README.md, src/openapi.rs)
4
+ branch: main
5
+ commit: 9dcbdad
6
+ analisado-em: 2026-10-16
7
+ ---
8
+
9
+ # Pulse — protocolo realtime e API HTTP
10
+
11
+ Resumo do contrato. Fonte completa: `backend/PROTOCOL.md` (protocolVersion **1**) e a spec OpenAPI gerada pelo servidor. Voltar: [[SeedGrid/Produtos/Pulse/Visao-geral|Visão geral]] · credenciais: [[SeedGrid/Produtos/Pulse/Autenticacao-e-chaves|Autenticação]].
12
+
13
+ ## Endereçamento
14
+
15
+ Uma mensagem tem: `accountId` (da chave), `channel` (obrigatório), `event` (concreto, obrigatório), `group` e `user` (opcionais, independentes), `data`.
16
+
17
+ | `group` | `user` | entrega para |
18
+ |---|---|---|
19
+ | — | — | todas as conexões **da conta** no canal |
20
+ | G | — | quem assinou G |
21
+ | — | U | as conexões do usuário U |
22
+ | G | U | as conexões de U **que também assinaram** G |
23
+
24
+ - `user` vem da **credencial** (subject), nunca do `subscribe`. `group` é **namespace, não autorização**: trate a mensagem como **sinal**, não como conteúdo sensível.
25
+ - `group`/`user` não podem ser vazios nem conter `|`. Use `publicId`, nunca e-mail.
26
+ - Fronteira = conta; sem destinatário a mensagem fica `pending` até alguém conectar (TTL expira).
27
+
28
+ ## Publicar (REST)
29
+
30
+ ```http
31
+ POST /api/v1/messages
32
+ x-api-key: <chave project>
33
+ ```
34
+
35
+ ```json
36
+ {
37
+ "channel": "orders",
38
+ "event": "order.updated",
39
+ "user": "usr_7f3a9c1e",
40
+ "deliveryMode": "notification",
41
+ "data": { "orderId": "o-1", "status": "paid" },
42
+ "deliveryPolicy": { "maxAttempts": 5, "ttlSeconds": 3600, "retryBaseMs": 250, "retryMaxMs": 5000, "retryStrategy": "exponential" }
43
+ }
44
+ ```
45
+
46
+ Resposta: `{ "messageId": "msg_…", "status": "accepted", "createdAt": "…" }`. `deliveryPolicy` é opcional e limitada pelos tetos `*_CAP` do servidor (campo a campo em [[SeedGrid/Produtos/Pulse/Protocolo-e-API#deliveryPolicy: campo a campo|deliveryPolicy]]). Erros: 400 requisição malformada · 422 `deliveryPolicy`/endereçamento inválido · 401 credencial · 403 `ACCOUNT_MISMATCH`/chave `client` · 404/422 conta inexistente ou revogada, endereçamento ou `event` não concreto.
47
+
48
+ ## Delivery modes
49
+
50
+ | Modo | Fim do ciclo | `ack` | `result` | DLQ |
51
+ |---|---|---|---|---|
52
+ | `notification` (padrão) | escrita no socket ⇒ `completed` | no-op | erro `result_not_applicable` | nunca |
53
+ | `acknowledged` | `ack` do cliente ⇒ `completed` | termina | erro `result_not_applicable` | nunca |
54
+ | `processed` | `result: success` ⇒ `completed` | ⇒ `delivered` | `success` / `retryable` / `permanent` | **sim** |
55
+
56
+ Só o `processed` consome tentativas e chega à DLQ. Offline ou falha de transporte **não** vão à DLQ (a mensagem fica `pending`).
57
+
58
+ ## deliveryPolicy: campo a campo
59
+
60
+ Opcional, por mensagem. Diz **como insistir**, não para quem entregar. Todo campo é opcional: ausente ⇒ vale o padrão do servidor. Uso nos SDKs: [[SeedGrid/Produtos/Pulse/SDKs|SDKs]].
61
+
62
+ | Campo | Tipo | Padrão do servidor | O que faz |
63
+ |---|---|---|---|
64
+ | `maxAttempts` | inteiro > 0 | `20` (`DELIVERY_MAX_ATTEMPTS`) | Quantas falhas de **execução** (`result: retryable`) a mensagem aguenta. Só vale em `processed`. A cada `retryable` o contador sobe; quando passa de `maxAttempts` (ou seja, na falha de número `maxAttempts + 1`) a mensagem vai à **DLQ**. Falha de transporte (cliente offline, escrita no socket) **não** consome tentativa. `result: permanent` vai à DLQ na hora, independentemente deste valor. |
65
+ | `ttlSeconds` | inteiro > 0 | `86400` = 1 dia (`DELIVERY_TTL_SECONDS`) | Validade da mensagem, contada a partir da publicação. Se ao fim do prazo ela ainda estiver `pending` (ninguém a recebeu), o estado vira `expired` e ela não é mais entregue. Vale para **todos** os modos. Expirar **não** manda para a DLQ nem consome tentativa. Veja [[SeedGrid/Produtos/Pulse/Protocolo-e-API#Como o TTL é aplicado|Como o TTL é aplicado]]. |
66
+ | `resultTimeoutSeconds` | inteiro > 0 (s) | `0` = **sem prazo** (`DELIVERY_RESULT_TIMEOUT_SECONDS`) | **Prazo de execução**, só em `processed`: segundos entre o `ack` e o `result`. Estourado sem `result`, vale como um `result: retryable` com motivo `result_timeout` (consome tentativa; esgotadas, vai à **DLQ**). Detalhes em [[SeedGrid/Produtos/Pulse/Protocolo-e-API#Prazo de execução (resultTimeoutSeconds)|Prazo de execução]]. |
67
+ | `retryBaseMs` | inteiro > 0 (ms) | `300` (`DELIVERY_RETRY_BASE_MS`) | Atraso antes da próxima tentativa. Na estratégia `exponential` é o atraso inicial; em `fixed` é o atraso de toda tentativa. |
68
+ | `retryMaxMs` | inteiro > 0 (ms), `>= retryBaseMs` | `5000` (`DELIVERY_RETRY_MAX_MS`) | Teto do atraso entre tentativas. |
69
+ | `retryStrategy` | `"fixed"` \| `"exponential"` | `exponential` (`DELIVERY_RETRY_STRATEGY`) | Como o atraso cresce. |
70
+
71
+ **Cálculo do atraso** (Confirmado, `DeliveryPolicy::next_delay` no servidor), com *n* = número da tentativa que falhou:
72
+
73
+ - `exponential`: `min(retryBaseMs × 2^(n−1), retryMaxMs)`. Com base 200 e teto 2000: 200, 400, 800, 1600, 2000, 2000…
74
+ - `fixed`: `min(retryBaseMs, retryMaxMs)`, sempre o mesmo atraso.
75
+ - No `result: retryable`, se o cliente mandar `retryAfterSecs`, **ele vale no lugar** do cálculo acima.
76
+ - O mesmo atraso espaça as tentativas de transporte quando o destinatário está offline ou a escrita no socket falha (a mensagem fica `pending` até o TTL).
77
+
78
+ **Validação** (rejeita com **422** `INVALID_MESSAGE_REQUEST`): qualquer campo numérico igual a `0` (inclusive `resultTimeoutSeconds`); `retryMaxMs < retryBaseMs` quando os dois vêm; `retryStrategy` diferente de `fixed`/`exponential` (minúsculas).
79
+
80
+ **Tetos do servidor.** O que o publisher manda **nunca passa** do teto configurado (`DELIVERY_MAX_ATTEMPTS_CAP`, `DELIVERY_TTL_SECONDS_CAP`, `DELIVERY_RETRY_BASE_MS_CAP`, `DELIVERY_RETRY_MAX_MS_CAP`, `DELIVERY_RESULT_TIMEOUT_SECONDS_CAP`): acima dele o valor é **reduzido em silêncio**, sem erro. Como cada teto, quando não definido, é igual ao padrão, **por padrão uma mensagem só consegue pedir menos que o padrão**: `maxAttempts: 50` vira `20`, `ttlSeconds: 604800` vira `86400`. Para permitir mais, suba o `*_CAP` no servidor ([[SeedGrid/Produtos/Pulse/Servidor/Configuracao|Configuração do servidor]]). Dentro dos tetos, `retryMaxMs` nunca fica abaixo do `retryBaseMs` efetivo. Exceção: o teto do prazo de execução tem padrão `86400` (e não o valor do padrão, que é `0`), então o publisher consegue pedir até 1 dia mesmo com o recurso desligado por padrão; `DELIVERY_RESULT_TIMEOUT_SECONDS_CAP=0` **proíbe** o prazo de execução.
81
+
82
+ **Quando usar.** Em `notification` e `acknowledged` só o `ttlSeconds` (e o espaçamento das tentativas de transporte) importa; `maxAttempts` e a DLQ só entram em `processed`. Exemplo para um job que pode ser reentregue algumas vezes e deve morrer em 5 minutos:
83
+
84
+ ```json
85
+ "deliveryMode": "processed",
86
+ "deliveryPolicy": { "maxAttempts": 5, "ttlSeconds": 300, "resultTimeoutSeconds": 120, "retryBaseMs": 200, "retryMaxMs": 2000, "retryStrategy": "exponential" }
87
+ ```
88
+
89
+ ### Como o TTL é aplicado
90
+
91
+ - **Quando é verificado.** A cada tentativa de entrega e por um verificador periódico que roda a cada `ceil(DELIVERY_RETRY_BASE_MS / 1000)` segundos, no mínimo 1 s (com o padrão de 300 ms, **a cada 1 s**). Na prática a mensagem expira com até ~1 s de atraso depois do prazo. Esse verificador usa o valor **do servidor**, não o `retryBaseMs` da mensagem. Ele **não** tem relação com o intervalo de limpeza (`DELIVERY_CLEANUP_INTERVAL_SECONDS`), que cuida só de retenção e lixeira.
92
+ - **Só vale para `pending`.** O TTL mede quanto tempo a mensagem espera por um destinatário. Depois que o `ack` chega, uma mensagem `processed` fica `delivered`, e a partir daí o TTL **não a expira mais**. Quem cobre essa fase é o prazo de execução (`resultTimeoutSeconds`, veja abaixo). Sem esse prazo (o padrão é `0` = sem prazo), uma tarefa que o consumidor recebeu e nunca respondeu fica `delivered` até o `result` chegar ou até um operador descartar.
93
+ - **Descarte manual antecipa o TTL.** Em Pendências, "descartar" (`POST /backlog/discard`) leva `pending` **e** `delivered` sem resultado para `expired` na hora. A mensagem continua visível em "Mensagens" e só é apagada pela retenção (`DELIVERY_CLEANUP_RETENTION_DAYS`).
94
+ - **Lixeira.** Mensagem na lixeira não expira por TTL.
95
+ - **Não confunda com o token.** `ttlSeconds` também existe em `issueConnectToken`, mas ali é a janela do token curto para **conectar** (padrão 300 s, teto 900 s). Os dois nomes são iguais e não têm relação entre si.
96
+ - **O publisher não é avisado.** O publish responde `202 accepted` e pronto. Que a mensagem expirou aparece na UI (Mensagens, filtro `expired`), na métrica `delivery_expired_total` e no log de tentativas da mensagem (`outcome: expired`, `message_ttl_exceeded`).
97
+
98
+ ### Prazo de execução (resultTimeoutSeconds)
99
+
100
+ Disponível a partir do Pulse **0.3.0** (Confirmado no commit `9dcbdad`). Cobre o caso da tarefa que o consumidor recebeu e travou.
101
+
102
+ - **Só `processed`.** Em `notification` e `acknowledged` não há `result`, então o campo não tem efeito.
103
+ - **Conta do `ack` até o `result`** (`delivered_at`). Cada nova entrega confirmada reinicia a contagem.
104
+ - **Estourou sem `result`:** o servidor aplica o mesmo tratamento de um `result: retryable`: soma 1 à tentativa, devolve a mensagem a `pending` com o backoff da `deliveryPolicy` e a reentrega. Se `attemptCount > maxAttempts`, vai à DLQ com `lastExecutionError = "result_timeout"`. No log de tentativas aparece como `execution_retryable` com detalhe `result_timeout` (não existe outcome separado).
105
+ - **Quando é verificado:** pelo mesmo verificador periódico do retry (a cada ~1 s com o padrão), então o estouro é detectado com até ~1 s de atraso.
106
+ - **Padrão `0` = sem prazo** (`DELIVERY_RESULT_TIMEOUT_SECONDS`), para não mudar o comportamento de quem já usa o Pulse. Peça o prazo por mensagem, ou o operador define um padrão em Configurações. `0` **enviado** no `deliveryPolicy` é inválido (422); para não ter prazo, omita o campo.
107
+ - **Teto:** `DELIVERY_RESULT_TIMEOUT_SECONDS_CAP` (padrão `86400`; `0` proíbe). Acima do teto, o valor é reduzido em silêncio, como nos outros campos.
108
+ - **`result` tardio é ignorado.** Se o `result` da tentativa antiga chegar depois do estouro, enquanto a mensagem espera a nova tentativa (`pending` com `lastExecutionError = "result_timeout"`), o servidor o ignora (sem erro de protocolo e sem alterar o estado). Depois que a nova tentativa é entregue e confirmada, o `result` volta a valer. **Limitação:** não há identificador de tentativa; um `result` antigo que chegue *depois* da reentrega ser confirmada é aceito.
109
+ - **Mensagens antigas:** as `delivered` que já existiam antes da migration (`0013`) ficam **sem prazo**; o descarte manual em Pendências continua sendo a saída para elas.
110
+ - **Cuidado (a tarefa pode rodar duas vezes).** Um prazo curto reentrega uma tarefa que ainda está em execução. Escolha um prazo **maior que o pior caso** da tarefa e torne o consumidor **idempotente** (deduplicando por `messageId`).
111
+ - **Observabilidade:** motivo `result_timeout` na UI (tela de detalhes da mensagem: "Último erro de execução" e detalhe das tentativas; as telas de DLQ não mostram esse campo) e o campo opcional "Prazo de execução (s)" na tela Publicar e a métrica `seedgrid_pulse_delivery_result_timeout_total`. O endpoint `/meta` expõe o padrão e o teto (`deliveryPolicyDefaults.resultTimeoutSeconds` e `deliveryPolicyCaps.resultTimeoutSeconds`).
112
+
113
+ ## DLQ: quando uma mensagem vai para lá
114
+
115
+ A DLQ (fila de mensagens mortas) guarda **tarefas** que desistiram de executar. Só entra nela mensagem em `processed`, por um destes dois caminhos:
116
+
117
+ | Situação | Vai à DLQ? |
118
+ |---|---|
119
+ | Subscriber responde `result: permanent` | **Sim, na hora**, sem olhar as tentativas que sobram |
120
+ | Subscriber responde `result: retryable` e as tentativas esgotam (`attemptCount > maxAttempts`) | **Sim** |
121
+ | Subscriber responde `result: retryable` e ainda há tentativas | Não: volta a `pending` e é reentregue após o atraso do backoff (ou do `retryAfterSecs`) |
122
+ | Tarefa `delivered` e o prazo de execução (`resultTimeoutSeconds`) estoura sem `result` | Conta como `retryable` com motivo `result_timeout`: volta a `pending` com backoff; **se as tentativas esgotam, sim**, com `lastExecutionError = "result_timeout"` |
123
+ | Subscriber responde `result: success` | Não: `completed` |
124
+
125
+ **Nunca vai à DLQ:**
126
+
127
+ - `notification` e `acknowledged`, mesmo que o destinatário nunca receba (não há execução para falhar; `result` nesses modos é recusado com `result_not_applicable`).
128
+ - Destinatário offline, ou falha ao escrever no socket: a mensagem fica `pending`, tenta de novo e não consome tentativa.
129
+ - TTL vencido: vira `expired`.
130
+ - Tarefa que o consumidor recebeu (`delivered`) e nunca respondeu, **quando não há prazo de execução** (`resultTimeoutSeconds` ausente e padrão `0`): não cai sozinha na DLQ; só o descarte manual a tira dali.
131
+
132
+ **O que fazer com o que está na DLQ** (UI: Falhas, ou API `/dlq/*`, com as permissões `PULSE_DLQ_READ`, `PULSE_DLQ_REPLAY`, `PULSE_DLQ_CLEAN`):
133
+
134
+ - **Reprocessar** (`POST /dlq/requeue`): a mensagem volta a `pending` com o contador de tentativas **zerado**, o erro anterior apagado e a **data de publicação reiniciada**, então o TTL também recomeça.
135
+ - **Limpar** (`/dlq/clean`): vai para a lixeira da DLQ (`dlq_removed`); dá para restaurar (`/dlq/trash/restore`) ou apagar de vez (`/dlq/trash/purge`).
136
+ - O motivo da última falha fica registrado (`reason` do `result`).
137
+
138
+ ## Conectar (WebSocket)
139
+
140
+ ```text
141
+ GET /api/v1/connect?protocolVersion=1
142
+ ```
143
+
144
+ Credencial por um de três caminhos (browser não envia header em WebSocket): header `Authorization: Bearer`, query `?access_token=`, ou subprotocolo `bearer, <credencial>` / `pulse.bearer.<credencial>`. Credencial inválida/ausente ⇒ 401 no upgrade.
145
+
146
+ Fluxo:
147
+
148
+ ```text
149
+ servidor → {"type":"connected","protocolVersion":1,"connectionId":"…"}
150
+ cliente → {"type":"subscribe","channels":["orders"],"groups":["loja:15"],"events":["order.*"]}
151
+ servidor → {"type":"subscribed","accountId":"…","user":"…","channels":[…],"groups":[…,"user:<u>"],"events":[…]}
152
+ servidor → {"type":"message","messageId":"…","channel":"…","event":"…","data":{…},"requiresAck":true,"deliveryMode":"processed"}
153
+ cliente → {"type":"ack","messageId":"…"}
154
+ cliente → {"type":"result","messageId":"…","outcome":"success"|"retryable"|"permanent","reason":"…","retryAfterSecs":30}
155
+ ```
156
+
157
+ - `events`: filtro opcional; `*` só como último segmento e casa **um ou mais** segmentos (`order.*` casa `order.item.updated`, não `order`). Um `subscribe` novo **acumula** canais/grupos e **substitui** o filtro de eventos.
158
+ - O cliente **não declara identidade**: `userId`/`user` enviados são ignorados.
159
+ - Replay: ao assinar, o servidor entrega as pendências da conta que a conexão cobre.
160
+ - Entrega é **at-least-once**: o cliente deve **deduplicar por `messageId`**; `result` é idempotente.
161
+
162
+ Erros realtime: `unsupported_protocol_version`, `invalid_subscribe`, `invalid_ack`, `invalid_result`, `result_not_applicable`, `channel_not_allowed`, `unauthorized`, `internal_error`.
163
+
164
+ Fechamentos: **`4403`** `credential_revoked` (chave/conta revogada — **não reconectar** com a mesma credencial); `4401` legado (token expirado; os SDKs ainda tratam).
165
+
166
+ ## Estados de uma mensagem
167
+
168
+ `pending` → `delivered` → `completed` (terminal) · `dead_letter` (falha de execução) · `expired` (TTL) · `trashed` (na lixeira, só vinda de `pending`) · `dlq_removed` (lixeira da DLQ) · `acknowledged` (legado, lido como `delivered`).
169
+
170
+ ## Endpoints principais
171
+
172
+ | Área | Endpoints |
173
+ |---|---|
174
+ | Mensagens | `POST/GET /api/v1/messages`, `GET /messages/{id}`, `POST /messages/trash`, `/trash/restore`, `/trash/purge` |
175
+ | Pendências | `GET /backlog`, `POST /backlog/discard` |
176
+ | DLQ | `POST /dlq/requeue`, `/dlq/clean`, `/dlq/trash/restore`, `/dlq/trash/purge` |
177
+ | Conexões / presença / stats | `GET /connections`, `GET /presence/groups/{group}`, `GET /stats/connections`, `GET /stats/messages/aggregates` |
178
+ | Token | `POST /connect-tokens` |
179
+ | Contas | `GET/POST /accounts`, `/accounts/{id}`, `/activate`, `/revoke`, `/trash`, `/trash/restore`, `/purge`, `GET /accounts/{id}/deletion-summary`; chaves em `/accounts/{id}/keys` |
180
+ | Ajustes | `GET /api/v1/admin/settings`; alterar/resetar um ajuste em `/api/v1/admin/settings/{key}` (`POST` altera, `DELETE` volta ao padrão) |
181
+ | Operação | `GET /health`, `/ready`, `/meta`, `/metrics`, `/metrics/prometheus` |
182
+
183
+ Lotes de lixeira: máximo **1000** ids (`400 BATCH_TOO_LARGE`); resposta `{requested, affected, skipped}`. Erros de domínio seguem RFC 7807 (`application/problem+json`).
184
+
185
+ ## Observabilidade por destinatário
186
+
187
+ `GET /messages/{id}/recipients` e `GET /recipient-deliveries` (filtros `userId`, `groupId`, `messageId`, `connectionId`, `status`, `acknowledged`) mostram quem recebeu cada mensagem — o histórico serve de **prova de entrega**.
188
+
189
+ ## Compatibilidade
190
+
191
+ Mudanças aditivas (campos opcionais, novos eventos) não mudam `protocolVersion`; mudar semântica de endereçamento, remover `ack` ou alterar o shape de `message` exige incremento. Ordem de rollout quando o protocolo muda: publicar SDKs ⇒ reinstalar agentes ⇒ subir servidor ⇒ UI.
@@ -0,0 +1,287 @@
1
+ ---
2
+ tags: [seedgrid, produto, pulse, sdk, publisher, subscriber]
3
+ fonte: seedgrid-products/seedgrid-pulse/components
4
+ branch: main
5
+ commit: 9dcbdad
6
+ analisado-em: 2026-10-16
7
+ ---
8
+
9
+ # Pulse — SDKs
10
+
11
+ Dois lados independentes, cada um com sua credencial e seu transporte. Copie/instale **só o lado que precisa**. Voltar: [[SeedGrid/Produtos/Pulse/Visao-geral|Visão geral]] · exemplos prontos: [[SeedGrid/Produtos/Pulse/Exemplos|Exemplos]].
12
+
13
+ | | **Publisher** | **Subscriber** |
14
+ |---|---|---|
15
+ | Papel | publicar mensagens e emitir token curto | conectar, assinar, receber, `ack`/`result` |
16
+ | Roda em | backend confiável (API, worker, job) | navegador, app, agente |
17
+ | Credencial | chave **`project`** (`x-api-key`) 🔒 | token curto (`getToken`) **ou** chave `client` (agente) |
18
+ | Transporte | REST `POST /api/v1/messages` | WebSocket (reconecta sozinho) |
19
+ | Regra de ouro | a chave **nunca** vai ao cliente | **nenhuma** chave no navegador/app |
20
+
21
+ ## Publishers (`components/publishers/`)
22
+
23
+ | Linguagem | Pacote / coordenada | Versão (manifesto) | Entrada |
24
+ |---|---|---|---|
25
+ | Java 17+ | `br.com.seedgrid:seedgrid-pulse-publisher` (Maven Central) | `2026.10.0` | `PulsePublisher.builder().baseUrl().projectKey().build()` |
26
+ | Next.js (server-only) | `@seedgrid/pulse-publisher-next` | `2026.09.0` | `createPulsePublisher({ baseUrl, projectKey })` |
27
+ | Dart | `seedgrid_pulse_publisher` | `2026.9.1` | `PulsePublisher.create(baseUrl:, projectKey:)` |
28
+ | PHP | `seedgrid/pulse-publisher` (Composer) | — | `HttpPulsePublisher::create(baseUrl:, projectKey:)` |
29
+ | Rust | `seedgrid-pulse-publisher-rust` | `0.2.0` | `PulsePublisher::builder(url, key).build()?` |
30
+ | Delphi | fonte em `src/` (XE6+/FPC) | — | `NewPulsePublisher(url, key)` |
31
+
32
+ Todos oferecem: `publish(...)` com helpers de destino (`toUser`, `toGroup`, `toAccount`, e grupo+usuário), `deliveryMode`, `deliveryPolicy`, `metadata` e `issueConnectToken(...)`.
33
+
34
+ Java:
35
+
36
+ ```java
37
+ PulsePublisher publisher = PulsePublisher.builder()
38
+ .baseUrl(System.getenv("PULSE_BASE_URL"))
39
+ .projectKey(System.getenv("PULSE_PROJECT_KEY"))
40
+ .build();
41
+
42
+ publisher.publish(PublishMessageInput.toUser(
43
+ "orders", "order.updated", "usr_7f3a9c1e", Map.of("orderId", "o-1")));
44
+
45
+ // rota autenticada do SEU backend, para o navegador:
46
+ ConnectToken token = publisher.issueConnectToken(
47
+ new IssueConnectTokenInput(sessao.publicId(), Duration.ofMinutes(5), List.of("orders")));
48
+ ```
49
+
50
+ Next.js:
51
+
52
+ ```ts
53
+ import "server-only";
54
+ import { createPulsePublisher, message } from "@seedgrid/pulse-publisher-next";
55
+
56
+ export const pulse = createPulsePublisher({
57
+ baseUrl: process.env.PULSE_BASE_URL!,
58
+ projectKey: process.env.PULSE_PROJECT_KEY!, // nunca NEXT_PUBLIC_*
59
+ });
60
+
61
+ await pulse.publish(message.toUser("orders", "order.updated", usuario.publicId, { orderId }));
62
+ const token = await pulse.issueConnectToken({ subject: usuario.publicId, ttlSeconds: 300 });
63
+ ```
64
+
65
+ PHP:
66
+
67
+ ```php
68
+ $publisher = HttpPulsePublisher::create(baseUrl: getenv('PULSE_BASE_URL'), projectKey: getenv('PULSE_PROJECT_KEY'));
69
+ $publisher->publish(PublishMessageInput::toUser('orders', 'order.updated', 'usr_7f3a9c1e', ['orderId' => 'o-1']));
70
+ ```
71
+
72
+ Dart:
73
+
74
+ ```dart
75
+ final publisher = PulsePublisher.create(
76
+ baseUrl: Platform.environment['PULSE_BASE_URL']!,
77
+ projectKey: Platform.environment['PULSE_PROJECT_KEY']!,
78
+ );
79
+ final result = await publisher.publish(
80
+ PublishMessageInput.toUser('orders', 'order.updated', 'usr_7f3a9c1e', {'orderId': 'o-1'}),
81
+ );
82
+ print(result.messageId);
83
+ publisher.close();
84
+ ```
85
+
86
+ Rust:
87
+
88
+ ```rust
89
+ let publisher = PulsePublisher::builder(base_url, project_key).build()?;
90
+ let result = publisher
91
+ .publish(PublishMessageInput::to_user("orders", "order.updated", "usr_7f3a9c1e", json!({ "orderId": "o-1" })))
92
+ .await?;
93
+ println!("{}", result.message_id);
94
+ ```
95
+
96
+ Delphi:
97
+
98
+ ```pascal
99
+ Config := TPulsePublisherConfig.Create(GetEnvironmentVariable('PULSE_BASE_URL'), GetEnvironmentVariable('PULSE_PROJECT_KEY'));
100
+ Publisher := TPulsePublisher.Create(Config, TIndyPulseHttpTransport.Create); // Delphi moderno: transporte NetHttp
101
+ R := Publisher.Publish(TPublishMessageInput.ToUser('orders', 'order.updated', 'usr_7f3a9c1e', '{"orderId":"o-1"}'));
102
+ Writeln(R.MessageId);
103
+ ```
104
+
105
+ Variáveis recomendadas no backend: `PULSE_BASE_URL`, `PULSE_PROJECT_KEY` 🔒. Os exemplos completos de cada SDK ficam em `examples/` (ou `example/`) da pasta do pacote.
106
+
107
+ ### Publicar com modo e política de entrega
108
+
109
+ `deliveryMode` escolhe o ciclo de vida da mensagem e `deliveryPolicy` diz **como insistir** (tentativas, validade, espaçamento). Os dois são opcionais e independentes; o significado campo a campo está em [[SeedGrid/Produtos/Pulse/Protocolo-e-API#deliveryPolicy: campo a campo|Protocolo e API]]. Omitidos, vale `notification` e os padrões do servidor. Um job que o consumidor precisa **executar** usa `processed` (único modo com `result`, retry de execução e DLQ):
110
+
111
+ Java:
112
+
113
+ ```java
114
+ publisher.publish(PublishMessageInput.builder()
115
+ .channel("render").event("render.requested").toUser("agent:render-node-7")
116
+ .data(Map.of("jobId", "job_9f2"))
117
+ .deliveryMode(DeliveryMode.PROCESSED)
118
+ .deliveryPolicy(DeliveryPolicy.builder()
119
+ .maxAttempts(5).ttlSeconds(300).resultTimeoutSeconds(120)
120
+ .retryBaseMs(200).retryMaxMs(2000)
121
+ .retryStrategy(RetryStrategy.EXPONENTIAL)
122
+ .build())
123
+ .build());
124
+ ```
125
+
126
+ Next.js:
127
+
128
+ ```ts
129
+ await pulse.publish({
130
+ channel: "render",
131
+ event: "render.requested",
132
+ target: { user: "agent:render-node-7" },
133
+ data: { jobId: "job_9f2" },
134
+ deliveryMode: "processed",
135
+ deliveryPolicy: { maxAttempts: 5, ttlSeconds: 300, resultTimeoutSeconds: 120, retryBaseMs: 200, retryMaxMs: 2000, retryStrategy: "exponential" },
136
+ metadata: { correlationId: "report-4471", source: "report-api" },
137
+ });
138
+ ```
139
+
140
+ PHP:
141
+
142
+ ```php
143
+ $input = PublishMessageInput::toUser('render', 'render.requested', 'agent:render-node-7', ['jobId' => 'job_9f2'])
144
+ ->withDeliveryMode(DeliveryMode::Processed)
145
+ ->withDeliveryPolicy(new DeliveryPolicy(
146
+ maxAttempts: 5, ttlSeconds: 300, retryBaseMs: 200, retryMaxMs: 2000,
147
+ retryStrategy: RetryStrategy::Exponential,
148
+ resultTimeoutSeconds: 120, // último parâmetro do construtor
149
+ ))
150
+ ->withMetadata(new Metadata(correlationId: 'report-4471', source: 'report-api'));
151
+ $publisher->publish($input);
152
+ ```
153
+
154
+ Dart:
155
+
156
+ ```dart
157
+ await publisher.publish(
158
+ PublishMessageInput.toUser('render', 'render.requested', 'agent:render-node-7', {'jobId': 'job_9f2'})
159
+ .withDeliveryMode(DeliveryMode.processed)
160
+ .withDeliveryPolicy(const DeliveryPolicy(
161
+ maxAttempts: 5, ttlSeconds: 300, retryBaseMs: 200, retryMaxMs: 2000,
162
+ retryStrategy: RetryStrategy.exponential,
163
+ resultTimeoutSeconds: 120,
164
+ ))
165
+ .withCorrelationId('report-4471'),
166
+ );
167
+ ```
168
+
169
+ Rust:
170
+
171
+ ```rust
172
+ publisher.publish(
173
+ PublishMessageInput::to_user("render", "render.requested", "agent:render-node-7", json!({ "jobId": "job_9f2" }))
174
+ .with_delivery_mode(DeliveryMode::Processed)
175
+ .with_delivery_policy(DeliveryPolicy {
176
+ max_attempts: Some(5),
177
+ ttl_seconds: Some(300),
178
+ result_timeout_seconds: Some(120),
179
+ retry_base_ms: Some(200),
180
+ retry_max_ms: Some(2000),
181
+ retry_strategy: Some(RetryStrategy::Exponential),
182
+ }),
183
+ ).await?;
184
+ ```
185
+
186
+ Delphi (campo `0`/`rsUnset` = omitir):
187
+
188
+ ```pascal
189
+ Policy := TDeliveryPolicy.Empty;
190
+ Policy.MaxAttempts := 5;
191
+ Policy.TtlSeconds := 300;
192
+ Policy.ResultTimeoutSeconds := 120;
193
+ Policy.RetryBaseMs := 200;
194
+ Policy.RetryMaxMs := 2000;
195
+ Policy.RetryStrategy := rsExponential;
196
+ R := Publisher.Publish(TPublishMessageInput.ToUser('render', 'render.requested', 'agent:render-node-7', '{"jobId":"job_9f2"}')
197
+ .WithDeliveryMode(dmProcessed)
198
+ .WithDeliveryPolicy(Policy));
199
+ ```
200
+
201
+ Valores inválidos (`0`, `retryMaxMs < retryBaseMs`, estratégia desconhecida) voltam como **422** `INVALID_MESSAGE_REQUEST` e viram a exceção de validação do SDK (por exemplo `PulseValidationException`). Valores acima dos tetos do servidor **não** dão erro: são reduzidos ao teto.
202
+
203
+ `resultTimeoutSeconds` (SDKs a partir do Pulse 0.3.0) é o prazo, em segundos, para o consumidor devolver o `result` depois do `ack`; só vale com `processed`. Omitido, vale o padrão do servidor (`0` = sem prazo). Cuidado: um prazo curto reentrega a tarefa ainda em execução, então use um valor maior que o pior caso e deduplique por `messageId`. Os SDKs não validam o valor (o servidor responde 422 para `0`). Notas por linguagem: PHP recebe o parâmetro **no fim** do construtor de `DeliveryPolicy`; no Rust, literais de `DeliveryPolicy` precisam do campo (ou `..Default::default()`); no Delphi, `ResultTimeoutSeconds = 0` omite o campo; no Java o construtor antigo continua existindo. Semântica completa: [[SeedGrid/Produtos/Pulse/Protocolo-e-API#Prazo de execução (resultTimeoutSeconds)|Prazo de execução]].
204
+
205
+ ## Subscribers (`components/subscribers/`)
206
+
207
+ | Plataforma | Pacote | Versão | Camada de framework |
208
+ |---|---|---|---|
209
+ | React / JS | `@seedgrid/pulse-subscriber-react` (npm) | `3.0.0` | hook `usePulseSubscriber` + core `PulseSubscriberClient` |
210
+ | Angular 17+ | `@seedgrid/pulse-subscriber-angular` | `2.0.0` | `PulseSubscriberService` (Observables) + `providePulseSubscriber()` |
211
+ | Flutter / Dart | `seedgrid_pulse_subscriber` | `2.0.0` | streams `states`, `messages` |
212
+ | Rust | `seedgrid-pulse-subscriber-rust` | `0.2.0` | callbacks `on_message`, `on_state_change` |
213
+ | Delphi | fonte em `src/` (Indy) | — | `IPulseSubscriber` (`NewPulseSubscriberIndy`) |
214
+
215
+ Capacidades comuns: autenticação **só no handshake**; reconexão automática (backoff com jitter); **replay** do backlog ao reconectar (a assinatura é reenviada); `ack` (recebimento) e `reportResult` (execução); deduplicação por `messageId` opcional; `auth-error` ao receber `4403` (sem reconectar).
216
+
217
+ > **Paridade (Confirmado — comparação dos SDKs em 2026-10):** `unsubscribe`, listas `subscribedChannels/Groups` e `autoAck` existem em React, Flutter e Delphi. O **Angular ainda não os tem** (guarda só a última assinatura); o exemplo Angular contorna isso.
218
+
219
+ ### React
220
+
221
+ ```tsx
222
+ const { messages, connectionState, ack } = usePulseSubscriber({
223
+ getToken: async () => (await fetch("/api/realtime/connect", { method: "POST" })).json(),
224
+ subscription: { channels: ["orders"], groups: ["tenant:15"] },
225
+ });
226
+ ```
227
+
228
+ ### Angular
229
+
230
+ ```ts
231
+ providePulseSubscriber(() => {
232
+ const http = inject(HttpClient);
233
+ return {
234
+ getToken: () => firstValueFrom(http.post<PulseConnectToken>("/api/realtime/connect", {})),
235
+ subscription: { channels: ["orders"] },
236
+ };
237
+ });
238
+ // componente: inject(PulseSubscriberService).messages$ / state$ / ack({ messageId })
239
+ ```
240
+
241
+ ### Flutter
242
+
243
+ ```dart
244
+ final subscriber = PulseSubscriber();
245
+ subscriber.messages.listen((m) async { if (m.requiresAck) await subscriber.ack(AckInput(m.messageId)); });
246
+ await subscriber.connect(ConnectInput(getToken: () async => PulseConnectToken.fromJson(await meuBackend.post('/api/realtime/connect'))));
247
+ await subscriber.subscribe(const SubscribeInput(channels: ['orders']));
248
+ ```
249
+
250
+ ### Rust (agente, chave `client`)
251
+
252
+ ```rust
253
+ let subscriber = PulseSubscriber::new();
254
+ subscriber.on_message(|m| println!("{} em {}", m.event, m.channel));
255
+ subscriber.connect(ConnectInput::new("http://localhost:18080", "<chave client>")).await?;
256
+ subscriber.subscribe(SubscribeInput::channels(["orders"])).await?;
257
+ ```
258
+
259
+ ### Delphi
260
+
261
+ ```pascal
262
+ FSub := NewPulseSubscriberIndy;
263
+ FSub.UseAutoAck(True);
264
+ FSub.OnMessage(App.OnMsg);
265
+ FSub.Connect(TConnectInput.CreateWithToken(App.BuscarToken)); // ou TConnectInput.Create(url, chaveClient)
266
+ FSub.Subscribe(TSubscribeInput.ForChannels(['orders']));
267
+ ```
268
+
269
+ ## Fluxo de ponta a ponta (navegador)
270
+
271
+ ```text
272
+ navegador ──(sessão)──▶ POST /api/realtime/connect [rota do SEU backend]
273
+ │ issueConnectToken(subject = usuário da sessão) [chave project]
274
+ ▼
275
+ navegador ◀── { token, expiresAt, connectUrl }
276
+ navegador ──WebSocket (access_token=pct_…)──▶ Pulse ──▶ message / ack / result
277
+ ```
278
+
279
+ ## Ack × result
280
+
281
+ - `ack` confirma o **recebimento**; `result` informa o **resultado de execução** (`success`/`retryable`/`permanent`).
282
+ - Só mensagens `processed` esperam `result`; em `notification`/`acknowledged` o servidor o recusa com `result_not_applicable`. Com `autoAck`/auto-ack nos SDKs que o têm, o SDK envia `ack` e, para `processed`, `result:success` depois que o seu handler retorna sem erro.
283
+ - Entrega é at-least-once: **deduplique por `messageId`**.
284
+
285
+ ## Instalação a partir do fonte
286
+
287
+ Nem todos os SDKs estão em registro público (Confirmado nos commits: Java em Maven Central, React no npm). Para os demais use dependência por caminho/cópia do fonte, como fazem os [[SeedGrid/Produtos/Pulse/Exemplos|exemplos]] (ex.: Flutter com `path: sdk/subscriber`). Cada SDK traz `INSTALLATION.md`, `CONFIGURATION.md`, `SECURITY.md`, `TROUBLESHOOTING.md` e `TESTING.md`.
@@ -0,0 +1,128 @@
1
+ ---
2
+ tags: [seedgrid, produto, pulse, servidor, configuracao, variaveis]
3
+ fonte: seedgrid-products/seedgrid-pulse/backend (src/config.rs, env.md)
4
+ branch: main
5
+ commit: 9dcbdad
6
+ analisado-em: 2026-10-16
7
+ ---
8
+
9
+ # Pulse (servidor) — variáveis de ambiente
10
+
11
+ Todas as variáveis do **servidor**. As da UI estão em [[SeedGrid/Produtos/Pulse/UI/Configuracao|Configuração da UI]]. Arquitetura atual: **login-tenancy, banco único** — a separação entre clientes é lógica, por `company_id`. Sem `POSTGRES_URL` o servidor sobe em modo memória/dev com a plataforma desabilitada.
12
+
13
+ Convenção: 🔒 = segredo (nunca em repositório/log); valores de exemplo são placeholders, **não** padrões.
14
+
15
+ ## Runtime (HTTP, WebSocket, CORS)
16
+
17
+ | Variável | Padrão | Descrição |
18
+ |---|---|---|
19
+ | `HTTP_HOST` | `127.0.0.1` | IP de bind. Em container use `0.0.0.0`. |
20
+ | `HTTP_PORT` | `8080` | Porta HTTP. |
21
+ | `WS_PATH` | `/ws` | Path do WebSocket legado (o endpoint do protocolo é `/api/v1/connect`). |
22
+ | `CORS_ALLOWED_ORIGINS` | vazio | Lista separada por vírgula. Vazio = nenhuma origem liberada. |
23
+ | `WS_HEARTBEAT_INTERVAL_SECONDS` | `30` | Heartbeat do WebSocket. |
24
+ | `WS_IDLE_TIMEOUT_SECONDS` | `90` | Fecha conexão ociosa. |
25
+
26
+ ## Credenciais do Pulse 🔒
27
+
28
+ | Variável | Padrão | Descrição |
29
+ |---|---|---|
30
+ | `SEEDGRID_PULSE_SERVER_KEY` | — | Chave **administrativa** (contas/chaves, métricas) em `x-api-key`. **Não publica** (403). Só em ENV; fica só com a operação do Pulse. |
31
+ | `SEEDGRID_PULSE_CONNECT_TOKEN_SECRET` | — | Segredo HMAC-SHA256 que assina o token curto de conexão. Sem ele, `POST /api/v1/connect-tokens` responde 503. |
32
+ | `SEEDGRID_PULSE_CONNECT_TOKEN_PREVIOUS_SECRET` | — | Segredo anterior, aceito só na verificação (rotação sem derrubar tokens válidos). |
33
+ | `SEEDGRID_PULSE_CONNECT_TOKEN_TTL_SECS` | `300` | Validade do token quando o pedido não informa `ttlSeconds`. É a janela para **conectar**. |
34
+ | `SEEDGRID_PULSE_CONNECT_TOKEN_MAX_TTL_SECS` | `900` | Teto: pedido acima → 422. |
35
+ | `SEEDGRID_PULSE_CONNECT_TOKEN_RATE_PER_KEY` | `600` | Emissões/min por chave `project`; `0` desliga. Por instância. |
36
+ | `SEEDGRID_PULSE_CONNECT_TOKEN_RATE_PER_SUBJECT` | `30` | Emissões/min por `subject` na mesma chave; `0` desliga. Por instância. |
37
+ | `SEEDGRID_PULSE_PUBLIC_WS_URL` | — | Endereço **público** do WebSocket devolvido em `connectUrl`. Ausente = campo omitido. |
38
+
39
+ Quem usa cada credencial: [[SeedGrid/Produtos/Pulse/Autenticacao-e-chaves|Autenticação e chaves]].
40
+
41
+ ## Persistência e mensageria
42
+
43
+ | Variável | Padrão | Descrição |
44
+ |---|---|---|
45
+ | `POSTGRES_URL` | — | **Interruptor da plataforma.** Sem ela, security/login-tenancy/audit/apikeys ficam desligados. 🔒 |
46
+ | `POSTGRES_RUN_MIGRATIONS` | `true` | Roda migrations na subida. |
47
+ | `DELIVERY_STORE_BACKEND` | `memory` | `memory` \| `postgres`. Em produção, `postgres`. |
48
+ | `BROKER_PROVIDER` | `memory` | `memory` \| `redis` \| `nats` \| `rabbitmq` (fallback `EVENT_BUS_BACKEND`). Valor desconhecido cai em `memory`. Detalhes e variáveis por provider: [[SeedGrid/Produtos/Pulse/Servidor/Providers|Providers]]. |
49
+ | `PRESENCE_BACKEND` | `memory` | `memory` \| `redis`. Multi-réplica ⇒ `redis`. |
50
+ | `BROKER_REDIS_URL` | — | URL Redis-compatível (fallback `REDIS_URL`). 🔒 |
51
+ | `BROKER_REDIS_CHANNEL` | `seedgrid:pulse:events` | Canal pub/sub (fallback `REDIS_PUBSUB_CHANNEL`). |
52
+ | `BROKER_NATS_URL` | — | URL do NATS (`nats://host:4222`; fallback `NATS_URL`). Obrigatória com `nats`. 🔒 |
53
+ | `BROKER_NATS_SUBJECT` | `seedgrid.pulse.events` | Subject do pub/sub. |
54
+ | `BROKER_RABBITMQ_URL` | — | URL AMQP (fallbacks `RABBITMQ_URL`, `AMQP_URL`). Obrigatória com `rabbitmq`. 🔒 |
55
+ | `BROKER_RABBITMQ_EXCHANGE` | `seedgrid.pulse.events` | Exchange fanout. |
56
+
57
+ As URLs Redis-compatíveis servem para Valkey e Dragonfly sem mudar mais nada; ver [[SeedGrid/Produtos/Pulse/Servidor/Providers|Providers]].
58
+
59
+ ## Entrega (retry, TTL, limpeza)
60
+
61
+ Os pares `*_CAP` são o **teto** que uma configuração por mensagem pode pedir; omitidos, assumem o valor base. Todos (exceto o intervalo de limpeza) são editáveis em runtime em **Configurações**.
62
+
63
+ | Variável | Padrão | Descrição |
64
+ |---|---|---|
65
+ | `DELIVERY_RETRY_STRATEGY` | `exponential` | `exponential` (qualquer outro valor = `fixed`: atraso constante de `DELIVERY_RETRY_BASE_MS`). |
66
+ | `DELIVERY_RETRY_BASE_MS` | `300` | Backoff base. |
67
+ | `DELIVERY_RETRY_MAX_MS` | `5000` | Backoff máximo. |
68
+ | `DELIVERY_MAX_ATTEMPTS` | `20` | Tentativas de execução por mensagem (modo `processed`). |
69
+ | `DELIVERY_TTL_SECONDS` | `86400` | TTL da mensagem (1 dia). |
70
+ | `DELIVERY_RESULT_TIMEOUT_SECONDS` | `0` (sem prazo) | **Prazo de execução** padrão (modo `processed`): segundos entre o `ack` e o `result`. `0` desliga. Estourado, vale como `retryable` com motivo `result_timeout`. Editável em Configurações, sem reinício. |
71
+ | `DELIVERY_RESULT_TIMEOUT_SECONDS_CAP` | `86400` (ou o padrão, se maior) | Teto do prazo que o publisher pode pedir; `0` **proíbe** o prazo de execução. |
72
+ | `DELIVERY_RETRY_BASE_MS_CAP` / `DELIVERY_RETRY_MAX_MS_CAP` / `DELIVERY_MAX_ATTEMPTS_CAP` / `DELIVERY_TTL_SECONDS_CAP` | = valor base | Tetos para o override por mensagem. |
73
+ | `DELIVERY_CLEANUP_INTERVAL_SECONDS` | `60` | Intervalo da varredura de retenção (`DELIVERY_CLEANUP_RETENTION_DAYS`) e da limpeza automática da lixeira. **Não** controla a expiração por TTL, que tem verificador próprio (a cada `ceil(DELIVERY_RETRY_BASE_MS/1000)` s, mínimo 1 s). Exige reinício. |
74
+ | `DELIVERY_CLEANUP_RETENTION_DAYS` | `0` (nunca apaga) | **Dias** que uma mensagem em estado final (`completed`/`dead_letter`/`expired`) é mantida. `0` desliga a varredura: o histórico é prova. Obsoleta: `DELIVERY_CLEANUP_RETENTION_SECONDS` (lida só se a de dias não existir). |
75
+ | `DELIVERY_TRASH_AUTO_CLEAN_DAYS` | `0` (só manual) | Dias na lixeira antes da limpeza automática. Cada limpeza vai ao audit como `system:pulse`. |
76
+
77
+ Detalhes e as regras de lixeira: [[SeedGrid/Produtos/Pulse/Servidor/Operacao|Operação]].
78
+
79
+ ## Security (login, JWT, RBAC)
80
+
81
+ Habilitado com `POSTGRES_URL` **e** as chaves RSA. Nomes alinhados à extension Java `security` ([[SeedGrid/Extensions/security/Configuracao|Configuração do security]]).
82
+
83
+ | Variável | Descrição |
84
+ |---|---|
85
+ | `SECURITY_PRIVATE_KEY_LOCATION` / `SECURITY_PUBLIC_KEY_LOCATION` | Caminhos do par RSA (primários). 🔒 |
86
+ | `SECURITY_JWT_PRIVATE_KEY_PEM` / `SECURITY_JWT_PUBLIC_KEY_PEM` | PEM inline (secundário). 🔒 |
87
+ | `SECURITY_JWT_ISSUER` / `SECURITY_JWT_KID` | Issuer e `kid` do JWT. |
88
+ | `SECURITY_JWT_TTL` | Validade do access token, ISO-8601 (`PT8H`). |
89
+ | `SECURITY_REFRESH_TOKEN_TTL` | Validade do refresh token (`P30D`). |
90
+ | `SECURITY_PASSWORD_MIN_SIZE` / `_EXPIRATION_DAYS` / `_MAX_HISTORY` | Política de senha. |
91
+ | `SECURITY_ENCRYPTION_KEY` | AES-256 (base64) da cripto de campo; obrigatória se usar TOTP. 🔒 |
92
+ | `SECURITY_FRONTEND_BASE_URL` | Base da UI nos links de e-mail. |
93
+ | `SEEDGRID_SECURITY_FIRST_ADMIN_USER_EMAIL` / `_NAME` / `_INITIAL_PASSWORD` | Bootstrap do primeiro admin. 🔒 |
94
+ | `SEEDGRID_SECURITY_DEFAULT_COMPANY_CNPJ` / `_CORPORATE_NAME` / `_TRADE_NAME` | Company da plataforma (razão social e fantasia são obrigatórias no bootstrap). |
95
+
96
+ ### 2FA
97
+
98
+ Desligado por padrão. `SEEDGRID_SECURITY_2F_ENABLED=true` liga; `_ALLOWED_METHODS` (`EMAIL,TOTP`), `_DEFAULT_METHOD` (`TOTP`), `_EMAIL_CODE_TTL`, `_SESSION_TTL`, `_VERIFY_MAX_ATTEMPTS` (5), `_TOTP_ISSUER` (`SeedGrid`; vale trocar para `SeedGrid Pulse`), `_TOTP_DIGITS` (6), `_TOTP_PERIOD` (30), `_TOTP_WINDOW` (1), `_BACKUP_CODES_COUNT` (10). WhatsApp: `SEEDGRID_SECURITY_2F_WHATSAPP_CHANNEL_ID` mais um provider em `SEEDGRID_WHATSAPP_API` (`evolution`/`cloud`) com credenciais ([[SeedGrid/Extensions/whatsapp/Configuracao|whatsapp]]).
99
+
100
+ ### E-mail (SMTP)
101
+
102
+ `SECURITY_MAILER_USERNAME` e `_PASSWORD` ligam o SMTP real (🔒); `_HOST` (`smtp.gmail.com`), `_PORT` (587), `_START_TLS` (`REQUIRED`), `_FROM`. **Sem usuário e senha o e-mail só vai para o log** — confirmação, reset de senha e 2FA por e-mail não chegam.
103
+
104
+ ## Login-tenancy
105
+
106
+ `SEEDGRID_LOGIN_TENANCY_APP_CODE` (`seedgrid-app`; use `seedgrid-pulse`), `_APP_NAME`, `_DATABASE_MIGRATE_AT_START` (`true`), `_DATABASE_BASELINE_ON_MIGRATE` (`true`), `_FOUNDATION_STATUS_ENABLED`, `_FOUNDATION_HEALTH_ENABLED`, `_BOOTSTRAP_ENABLED`, `_BOOTSTRAP_ADMIN_ENABLED`. Não existe flag "enabled": a dependência ativa o baseline. `SEEDGRID_SHOW_DEV_MESSAGE` (`false`) inclui detalhe interno nos erros — **nunca em produção**. Ver [[SeedGrid/Extensions/login-tenancy/Configuracao|login-tenancy]].
107
+
108
+ ## Audit
109
+
110
+ `SEEDGRID_AUDIT_ENABLED` (`true`), `AUDIT_STRATEGY` (`JPA` = Postgres \| `MONGODB`), `SEEDGRID_AUDIT_DB_*` só para banco de auditoria separado, `SEEDGRID_AUDIT_MONGODB_URI`/`_DATABASE`. No deploy padrão as migrations de audit rodam no mesmo banco. A leitura é escopada por `company_id` via headers internos sobrescritos pelo JWT — não os repasse de proxy. Ver [[SeedGrid/Extensions/audit/Configuracao|audit]].
111
+
112
+ ## API keys
113
+
114
+ `SEEDGRID_APIKEYS_PREFIX_LENGTH` (`8`), `SEEDGRID_APIKEYS_KEY_BYTES` (`32`). Ver [[SeedGrid/Extensions/apikeys/Configuracao|apikeys]].
115
+
116
+ ## Observabilidade
117
+
118
+ `RUST_LOG` (ex.: `info`). Métricas: `GET /metrics` (JSON) e `GET /metrics/prometheus`; Prometheus e Grafana vêm no compose.
119
+
120
+ ## Variáveis removidas (não configure)
121
+
122
+ As de multitenancy por subdomínio (`DOMAIN_SUFFIX`, `SEEDGRID_MULTITENANCY_*`, `PRODUCT_PREFIX`, `MULTITENANCY_MAILER_*`) foram eliminadas e são ignoradas. Backup (`BACKUP_*`) e billing (`SEEDGRID_BILLING_*`) não se aplicam ao Pulse hoje.
123
+
124
+ ## Pegadinhas
125
+
126
+ - A chave de servidor nunca vai para produto nenhum nem para o serviço da UI.
127
+ - Variável definida no EasyPanel **vence** o padrão de código: ao mudar um padrão, confira o ambiente.
128
+ - `0` em retenção/limpeza significa **desligado** (não "apagar tudo").