@seedgrid/ai-toolkit 0.3.1 → 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.
- package/README.md +3 -3
- package/dist/adapters/shared.js +12 -7
- package/dist/cli.js +1 -1
- package/dist/commands/review.js +2 -2
- package/dist/core/plan.js +5 -0
- package/docs/arquitetura.md +2 -2
- package/docs/compatibilidade.md +1 -1
- package/docs/formato.md +4 -4
- package/package.json +1 -1
- package/seedgridKB/SeedGrid/Extensions/Catalogo-de-propriedades.md +3 -2
- package/seedgridKB/SeedGrid/Extensions/Indice.md +1 -0
- package/seedgridKB/SeedGrid/Extensions/ai-pricing/Configuracao.md +11 -18
- package/seedgridKB/SeedGrid/Extensions/ai-pricing/Exemplos.md +2 -2
- package/seedgridKB/SeedGrid/Extensions/ai-pricing/Visao-geral.md +2 -2
- package/seedgridKB/SeedGrid/Extensions/business-days/Configuracao.md +36 -0
- package/seedgridKB/SeedGrid/Extensions/business-days/Exemplos.md +37 -0
- package/seedgridKB/SeedGrid/Extensions/business-days/Visao-geral.md +48 -0
- package/seedgridKB/SeedGrid/Extensions/exchange/Configuracao.md +10 -16
- package/seedgridKB/SeedGrid/Extensions/exchange/Exemplos.md +2 -2
- package/seedgridKB/SeedGrid/Extensions/exchange/Visao-geral.md +2 -2
- package/seedgridKB/SeedGrid/Extensions/login-tenancy/Configuracao.md +4 -5
- package/seedgridKB/SeedGrid/Extensions/login-tenancy/Visao-geral.md +1 -1
- package/seedgridKB/SeedGrid/Extensions/subdomain-tenancy/Configuracao.md +3 -2
- package/seedgridKB/SeedGrid/Extensions/subdomain-tenancy/Visao-geral.md +2 -2
- package/seedgridKB/SeedGrid/Mapa-dos-repositorios.md +9 -0
- package/seedgridKB/SeedGrid/Produtos/Pulse/Autenticacao-e-chaves.md +89 -0
- package/seedgridKB/SeedGrid/Produtos/Pulse/Exemplos.md +44 -0
- package/seedgridKB/SeedGrid/Produtos/Pulse/Protocolo-e-API.md +191 -0
- package/seedgridKB/SeedGrid/Produtos/Pulse/SDKs.md +287 -0
- package/seedgridKB/SeedGrid/Produtos/Pulse/Servidor/Configuracao.md +128 -0
- package/seedgridKB/SeedGrid/Produtos/Pulse/Servidor/Instalacao.md +105 -0
- package/seedgridKB/SeedGrid/Produtos/Pulse/Servidor/Operacao.md +86 -0
- package/seedgridKB/SeedGrid/Produtos/Pulse/Servidor/Providers.md +121 -0
- package/seedgridKB/SeedGrid/Produtos/Pulse/UI/Configuracao.md +61 -0
- package/seedgridKB/SeedGrid/Produtos/Pulse/UI/Visao-geral.md +74 -0
- package/seedgridKB/SeedGrid/Produtos/Pulse/Visao-geral.md +96 -0
- package/seedgridKB/SeedGrid/Toolkit/agents/arquiteto.md +1 -1
- package/seedgridKB/SeedGrid/Toolkit/agents/backend-java.md +2 -2
- package/seedgridKB/SeedGrid/Toolkit/agents/backend-rust.md +1 -1
- package/seedgridKB/SeedGrid/Toolkit/agents/frontend-react.md +1 -1
- package/seedgridKB/SeedGrid/Toolkit/agents/qa.md +2 -2
- package/seedgridKB/SeedGrid/Toolkit/agents/revisor.md +2 -2
- package/seedgridKB/SeedGrid/Toolkit/references/fluxo-de-trabalho.md +3 -3
- package/seedgridKB/SeedGrid/Toolkit/rules/backend-java-seedgrid.md +27 -3
- package/seedgridKB/SeedGrid/Toolkit/rules/coordenacao.md +1 -1
- package/seedgridKB/SeedGrid/Toolkit/rules/documentos.md +26 -0
- package/seedgridKB/SeedGrid/Toolkit/rules/frontend-seedgrid.md +2 -2
- package/seedgridKB/SeedGrid/Toolkit/skills/{seedgrid-bump → sg-bump}/SKILL.md +1 -1
- package/seedgridKB/SeedGrid/Toolkit/skills/{revisar-codigo → sg-code-review}/SKILL.md +2 -2
- package/seedgridKB/SeedGrid/Toolkit/skills/{seedgrid-backend-crud → sg-crud-back}/SKILL.md +4 -4
- package/seedgridKB/SeedGrid/Toolkit/skills/{seedgrid-crud → sg-crud-front}/SKILL.md +5 -5
- package/seedgridKB/SeedGrid/Toolkit/skills/{seedgrid-crud → sg-crud-front}/agents/openai.yaml +1 -1
- package/seedgridKB/SeedGrid/Toolkit/skills/{seedgrid-crud/references/routing.md → sg-crud-front/references/crud-front-routing.md} +1 -1
- package/seedgridKB/SeedGrid/Toolkit/skills/{seedgrid-new-screen → sg-new-screen}/SKILL.md +3 -3
- package/seedgridKB/SeedGrid/Toolkit/skills/sg-new-screen/agents/openai.yaml +4 -0
- package/seedgridKB/SeedGrid/Toolkit/skills/sg-new-skill/SKILL.md +50 -0
- package/seedgridKB/SeedGrid/Toolkit/skills/{planejar-mudanca → sg-plan-change}/SKILL.md +1 -1
- package/seedgridKB/SeedGrid/Toolkit/skills/{seedgrid-review → sg-review}/SKILL.md +1 -1
- package/seedgridKB/SeedGrid/Toolkit/skills/{validar-mudanca → sg-validate-change}/SKILL.md +1 -1
- package/seedgridKB/SeedGrid/Visao-geral.md +7 -0
- package/seedgridKB/manifest.json +32 -23
- package/seedgridKB/SeedGrid/Toolkit/skills/seedgrid-new-screen/agents/openai.yaml +0 -4
- /package/seedgridKB/SeedGrid/Toolkit/skills/{revisar-codigo/references/checklist.md → sg-code-review/references/code-review-checklist.md} +0 -0
- /package/seedgridKB/SeedGrid/Toolkit/skills/{seedgrid-crud/references/checklist.md → sg-crud-front/references/crud-front-checklist.md} +0 -0
- /package/seedgridKB/SeedGrid/Toolkit/skills/{seedgrid-crud/references/patterns.md → sg-crud-front/references/crud-front-patterns.md} +0 -0
- /package/seedgridKB/SeedGrid/Toolkit/skills/{seedgrid-new-screen/references/checklist.md → sg-new-screen/references/new-screen-checklist.md} +0 -0
- /package/seedgridKB/SeedGrid/Toolkit/skills/{seedgrid-new-screen/references/steps.md → sg-new-screen/references/new-screen-steps.md} +0 -0
|
@@ -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").
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
---
|
|
2
|
+
tags: [seedgrid, produto, pulse, servidor, instalacao, deploy]
|
|
3
|
+
fonte: seedgrid-products/seedgrid-pulse/backend
|
|
4
|
+
branch: main
|
|
5
|
+
commit: 9dcbdad
|
|
6
|
+
analisado-em: 2026-10-16
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Pulse (servidor) — instalação e execução
|
|
10
|
+
|
|
11
|
+
O **servidor Pulse** é o backend Rust (Axum): API REST, WebSocket, entrega, retry, DLQ, presença e módulos de plataforma. A UI é outro componente: [[SeedGrid/Produtos/Pulse/UI/Visao-geral|Pulse UI]].
|
|
12
|
+
|
|
13
|
+
Voltar: [[SeedGrid/Produtos/Pulse/Visao-geral|Visão geral]] · variáveis: [[SeedGrid/Produtos/Pulse/Servidor/Configuracao|Configuração do servidor]] · brokers: [[SeedGrid/Produtos/Pulse/Servidor/Providers|Providers (Redis, Valkey, Dragonfly, NATS, RabbitMQ)]]
|
|
14
|
+
|
|
15
|
+
## Modos de execução
|
|
16
|
+
|
|
17
|
+
| Modo | Quando | Persistência |
|
|
18
|
+
|---|---|---|
|
|
19
|
+
| **Memória** (sem `POSTGRES_URL`) | desenvolvimento rápido, testes | nenhuma; plataforma (login, audit, apikeys) **desabilitada** — só o núcleo realtime |
|
|
20
|
+
| **Postgres** | produção de instância única | mensagens, contas, usuários, audit |
|
|
21
|
+
| **Distribuído** | várias réplicas | Postgres + broker pub/sub + presença em Redis |
|
|
22
|
+
|
|
23
|
+
Exemplos de `env` (Confirmado — `backend/README.md`):
|
|
24
|
+
|
|
25
|
+
```env
|
|
26
|
+
# simples
|
|
27
|
+
BROKER_PROVIDER=memory
|
|
28
|
+
PRESENCE_BACKEND=memory
|
|
29
|
+
DELIVERY_STORE_BACKEND=memory
|
|
30
|
+
|
|
31
|
+
# com Postgres
|
|
32
|
+
DELIVERY_STORE_BACKEND=postgres
|
|
33
|
+
POSTGRES_URL=postgres://<user>:<senha>@<host>:5432/<db>
|
|
34
|
+
|
|
35
|
+
# distribuído
|
|
36
|
+
BROKER_PROVIDER=redis
|
|
37
|
+
PRESENCE_BACKEND=redis
|
|
38
|
+
DELIVERY_STORE_BACKEND=postgres
|
|
39
|
+
BROKER_REDIS_URL=redis://<host>:6379/0
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Subir com Docker/Podman Compose
|
|
43
|
+
|
|
44
|
+
Em `backend/`:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
docker compose up -d --build
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Sobe `postgres`, `redis`, `seedgrid-pulse`, `prometheus`, `grafana` e `seedgrid-pulse-ui`. Portas locais (Confirmado — `docker-compose.yml`): API `18080`, Prometheus `19090`, Grafana `13001`, UI `13000`.
|
|
51
|
+
|
|
52
|
+
O compose de desenvolvimento traz valores de exemplo (chave de servidor, segredo do token, admin inicial). **Troque todos** em qualquer ambiente real. 🔒
|
|
53
|
+
|
|
54
|
+
## Rodar a partir do fonte
|
|
55
|
+
|
|
56
|
+
Pré-requisitos: Rust estável recente (edition 2024) e o repositório de crates da plataforma clonado ao lado (path dependencies).
|
|
57
|
+
|
|
58
|
+
```text
|
|
59
|
+
<raiz>/
|
|
60
|
+
├─ seedgrid-platform/rust/ # crates seedgrid-rust-crate-*
|
|
61
|
+
└─ seedgrid-products/seedgrid-pulse/ # este repo
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
O prefixo `../../../seedgrid-platform/rust/` está em `backend/Cargo.toml`; ajuste-o se o layout for outro.
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
cd backend
|
|
68
|
+
docker compose up -d postgres redis
|
|
69
|
+
cargo run
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
O build não precisa de banco (não há `sqlx::query!` verificada em compilação).
|
|
73
|
+
|
|
74
|
+
## Primeiro acesso
|
|
75
|
+
|
|
76
|
+
Com `POSTGRES_URL` e o par RSA do JWT, o boot roda as migrations e cria a company da plataforma e o **primeiro admin** a partir de `SEEDGRID_SECURITY_FIRST_ADMIN_USER_*` e `SEEDGRID_SECURITY_DEFAULT_COMPANY_*` ([[SeedGrid/Produtos/Pulse/Servidor/Configuracao|Configuração]]). Entre na UI com esse admin, crie a **conta** do seu produto e as **chaves** (Contas → conta → Chaves → Nova chave). O segredo da chave aparece **uma única vez**.
|
|
77
|
+
|
|
78
|
+
Par RSA do JWT (exemplo):
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out private.pem
|
|
82
|
+
openssl rsa -pubout -in private.pem -out public.pem
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Aponte `SECURITY_PRIVATE_KEY_LOCATION` / `SECURITY_PUBLIC_KEY_LOCATION` (ou use PEM inline).
|
|
86
|
+
|
|
87
|
+
## Imagem e deploy
|
|
88
|
+
|
|
89
|
+
- Imagem no GHCR: `ghcr.io/seedgrid/seedgrid-pulse-backend` (Confirmado — `.github/workflows/deploy.yml`).
|
|
90
|
+
- `push` em `main` dispara o workflow, que detecta o que mudou, publica a imagem e chama o webhook do EasyPanel (secret `EASYPANEL_DEPLOY_URL_PULSE_BACKEND`). Commit só de documentação **não** redeploya.
|
|
91
|
+
- O container escuta `8080`; use `HTTP_HOST=0.0.0.0` em container.
|
|
92
|
+
- Probes: liveness `GET /health`, readiness `GET /ready` (200/503 com o estado de store, broker e presença).
|
|
93
|
+
- Migrations rodam no boot (`POSTGRES_RUN_MIGRATIONS`, padrão `true`).
|
|
94
|
+
|
|
95
|
+
## Checklist de produção
|
|
96
|
+
|
|
97
|
+
- [ ] `POSTGRES_URL` + `DELIVERY_STORE_BACKEND=postgres`.
|
|
98
|
+
- [ ] Mais de uma réplica ⇒ broker pub/sub (`redis`, ou `nats`/`rabbitmq` — ver [[SeedGrid/Produtos/Pulse/Servidor/Providers|Providers]]) **e** `PRESENCE_BACKEND=redis`.
|
|
99
|
+
- [ ] `SEEDGRID_PULSE_SERVER_KEY` e `SEEDGRID_PULSE_CONNECT_TOKEN_SECRET` longos e aleatórios, só em ENV. 🔒
|
|
100
|
+
- [ ] `SEEDGRID_PULSE_PUBLIC_WS_URL` com o endereço público do WebSocket (`wss://…`), se há clientes de navegador.
|
|
101
|
+
- [ ] `CORS_ALLOWED_ORIGINS` com a origem da UI.
|
|
102
|
+
- [ ] Par RSA do JWT próprio; admin inicial trocado.
|
|
103
|
+
- [ ] `SEEDGRID_SHOW_DEV_MESSAGE` desligado (padrão).
|
|
104
|
+
- [ ] Proxy reverso **não** repassa `x-seedgrid-company-id` / `x-seedgrid-all-companies` vindos do cliente.
|
|
105
|
+
- [ ] Retenção do histórico decidida (padrão: nunca apagar) — [[SeedGrid/Produtos/Pulse/Servidor/Operacao|Operação]].
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
---
|
|
2
|
+
tags: [seedgrid, produto, pulse, servidor, operacao, lixeira, retencao]
|
|
3
|
+
fonte: seedgrid-products/seedgrid-pulse/backend (docs/LIXEIRA-mensagens-e-contas.md, README.md)
|
|
4
|
+
branch: main
|
|
5
|
+
commit: 9dcbdad
|
|
6
|
+
analisado-em: 2026-10-16
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# Pulse (servidor) — operação
|
|
10
|
+
|
|
11
|
+
Lixeira, retenção, auditoria, observabilidade e escala. Voltar: [[SeedGrid/Produtos/Pulse/Visao-geral|Visão geral]] · variáveis: [[SeedGrid/Produtos/Pulse/Servidor/Configuracao|Configuração]].
|
|
12
|
+
|
|
13
|
+
## Princípios
|
|
14
|
+
|
|
15
|
+
- O **histórico de entregas é prova**: por padrão **nunca é apagado sozinho**.
|
|
16
|
+
- Apagar é **sempre ação humana**, com permission própria, confirmação na UI e **audit** (quem, o quê, quantos).
|
|
17
|
+
- A chave de servidor **não apaga nada** (401): toda exclusão tem um autor.
|
|
18
|
+
|
|
19
|
+
## Lixeira de mensagens
|
|
20
|
+
|
|
21
|
+
- **Só mensagens `pending`** podem ir para a lixeira (mensagens entregues/processadas são histórico; só saem com a exclusão da conta).
|
|
22
|
+
- Na UI: **Mensagens**, filtro de status **"Na lixeira"**. Mover (`PULSE_MESSAGE_DELETE`), restaurar (volta a `pending`) e limpar de vez (`PULSE_MESSAGE_PURGE`, apaga também as entregas por destinatário).
|
|
23
|
+
- API: `POST /api/v1/messages/trash` · `/trash/restore` · `/trash/purge`, lote de até **1000** ids (`400 BATCH_TOO_LARGE`), resposta `{requested, affected, skipped}`.
|
|
24
|
+
- Mensagem na lixeira não é entregue, não expira por TTL e some das listas padrão.
|
|
25
|
+
- **Descartar** (tela Pendências, `PULSE_BACKLOG_DISCARD`) e **Lixeira** coexistem como ações distintas; só a lixeira tem restaurar e limpar de vez.
|
|
26
|
+
- Tarefas `delivered` sem `result` e sem prazo de execução (as anteriores ao Pulse 0.3.0, ou quando `resultTimeoutSeconds` não foi usado) só saem por **descarte**; veja [[SeedGrid/Produtos/Pulse/Protocolo-e-API#Prazo de execução (resultTimeoutSeconds)|Prazo de execução]].
|
|
27
|
+
|
|
28
|
+
## Lixeira de contas
|
|
29
|
+
|
|
30
|
+
- A lixeirinha da lista de contas pede confirmação e leva a conta para `pending_delete` (`PULSE_ACCOUNT_DELETE`). A conta **deixa de autenticar** e suas conexões são derrubadas.
|
|
31
|
+
- **Lixeira de contas** (menu): restaurar (volta ao status anterior) ou **apagar definitivamente** (`PULSE_ACCOUNT_PURGE`), que remove a conta, suas chaves e **todas as suas mensagens** em cascata. O diálogo mostra antes quantas chaves e mensagens saem (`GET /accounts/{id}/deletion-summary`).
|
|
32
|
+
- O apagar definitivo **não é uma transação única**: mensagens primeiro, conta por último. Se falhar no meio, a conta continua na lixeira e dá para repetir.
|
|
33
|
+
|
|
34
|
+
## Retenção e limpeza automática
|
|
35
|
+
|
|
36
|
+
| Variável | Padrão | Efeito |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| `DELIVERY_CLEANUP_RETENTION_DAYS` | `0` | Dias que uma mensagem **final** (`completed`/`dead_letter`/`expired`) é mantida. `0` = **nunca apaga**. |
|
|
39
|
+
| `DELIVERY_TRASH_AUTO_CLEAN_DAYS` | `0` | Dias na lixeira antes de apagar sozinho. `0` = só manual. Cada limpeza vai ao audit como `system:pulse`. |
|
|
40
|
+
|
|
41
|
+
As duas **não se sobrepõem**: a primeira atua no histórico (estados finais), a segunda na lixeira (`trashed`). Mensagens `pending` e `delivered` nunca são limpas pela retenção. A obsoleta `DELIVERY_CLEANUP_RETENTION_SECONDS` só vale se a de dias não existir. Ambas são editáveis em **Configurações**, sem redeploy.
|
|
42
|
+
|
|
43
|
+
> **Mudança de comportamento (2026-10-15):** o padrão era 7 dias e `0` apagava tudo. Ambientes que definem a variável explicitamente (EasyPanel) mantêm o valor definido.
|
|
44
|
+
|
|
45
|
+
Sem consumidor, uma `pending` não fica para sempre: o TTL (`DELIVERY_TTL_SECONDS`, 1 dia) a expira.
|
|
46
|
+
|
|
47
|
+
## Ordem das listas e paginação
|
|
48
|
+
|
|
49
|
+
As telas listam **da mais recente para a mais antiga** (quem acabou de publicar vê a sua primeira): contas por `created_at`, mensagens por `published_at` e entregas por destinatário por `delivered_at`, todas **com desempate por id**. A paginação é 1-based (`page`, `pageSize`; a Auditoria usa `page`/`size` e o backend dela é base 0, convertido pela UI). Até o Pulse 0.2.1 a lista de contas ordenava sem desempate, e contas criadas no mesmo segundo repetiam ou sumiam entre páginas (corrigido no 0.2.2). O **replay** e as pendências devidas continuam em ordem cronológica (do mais antigo ao mais novo), porque entregar na ordem de publicação é o comportamento certo (Confirmado em `delivery/repo.rs`).
|
|
50
|
+
|
|
51
|
+
## Auditoria
|
|
52
|
+
|
|
53
|
+
Toda operação destrutiva (mover para lixeira, restaurar, limpar; contas e mensagens) grava em `audit_log` o autor (claim `sub` do JWT, e-mail na descrição), a ação e as contagens, **somente se algo mudou**. Visível em **Segurança → Auditoria** (escopo por `company_id`).
|
|
54
|
+
|
|
55
|
+
## Observabilidade
|
|
56
|
+
|
|
57
|
+
| O quê | Onde |
|
|
58
|
+
|---|---|
|
|
59
|
+
| Liveness / readiness | `GET /health` · `GET /ready` (200 `READY` / 503 `NOT_READY`, com store, broker e presença) |
|
|
60
|
+
| Métricas | `GET /metrics` (JSON) · `GET /metrics/prometheus` |
|
|
61
|
+
| Dashboards | Prometheus + Grafana no compose (`SeedGrid Pulse Overview`) |
|
|
62
|
+
| Telas | dashboard, conexões, backlog, DLQ, mensagens, entregas por destinatário |
|
|
63
|
+
|
|
64
|
+
Métricas úteis: `publish_requests_total`, `delivery_attempts_total`, `delivery_success_total`, `delivery_dead_letter_total`, `delivery_expired_total`, `delivery_result_timeout_total` (prazo de execução estourado), `ack_total`, `ws_connections_opened_total`/`_closed_total`, latências `publish_to_deliver_*` e `deliver_to_ack_*`.
|
|
65
|
+
|
|
66
|
+
## Escala (várias réplicas)
|
|
67
|
+
|
|
68
|
+
- Estado durável no **Postgres** (`DELIVERY_STORE_BACKEND=postgres`).
|
|
69
|
+
- Entrega entre instâncias via **broker pub/sub** (`BROKER_PROVIDER`) e presença em Redis (`PRESENCE_BACKEND=redis`): ver [[SeedGrid/Produtos/Pulse/Servidor/Providers|Providers]].
|
|
70
|
+
- Revogação de credencial atravessa o broker (tópico `pulse.revocations`) e derruba as conexões em **todas** as instâncias.
|
|
71
|
+
- Limites de emissão de token são **por instância**: com N réplicas o efetivo é até N vezes.
|
|
72
|
+
|
|
73
|
+
## Diagnóstico rápido
|
|
74
|
+
|
|
75
|
+
| Sintoma | Verifique |
|
|
76
|
+
|---|---|
|
|
77
|
+
| Publicou e ninguém recebeu | `accountId`/conta ativa; `channel` e `event` batem com o `subscribe`; `user` do destino = `subject` da conexão; entrega fica `pending` até alguém conectar |
|
|
78
|
+
| 403 ao publicar | está usando chave `client` ou a chave de servidor; use a `project` |
|
|
79
|
+
| WebSocket 401 | credencial ausente/revogada; token `pct_` vencido antes de conectar |
|
|
80
|
+
| Conexão fecha com `4403` | chave/conta revogada: não reconecte com a mesma credencial |
|
|
81
|
+
| `connect-tokens` 503 | falta `SEEDGRID_PULSE_CONNECT_TOKEN_SECRET` |
|
|
82
|
+
| E-mail/2FA por e-mail não chega | `SECURITY_MAILER_USERNAME`/`_PASSWORD` ausentes (só loga) |
|
|
83
|
+
| Histórico "sumiu" | retenção definida no ambiente (`DELIVERY_CLEANUP_RETENTION_*`) |
|
|
84
|
+
| Réplica B não recebe o que A publicou | broker `memory` em produção multi-réplica |
|
|
85
|
+
|
|
86
|
+
Testes e CI: `cargo test` (unitários e e2e); com Docker/Podman, `cargo test redis_ -- --ignored`, `postgres_ -- --ignored` e `--test trash_integration -- --ignored --test-threads=1` sobem Postgres/Redis via testcontainers.
|