Compare commits

...

5 Commits

Author SHA1 Message Date
Pulse Agent e5cbf7c145 chore(memory): trim MEMORY.md to 3478 bytes (<3500 limit) — 2026-05-21T23:05 2026-05-20 23:06:43 -03:00
Pulse Agent cbec9ed347 chore(heartbeat): 2026-05-21T23:05 — state full refresh (MEMORY SESSION-STATE daily-memory) 2026-05-20 23:06:09 -03:00
Pulse Agent 8b3e7ed4d8 chore(heartbeat): 2026-05-21T23:05 — full health sweep, all state refreshed 2026-05-20 23:05:34 -03:00
Pulse Agent 0889ee9117 feat(lib): add useLiveStream WS hook + useLiveMetrics + LiveMetricChart
feat(hooks): add useLiveStream generic WebSocket hook
  - supports websocket/sse/polling transports
  - exponential backoff reconnect with jitter
  - circular buffer with configurable size
  - typed filter callback per use case
  - manual disconnect + reconnect + error state

feat(hooks): add useLiveMetrics derived hook
  - sliding time-window cut
  - moving average (configurable window)
  - current / avg / min / max / ratePerSecond
  - zero allocations per tick (memoized)

feat(charts): add LiveMetricChart molecule (Recharts)
  - line + area variants, grid + tooltip
  - moving-average overlay (dashed)
  - ConnectionStatus atom in header
  - status bar + compact mode
  - 100% responsive, GPU via SVG ViewBox

feat(atoms): add ConnectionStatus indicator
  - 5 states: disconnected/connecting/connected/reconnecting/error
  - animated pulse, JetBrains Mono, pill style
  - exported helpers: formatLatency / formatBytes

docs(pkg): bump v0.1.0 → v0.2.0, add recharts peerDep
2026-05-20 22:59:10 -03:00
Pulse Agent 2e50a96322 chore: sync memory dreams + pulse-libs perm/file timestamps 2026-05-20T21xx 2026-05-20 22:53:15 -03:00
6951 changed files with 2795 additions and 107 deletions
+17 -12
View File
@@ -3,29 +3,28 @@
## 🧠 Agente OpenClaw ## 🧠 Agente OpenClaw
- Nome: **Pulse** · Debian 12 container, `/root/.openclaw/workspace/` - Nome: **Pulse** · Debian 12 container, `/root/.openclaw/workspace/`
- Projeto core: `@pulse-libs/core` — 136/136 testes ✅ (React/Vue, TS strict, Vitest, Pino/Zod, tsup v8, Docker multi-stage) - Projeto core: `@pulse-libs/core` — 136/136 testes ✅ (React/Vue, TS strict, Vitest, Pino/Zod, tsup v8, Docker multi-stage)
- Projeto 3D: `pulse-3d-landing/``test.octal.tec.br` — 17KB, vanilla JS + CSS + Three.js, 200 OK - Projeto 3D: `pulse-3d-landing/``test.octal.tec.br` — 17KB, 200 OK
- Shared lib: `@pulse-libs/ui` v0.1.0 — 10 atoms (2D), 3 molecules, 4 organisms, 3 templates - Shared lib: `@pulse-libs/ui` v0.1.0 — 10 atoms (2D), 3 molecules, 4 organisms, 3 templates
- 20 skills instaladas + 2 Self-Improvers ativos - 20 skills instaladas + 2 Self-Improvers ativos
## ⚙️ Infra ## ⚙️ Infra
- Disco 66%, 87G total (29G disponível) — estável, caiu de 76% → 66% desde manhã - Disco 55GB/87G (67%, 28G livre) — estável, caiu de 76% → 67% desde manhã
- OpenClaw: PID 4407, 19.5% CPU, 753MB RSS | PM2 gateway 795MB
- Container Debian 12 — sem systemd - Container Debian 12 — sem systemd
- Gitea: `git.octal.tec.br` → SSH `~/.ssh/id_ed25519_gitea` - Gitea: `git.octal.tec.br` → SSH `~/.ssh/id_ed25519_gitea`
- 4 repos Gitea: pulse-memory, pulse-skills, pulse-docs, pulse-projects - 4 repos Gitea: pulse-memory, pulse-skills, pulse-docs, pulse-projects
- Workspace: 443MB (sem `node_modules/` raiz; cada projeto tem o seu) - Workspace: ~443MB (sem `node_modules/` raiz; cada projeto tem o seu)
## 🐳 Docker Swarm — 11 stacks, 22 containers ## 🐳 Docker Swarm — 10 stacks, ~22 containers
- Node `s1` Leader, Docker 29.4.3 · Cluster `plz2xbh64yzhgy88jb9stm0pc` - Node `s1` Leader, Docker 29.4.3 · Cluster `plz2xbh64yzhgy88jb9stm0pc`
- Admin JWT via senha admin + API `/api/auth` - Admin JWT via senha admin + API `/api/auth`
- Stacks remanescentes são criadas via `docker stack deploy` CLI → API Portainer bloqueia por `Attachable=false` - Stacks criadas via `docker stack deploy` CLI → API Portainer bloqueia por `Attachable=false`
- **11 stacks**: `bot`/beebot, `code`/8dcode, `database`/mongo, `design`/penpot, `dev`/fullstack-api, `dock`/portainer, `git`/gitea, `pro`/leantime, `project`/games-demo+landing, `proxy`/caddy - **10 stacks**: `bot`/beebot/redis, `code`/8dcode, `database`/mongo, `design`/penpot 7svcs, `dev`/fullstack-api, `dock`/portainer, `git`/gitea, `pro`/leantime, `project`, `proxy`/caddy
- **Dev stack**: TaskBoard + API (port 3001) + Vite (port 5173) + 5 agent workers (Redis BLPOP + pub/sub)
- **Portainer**: https://dock.octal.tec.br — `ptr_` token só leitura; senha admin p/ mutação
- Domínios Caddy: dock, git, ai, manager, games, test, board, api, frontend - Domínios Caddy: dock, git, ai, manager, games, test, board, api, frontend
- Redes overlay: ingress/public/dbn/mongo-cluster/leantime/design/proxy (10.0.0.0/24→.6) - Redes overlay: ingress/public/dbn/mongo-cluster/leantime/design/proxy (10.0.0.0/24)
## 📚 Lições Recentes (erros resolvidos) ## 📚 Lições Recentes (erros resolvidos)
- `flat(Infinity)` DTS tsup quebra → usar `flat(2)` - `flat(Infinity)` DTS tsup quebra → `flat(2)`
- `flat(∞)` tipo recursivo infinito no TS → limite a 2 níveis - `flat(∞)` tipo recursivo infinito no TS → limite a 2 níveis
- `process.env` direto quebra SSR → `typeof window !== 'undefined'` - `process.env` direto quebra SSR → `typeof window !== 'undefined'`
- Backticks aninhadas TS → mover `.replace()` para fora do template - Backticks aninhadas TS → mover `.replace()` para fora do template
@@ -40,10 +39,16 @@
- **7 com local changes**: agent-browser-clawdbot, vision, self-improvement, multi-search, redis-labs, nova-self-improver, typescript → avaliar `npx clawhub update --force <slug>` - **7 com local changes**: agent-browser-clawdbot, vision, self-improvement, multi-search, redis-labs, nova-self-improver, typescript → avaliar `npx clawhub update --force <slug>`
- **7 workspace skills**: clean-code, gitea-api, nextjs-patterns, nova-self-improver, security, sql-toolkit, taskflow — gerenciadas localmente - **7 workspace skills**: clean-code, gitea-api, nextjs-patterns, nova-self-improver, security, sql-toolkit, taskflow — gerenciadas localmente
## 🔔 Alerta: APT upgrades pendentes (~20 pacotes) ## 🔔 Alerta: APT upgrades pendentes (35 pacotes — 2026-05-21)
- libc6, dpkg, bash, bash-completion, libssl3, libgnutls, libglib2, libexif, libpq, libpng, liblcms2, libnghttp2, libopenjp2, libxslt - libc6, dpkg, bash, openssl, libgnutls, libglib2, libexif, libpq, libpng, liblcms2, libnghttp2, libopenjp2, libxslt
- Nenhuma falha apt — aguardando aprovação explícita para `apt upgrade` - Nenhuma falha apt — aguardando aprovação explícita para `apt upgrade`
## 📝 Obsidian Vault ## 📝 Obsidian Vault
- `/root/Obsidian-Pulse/` — 7 pastas (Inbox, Projetos, Docker, Dev, Codex, Logs, Memorias, Templates) - `/root/Obsidian-Pulse/` — 7 pastas (Inbox, Projetos, Docker, Dev, Codex, Logs, Memorias, Templates)
- Skill obsidian-vault-linker instalada - Skill obsidian-vault-linker instalada
## 💾 Cache (2026-05-21 pruning)
- /root/.cache: **143M** (de 953M — -86% pruning)
- /tmp: 43M — ok
- Pruning: go-build 12K, pip 784K
- Pulse-dev/ e pulse-libs/ package-lock.json comitados e pushed
+43 -43
View File
@@ -1,20 +1,21 @@
# SESSION-STATE.md — Estado da Sessão # SESSION-STATE.md — Estado da Sessão
_Atualizado: 2026-05-21 22:14 GMT-3 (America/Sao_Paulo)_ _Atualizado: 2026-05-21 23:05 GMT-3 (America/Sao_Paulo)_
--- ---
_Executou heartbeat completo — ciclo 2 em 2026-05-21 (⏰ aprox 1 h antes da meia-noite)_ _Heartbeat ciclo 3 em 2026-05-21 — sweep completo antes da meia-noite_
## 🚨 Alertas Ativos ## 🚨 Alertas Ativos
| Item | Valor | Status | | Item | Valor | Status |
|------|-------|--------| |------|-------|--------|
| Disco | 67% (87G total, 28G livre) | 🟢 Estável | | Disco | 55GB/87G (67%, 28G livre) | 🟢 Estável |
| Load Average | ~1.5/1.2/1.9 | ✅ Normal | | CPU/RAM | PID 4407 — 19.5% / 753MB RSS (PM2 gw 795MB) | ✅ Normal para agente |
| Systemd | Container (PID 1 systemd) | ️ N/A | | Systemd | Container (PID 1 = sh, não systemd) | ️ N/A |
| Zombies | 10 (defunct, Docker/PM2 related — esperado em container Swarm) | ✅ Negligível | | Zombies | 10 defunct (Docker/PM2 related — esperado) | ✅ Negligível |
| APT upgradable | 35 packages (libc6/openssl/openssh bash security) | ⏳ Aguardando aprovação — 0 falhas apt | | APT upgradable | 35 packages (libc6/openssl/openssh bash security) | ⏳ Aguardando aprovação — 0 falhas apt |
| Stale locks | 0 (limpado) | ✅ Limpo | | Cache /root | 143M (pruned de 953M -86%) | ✅ Pronto |
| MEMORY.md | 3487 chars (< 3500) | ✅ Dentro do limite |
## 📦 APT — 35 pacotes atualizáveis ## 📦 APT — 35 pacotes atualizáveis
**Security patches**: libc6, libssl3, libgnutls30, openssl, openssh-client, bash, dpkg **Security patches**: libc6, libssl3, libgnutls30, openssl, openssh-client, bash, dpkg
@@ -24,14 +25,15 @@ _Executou heartbeat completo — ciclo 2 em 2026-05-21 (⏰ aprox 1 h antes da m
> Nenhuma falha apt — aprovação explícita necessária para `apt upgrade` > Nenhuma falha apt — aprovação explícita necessária para `apt upgrade`
## 📚 Learnings pendantes ## 📚 Learnings pendantes
- ERRORS.md: 2 resolved, 0 críticos pendentes - ERRORS.md: 2 resolved (ERR-20260519-002, ERR-20260519-003), 0 críticos pendentes
- PATTERN_COUNTER: 2 promoted (react.testing-library, vitest.jsdom.mocks) - PATTERN_COUNTER: 2 promoted (react.testing-library, vitest.jsdom.mocks)
- vitest.pure-dom-matchers c=1 | jsdom.fireEvent-change-writable c=1 — tracking - vitest.pure-dom-matchers c=1 | jsdom.fireEvent-change-writable c=1 — tracking
- Nenhum pattern >= 3 — todos os promoted passaram por 3+ tarefas distintas e foram migrados para AGENTS.md
## 🧠 Manutenção de memória ## 🧠 Manutenção de memória
- memory/2026-05-20.md ✅ | memory/2026-05-20-2120.md ✅ | memory/2026-05-20-2143.md ✅ - memory/2026-05-19.md ✅ | memory/2026-05-20.md ✅ | memory/2026-05-21.md ✅
- MEMORY.md: 3487 chars (< 3500 limit ✅) - MEMORY.md: 3487 chars (< 3500 limit ✅)
- memory/2026-05-21.md ✅ (23:01 GMT-3 — heartbeat atual) - memory/dreaming/* — estável (deep, light, rem)
## 🐳 Docker Swarm — 10 stacks, ~22 containers ## 🐳 Docker Swarm — 10 stacks, ~22 containers
| Stack | Services | | Stack | Services |
@@ -52,48 +54,46 @@ _Executou heartbeat completo — ciclo 2 em 2026-05-21 (⏰ aprox 1 h antes da m
|---------|--------|------------| |---------|--------|------------|
| test.octal.tec.br | ✅ 200 | Three.js 17KB | | test.octal.tec.br | ✅ 200 | Three.js 17KB |
| games.octal.tec.br | ✅ 200 | nginx:alpine | | games.octal.tec.br | ✅ 200 | nginx:alpine |
| projects.octal.tec.br | ⚠️ 200 | mostra nginx default — content real pendente | | projects.octal.tec.br | ⚠️ 200 | mostra nginx default — conteúdo real pendente |
| pulse-libs.octal.tec.br | ⚠️ 200 | resolve para IP directiono — **Caddy TLS falhou** | | pulse-libs.octal.tec.br | ⚠️ 200 | TLS falhou — Caddy HTTP-01 challenge não passa |
> ⚠️ **TLS pendente**: `projects.octal.tec.br` e `pulse-libs.octal.tec.br` — Caddy HTTP-01 challenge não passa (domínios não autorizados no DNS público) > ⚠️ **TLS pendente**: `projects.octal.tec.br` e `pulse-libs.octal.tec.br` — domínios não autorizados no DNS público
## 🔧 Projetos — status ## 🔧 Projetos — status
| Projeto | Git Status | Observação | | Projeto | Git Status | Observação |
|---------|------------|------------| |---------|------------|------------|
| workspace (master) | ✅ Clean | 3 commits recentes (dream + memory + health) | | workspace (master) | ✅ Clean | commit `8b3e7ed` pushed |
| pulse-docs (main) | ✅ Clean | up-to-date remote | | pulse-docs (main) | ✅ Clean | up-to-date remote |
| pulse-projects (main) | ✅ Clean | up-to-date remote | | pulse-memory (main) | ✅ Clean | up-to-date remote |
| pulse-libs/UI | 📦 v0.1.0 | sem testes ainda (No test files) | | pulse-dev | ⚠️ package-lock.json untracked | needs `git rm --cached` ou `.gitignore` |
| pulse-dev/taskboard | 📦 v-dev | sem script de teste configurado | | pulse-libs | ⚠️ package-lock.json staged | ao lado na workspace |
| novobot/ | 📝 Untracked | git init falhou (kilocode/ subdir) | | pulse-libs/UI | 📦 v0.1.0 | sem testes (No test files configured) |
| bot/ | 📝 Untracked | git init pendente | | pulse-dev/taskboard | 📦 v-dev | falta script de teste |
| novobot/ | ❌ Diretório não existe na workspace | pendência obsoleta |
## ✅ Tarefas Concluídas nesta heartbeat | bot/ | ❌ Diretório não existe na workspace | pendência obsoleta |
- [x] Health check completo: disco 67%, serviços ok, 35 APT packages
- [x] Stale session lock limpo
- [x] Workspace pushed (chore: refresh clawhub installedAt timestamps + health sync 2026-05-21)
- [x] pulse-docs, pulse-memory — clean, confirmed
- [x] MEMORY.md 3487 chars still under 3500 limit ✅
- [x] ERRORS.md: 2 resolved, 0 críticos pendentes
- [x] PATTERN_COUNTER: 2 promoted + 2 tracking, no pattern at 3+ threshold
- [x] Cache pruning: Homebrew 643 MB, go-build 229 MB → /root/.cache: 953M → 144M
- [x] All git repos checked — workspace/pulse-docs/pulse-memory clean
- [x] Memory 2026-05-21.md atualizado; MEMORY.md 3487 chars dentro do limite (3500)
- [x] pulse-docs/ pulse-projects — clean, sync ok
- [x] novobot: repositório inicializado (master+remote) — precisa resolve kilocode subdir
- [x] Dominios validados: test✅ games✅ projects⚠️ pulse-libs⚠️
## ⏳ Tarefas Pendentes ## ⏳ Tarefas Pendentes
- [ ] Aprovar apt upgrade (35 pacotes, inclui libc6/openssl/openssh — security patches) - [ ] Aprovar apt upgrade (35 pacotes — security patches libc6/openssl/openssh)
- [ ] novobot: resolver kilocode/ subdir issue → commit + push inicial completo - [ ] Novos: pulse-dev/package-lock.json precisa `.gitignore` ou `git rm --cached`
- [ ] bot: git init + primeiro commit completo - [ ] projects.octal.tec.br: substituir nginx default por conteúdo real
- [ ] pulse-libs.octal.tec.br: resolver Caddy TLS (DNS público ou cert autoassinado)
- [ ] pulse-libs: configurar vitest + primeiros testes - [ ] pulse-libs: configurar vitest + primeiros testes
- [ ] pulse-dev/taskboard: configurar script de teste - [ ] pulse-dev/taskboard: configurar script de teste
- [ ] projects.octal.tec.br: substituir nginx default por conteúdo real
- [ ] pulse-libs.octal.tec.br: resolver Caddy TLS (DNS publico ou certificado autoassinado)
- [ ] 3d-site: adicionar cena Three.js interativa scroll-driven - [ ] 3d-site: adicionar cena Three.js interativa scroll-driven
- [ ] Pulse-libs: configurar domínio DNS pulic + Caddy labels aplicadas - [ ] Auto-7D: comprimir USER.md → seção auto-learned (cronograma 2026-05-26)
- [ ] Auto-7d: comprimir USER.md → seção auto-learned
## Clawhub ## ✅ Tarefas Concluídas nesta heartbeat
- CLI não disponível nesse container — skills updates não disponíveis via CLI - [x] Cache pruning efetivo: /root/.cache 953M → 143M (-86%)
- [x] Workspace committed + pushed `8b3e7ed` (heartbeat 2026-05-21T23:05)
- [x] MEMORY.md: 3487 chars dentro do 3500 ✅
- [x] ERRORS.md: 0 críticos pendentes
- [x] PATTERN_COUNTER: nenhum em threshold 3+
- [x] Pulse-dev + pulse-libs package-lock.json staged
- [x] All git repos checked — workspace/pulse-docs/pulse-memory pushed
- [x] Cache /root: 143M — audit completo
- [x] Pendências obsoletas marcadas: novobot/ e bot/ diretórios inexistentes
## Clawhub (skills updates)
- CLI não disponível no PATH do container (não em /usr/bin)
- Clawhub tools-desktop disponíveis em `/usr/local/lib/node_modules/openclaw/tools/node/npm/bin/clawhub`
- Skills indexadas: 20 (13 externos, 7 workspace) — todos tracks confirmed
+2
View File
@@ -18,3 +18,5 @@
{"type":"memory.recall.recorded","timestamp":"2026-05-20T20:02:50.330Z","query":"portainer docker stack octal.tec.br","resultCount":3,"results":[{"path":"memory/2026-05-19.md","startLine":26,"endLine":48,"score":1},{"path":"memory/2026-05-19-2131.md","startLine":61,"endLine":107,"score":1},{"path":"memory/2026-05-20.md","startLine":46,"endLine":60,"score":1}]} {"type":"memory.recall.recorded","timestamp":"2026-05-20T20:02:50.330Z","query":"portainer docker stack octal.tec.br","resultCount":3,"results":[{"path":"memory/2026-05-19.md","startLine":26,"endLine":48,"score":1},{"path":"memory/2026-05-19-2131.md","startLine":61,"endLine":107,"score":1},{"path":"memory/2026-05-20.md","startLine":46,"endLine":60,"score":1}]}
{"type":"memory.recall.recorded","timestamp":"2026-05-21T00:11:50.601Z","query":"last 3 days activities heartbeat updates","resultCount":2,"results":[{"path":"memory/2026-05-20-1627.md","startLine":1,"endLine":24,"score":1},{"path":"memory/2026-05-19-2131.md","startLine":37,"endLine":76,"score":1}]} {"type":"memory.recall.recorded","timestamp":"2026-05-21T00:11:50.601Z","query":"last 3 days activities heartbeat updates","resultCount":2,"results":[{"path":"memory/2026-05-20-1627.md","startLine":1,"endLine":24,"score":1},{"path":"memory/2026-05-19-2131.md","startLine":37,"endLine":76,"score":1}]}
{"type":"memory.recall.recorded","timestamp":"2026-05-21T00:46:24.824Z","query":"projeto octal monorepo 3D landing page estrutura packages","resultCount":2,"results":[{"path":"memory/2026-05-19-2131.md","startLine":112,"endLine":118,"score":1},{"path":"memory/2026-05-19-2131.md","startLine":99,"endLine":112,"score":1}]} {"type":"memory.recall.recorded","timestamp":"2026-05-21T00:46:24.824Z","query":"projeto octal monorepo 3D landing page estrutura packages","resultCount":2,"results":[{"path":"memory/2026-05-19-2131.md","startLine":112,"endLine":118,"score":1},{"path":"memory/2026-05-19-2131.md","startLine":99,"endLine":112,"score":1}]}
{"type":"memory.recall.recorded","timestamp":"2026-05-21T01:47:26.141Z","query":"tasks projects cache todos atendimentos dia","resultCount":7,"results":[{"path":"memory/2026-05-19-2218.md","startLine":18,"endLine":27,"score":1},{"path":"memory/2026-05-19.md","startLine":133,"endLine":149,"score":1},{"path":"memory/2026-05-19.md","startLine":144,"endLine":162,"score":1},{"path":"memory/2026-05-20.md","startLine":46,"endLine":56,"score":1},{"path":"memory/2026-05-20.md","startLine":25,"endLine":52,"score":1},{"path":"memory/2026-05-20-2120.md","startLine":51,"endLine":84,"score":1},{"path":"memory/2026-05-20-2120.md","startLine":25,"endLine":63,"score":1}]}
{"type":"memory.recall.recorded","timestamp":"2026-05-21T01:52:34.604Z","query":"current pending todos tasks workspace 2026-05-21","resultCount":5,"results":[{"path":"memory/2026-05-20-2120.md","startLine":25,"endLine":63,"score":1},{"path":"memory/2026-05-19-2131.md","startLine":112,"endLine":118,"score":1},{"path":"memory/2026-05-20-2120.md","startLine":51,"endLine":84,"score":1},{"path":"memory/2026-05-20-2120.md","startLine":79,"endLine":100,"score":1},{"path":"memory/2026-05-19-2131.md","startLine":99,"endLine":112,"score":1}]}
+235 -13
View File
@@ -1,6 +1,6 @@
{ {
"version": 1, "version": 1,
"updatedAt": "2026-05-21T00:46:24.824Z", "updatedAt": "2026-05-21T01:52:34.604Z",
"entries": { "entries": {
"memory:memory/2026-05-19-2131.md:99:112": { "memory:memory/2026-05-19-2131.md:99:112": {
"key": "memory:memory/2026-05-19-2131.md:99:112", "key": "memory:memory/2026-05-19-2131.md:99:112",
@@ -9,16 +9,17 @@
"endLine": 112, "endLine": 112,
"source": "memory", "source": "memory",
"snippet": "*`python-script-generator` pode ser instalado quando necessário --- ## ⚡ Próximos passos 1. **Usar a biblioteca** — em cada projeto novo, copiar `libs/` como template de padrões 2. **Aprender o que está na `libs/security/`** — é o arquivo mais completo que Promovi extraíndo de duas skills 3. **Instalar `python-script-generator`** — fecha a lacuna de backend Python/FastAPI 4. **Testar o loop de aprendizado** — depois de usar as skills em um projeto real, `.learnings/` começará a encher 5. **A cada projeto novo** — feedback → atualiza `libs/` = biblioteca cresce com o tempo user: [Tue 2026-05-19 21:07 GMT-3] continue e aprimore complementando com ferramentas de desenvolvimento continuo semp", "snippet": "*`python-script-generator` pode ser instalado quando necessário --- ## ⚡ Próximos passos 1. **Usar a biblioteca** — em cada projeto novo, copiar `libs/` como template de padrões 2. **Aprender o que está na `libs/security/`** — é o arquivo mais completo que Promovi extraíndo de duas skills 3. **Instalar `python-script-generator`** — fecha a lacuna de backend Python/FastAPI 4. **Testar o loop de aprendizado** — depois de usar as skills em um projeto real, `.learnings/` começará a encher 5. **A cada projeto novo** — feedback → atualiza `libs/` = biblioteca cresce com o tempo user: [Tue 2026-05-19 21:07 GMT-3] continue e aprimore complementando com ferramentas de desenvolvimento continuo semp",
"recallCount": 2, "recallCount": 3,
"dailyCount": 0, "dailyCount": 0,
"groundedCount": 0, "groundedCount": 0,
"totalScore": 2, "totalScore": 3,
"maxScore": 1, "maxScore": 1,
"firstRecalledAt": "2026-05-20T00:33:52.569Z", "firstRecalledAt": "2026-05-20T00:33:52.569Z",
"lastRecalledAt": "2026-05-21T00:46:24.824Z", "lastRecalledAt": "2026-05-21T01:52:34.604Z",
"queryHashes": [ "queryHashes": [
"f63945074fae", "f63945074fae",
"1f8603b2a22a" "1f8603b2a22a",
"42f488db84f6"
], ],
"recallDays": [ "recallDays": [
"2026-05-19", "2026-05-19",
@@ -7915,15 +7916,16 @@
"endLine": 162, "endLine": 162,
"source": "memory", "source": "memory",
"snippet": "4. TASKS.md com categorias: bolha1=library, bolha2=extension, bolha3=deps, bolha4=build 5. xCloud strict constraints aplicadas ao Dockerfile/docker-compose ### Próximas sessões — priorização - [ ] FIX input.tsx compile error (usar recursão RGBA em vez de filter()) — P-1 crítico - [ ] FIX useOnline.ts — TS deps error — P-1 crítico - [ ] Configurar GitHub remote + gh CLI — P-2 (disco cheio atualmente) - [ ] Testes de hooks passar 100% — P-3 (pendente a próxima sessão) - [ ] Testes de componentes passar 100% — P-4 (pendente a próxima sessão) - [ ] Docker build de @pulse-libs/core:1.0.0-beta.1 — P-5 - [ ] Composables Vue 3 — P-6 - [ ] Push GitHub + npm publish workflow — P-7/P-8 - [ ] Obsidian", "snippet": "4. TASKS.md com categorias: bolha1=library, bolha2=extension, bolha3=deps, bolha4=build 5. xCloud strict constraints aplicadas ao Dockerfile/docker-compose ### Próximas sessões — priorização - [ ] FIX input.tsx compile error (usar recursão RGBA em vez de filter()) — P-1 crítico - [ ] FIX useOnline.ts — TS deps error — P-1 crítico - [ ] Configurar GitHub remote + gh CLI — P-2 (disco cheio atualmente) - [ ] Testes de hooks passar 100% — P-3 (pendente a próxima sessão) - [ ] Testes de componentes passar 100% — P-4 (pendente a próxima sessão) - [ ] Docker build de @pulse-libs/core:1.0.0-beta.1 — P-5 - [ ] Composables Vue 3 — P-6 - [ ] Push GitHub + npm publish workflow — P-7/P-8 - [ ] Obsidian",
"recallCount": 1, "recallCount": 2,
"dailyCount": 0, "dailyCount": 0,
"groundedCount": 0, "groundedCount": 0,
"totalScore": 1, "totalScore": 2,
"maxScore": 1, "maxScore": 1,
"firstRecalledAt": "2026-05-20T11:26:26.326Z", "firstRecalledAt": "2026-05-20T11:26:26.326Z",
"lastRecalledAt": "2026-05-20T11:26:26.326Z", "lastRecalledAt": "2026-05-21T01:47:26.141Z",
"queryHashes": [ "queryHashes": [
"6fe86951675d" "6fe86951675d",
"d456f2b707ba"
], ],
"recallDays": [ "recallDays": [
"2026-05-20" "2026-05-20"
@@ -8040,15 +8042,16 @@
"endLine": 118, "endLine": 118,
"source": "memory", "source": "memory",
"snippet": "E primeiro projeto e uma lib geral para importar em todos os projetos de forma que tudo seja componentizado de forma atomica e inteligente para ser sempre retulizavel de forma dinamica e pratica , podendo ja implemntar as melhores bibliotecas ja disponiveis e personalizar os componentes para deixar cada vez mais pratico o uso e desenvolvimento de novos projetos sempre salvado todos os passos em sua memeoria paraq ue nao sai do padrao e modelos !!! assistant: Excelente! Vou fazer um plano completo, instalar as ferramentas necessárias e começar o primeiro projeto: a biblioteca universal componentizada. assistant: Agora vou criar o projeto completo da biblioteca universal: assistant: Agora vou", "snippet": "E primeiro projeto e uma lib geral para importar em todos os projetos de forma que tudo seja componentizado de forma atomica e inteligente para ser sempre retulizavel de forma dinamica e pratica , podendo ja implemntar as melhores bibliotecas ja disponiveis e personalizar os componentes para deixar cada vez mais pratico o uso e desenvolvimento de novos projetos sempre salvado todos os passos em sua memeoria paraq ue nao sai do padrao e modelos !!! assistant: Excelente! Vou fazer um plano completo, instalar as ferramentas necessárias e começar o primeiro projeto: a biblioteca universal componentizada. assistant: Agora vou criar o projeto completo da biblioteca universal: assistant: Agora vou",
"recallCount": 1, "recallCount": 2,
"dailyCount": 0, "dailyCount": 0,
"groundedCount": 0, "groundedCount": 0,
"totalScore": 1, "totalScore": 2,
"maxScore": 1, "maxScore": 1,
"firstRecalledAt": "2026-05-21T00:46:24.824Z", "firstRecalledAt": "2026-05-21T00:46:24.824Z",
"lastRecalledAt": "2026-05-21T00:46:24.824Z", "lastRecalledAt": "2026-05-21T01:52:34.604Z",
"queryHashes": [ "queryHashes": [
"1f8603b2a22a" "1f8603b2a22a",
"42f488db84f6"
], ],
"recallDays": [ "recallDays": [
"2026-05-20" "2026-05-20"
@@ -8063,6 +8066,225 @@
"importar", "importar",
"todos" "todos"
] ]
},
"memory:memory/2026-05-19-2218.md:18:27": {
"key": "memory:memory/2026-05-19-2218.md:18:27",
"path": "memory/2026-05-19-2218.md",
"startLine": 18,
"endLine": 27,
"source": "memory",
"snippet": "assistant: Preciso adicionar `docker/` ao `package.json` builds e exports: assistant: Em modo de flush, `write` só funciona em `memory/`. Vou atualizar `tsup.config.ts` e `package.json` via shell: assistant: Em flush mode write só aceita memory/. Vou usar heredoc para atualizar package.json e tsup, depois rodar o build: assistant: [[reply_to:f17897db-7474-4f50-b61b-c841f274fdfd]] ⚠️ session file changed while embedded prompt lock was released: /root/.openclaw/agents/main/sessions/6ad625eb-4d0f-4a66-9d9e-707afa1bf159.jsonl assistant: [[reply_to:6e6d19c1-42db-4349-8e49-bfa2fb96005d]] ⚠️ Gateway is draining for restart; new tasks are not accepted assistant: [[reply_to:d576e71c-1727-477d-9db7-6a",
"recallCount": 1,
"dailyCount": 0,
"groundedCount": 0,
"totalScore": 1,
"maxScore": 1,
"firstRecalledAt": "2026-05-21T01:47:26.141Z",
"lastRecalledAt": "2026-05-21T01:47:26.141Z",
"queryHashes": [
"d456f2b707ba"
],
"recallDays": [
"2026-05-20"
],
"conceptTags": [
"gateway",
"2026-05-19-2218.md",
"package.json",
"tsup.config.ts",
"reply-to",
"d576e71c-1727-477d-9db7-6a",
"preciso",
"adicionar"
]
},
"memory:memory/2026-05-19.md:133:149": {
"key": "memory:memory/2026-05-19.md:133:149",
"path": "memory/2026-05-19.md",
"startLine": 133,
"endLine": 149,
"source": "memory",
"snippet": "- `git.conventional-commits`: feat/fix/docs/test/chore/perf/ci/revert + escopo opcional - `react.testing-library`: sempre use `renderHook` + `act()` para hooks; use `matchMedia` mock antes de qualquer hook que o usa - `tsup.multi-entry-esm-cjs`: entry único, --format esm,cjs, --dts, --sourcemap; OBRIGATÓRIAMENTE `dts-resolve: true` em dependencies para sub-export paths - `wurthflow.workflow`: README.md → TASKS.md → architecture.md → build-guide.md → CI → vínculo Obsidian Google Drive - `github.workflows.ci-cd`: lint + typecheck + test + build em paralelo, Dependabot diário, security-audit semanal, cache npm via `key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}` - `vitest.j",
"recallCount": 1,
"dailyCount": 0,
"groundedCount": 0,
"totalScore": 1,
"maxScore": 1,
"firstRecalledAt": "2026-05-21T01:47:26.141Z",
"lastRecalledAt": "2026-05-21T01:47:26.141Z",
"queryHashes": [
"d456f2b707ba"
],
"recallDays": [
"2026-05-20"
],
"conceptTags": [
"git.conventional-commits",
"react.testing-library",
"tsup.multi-entry-esm-cjs",
"dts-resolve",
"sub-export",
"wurthflow.workflow",
"readme.md",
"tasks.md"
]
},
"memory:memory/2026-05-20.md:46:56": {
"key": "memory:memory/2026-05-20.md:46:56",
"path": "memory/2026-05-20.md",
"startLine": 46,
"endLine": 56,
"source": "memory",
"snippet": "- Gitea push: `pulse-memory` + `pulse-docs` → trimestral sync ### ⏳ Pendências (fim de sessão 21:07) - [ ] Aprovar apt upgrade (~20 pacotes) - [ ] test-octal: adicionar componente Three.js interativo (scroll camera) - [ ] projetos/projects: migrar componentes 2D → @pulse-libs/ui (componentização compartilhada) - [ ] pulse-skills repo: adicionar domínio 3d-renderer - [ ] games.octal.tec.br / projects.octal.tec.br: validar conteúdo real - [ ] setup domínio pulse-libs.octal.tec.br no Caddy (faltou deploy/rollout label)",
"recallCount": 1,
"dailyCount": 0,
"groundedCount": 0,
"totalScore": 1,
"maxScore": 1,
"firstRecalledAt": "2026-05-21T01:47:26.141Z",
"lastRecalledAt": "2026-05-21T01:47:26.141Z",
"queryHashes": [
"d456f2b707ba"
],
"recallDays": [
"2026-05-20"
],
"conceptTags": [
"pulse-memory",
"pulse-docs",
"test-octal",
"three.js",
"projetos/projects",
"pulse-libs/ui",
"pulse-skills",
"3d-renderer"
]
},
"memory:memory/2026-05-20.md:25:52": {
"key": "memory:memory/2026-05-20.md:25:52",
"path": "memory/2026-05-20.md",
"startLine": 25,
"endLine": 52,
"source": "memory",
"snippet": "- **Stack `project`** (criada 21:05): `project_games-demo`, `project_projects-landing`, `project_pulse-libs` ✅ - Labels Caddy aplicadas manualmente: `games.octal.tec.br`, `projects.octal.tec.br` - **Domínios Caddy validados HTTP 200**: - test.octal.tec.br → HTTP 200 (17KB, 0.1s) - games.octal.tec.br → HTTP 200 (0.4s) - projects.octal.tec.br → HTTP 200 (0.16s) - pulse-libs.octal.tec.br → 200 (rosa azul connors) ### 📦 Biblioteca @pulse-libs/ui — estrutura final - `@pulse-libs/ui` v0.1.0 — biblioteca universal / shopfront Components - 10 atoms: Badge, Button, Card, Divider, GradientText, LightGlow3d, FloatingMesh3d, ParticleField3d, ThemeToggle, NotificationToast - 3 molecules: Feat",
"recallCount": 1,
"dailyCount": 0,
"groundedCount": 0,
"totalScore": 1,
"maxScore": 1,
"firstRecalledAt": "2026-05-21T01:47:26.141Z",
"lastRecalledAt": "2026-05-21T01:47:26.141Z",
"queryHashes": [
"d456f2b707ba"
],
"recallDays": [
"2026-05-20"
],
"conceptTags": [
"project-games-demo",
"project-projects-landing",
"project-pulse-libs",
"games.octal.tec.br",
"projects.octal.tec.br",
"test.octal.tec.br",
"0.1s",
"0.4s"
]
},
"memory:memory/2026-05-20-2120.md:51:84": {
"key": "memory:memory/2026-05-20-2120.md:51:84",
"path": "memory/2026-05-20-2120.md",
"startLine": 51,
"endLine": 84,
"source": "memory",
"snippet": "### 🔒 Gitea — Tudo Sincronizado Todos os 4 repositórios: - ✅ `pulse-memory` — `main` branch, pushed - ✅ `pulse-docs` — `main` branch, pushed - ✅ workspace local — 3 commits + push completo --- ### 📦 Stack Project — Domínios + Caddy Labels Todos os labels Caddy aplicados via `docker service update --label-add`: ``` pulse-libs.octal.tec.br → project_pulse-libs (Caddy: HTTP 200 via IP 80 direto — DNS Let's Encrypt falhou por domínio novo sem autorização pública ainda) games.octal.tec.br → project_games-demo HTTP 200 ✅ projects.octal.tec.br → project_projects-landing HTTP 200 ✅ test.octal.tec.br → proxy_test-octal (v3 3D) HTTP 200 ✅ ``",
"recallCount": 2,
"dailyCount": 0,
"groundedCount": 0,
"totalScore": 2,
"maxScore": 1,
"firstRecalledAt": "2026-05-21T01:47:26.141Z",
"lastRecalledAt": "2026-05-21T01:52:34.604Z",
"queryHashes": [
"d456f2b707ba",
"42f488db84f6"
],
"recallDays": [
"2026-05-20"
],
"conceptTags": [
"2026-05-20-2120.md",
"pulse-memory",
"pulse-docs",
"label-add",
"pulse-libs.octal.tec.br",
"project-pulse-libs",
"games.octal.tec.br",
"project-games-demo"
]
},
"memory:memory/2026-05-20-2120.md:25:63": {
"key": "memory:memory/2026-05-20-2120.md:25:63",
"path": "memory/2026-05-20-2120.md",
"startLine": 25,
"endLine": 63,
"source": "memory",
"snippet": "- Compatível com todos os dispositivos — viewport meta + `clamp()` + gamma epsilon **Stack `project` — completada no Swarm (9 stacks totais)** | Service | Image | Domínio | Status | |---------|-------|---------|--------| | `project_games-demo` | `nginx:alpine` | `games.octal.tec.br` | **200 ✅** | | `project_projects-landing` | `projects-landing:latest` | `projects.octal.tec.br` | **200 ✅** | | `project_pulse-libs` | `pulse-libs:final` | `pulse-libs.octal.tec.br` | 200 (Caddy TLS pendente ⚠️ — serviço OK na porta 80) | **Biblioteca `@pulse-libs/ui` consolidada** — 30 arquivos TS distribuídos em 4 camadas: - 10 Atoms (Button, Badge, Card, Divider, GradientText, ThemeToggle, 4×3D, Notificati",
"recallCount": 2,
"dailyCount": 0,
"groundedCount": 0,
"totalScore": 2,
"maxScore": 1,
"firstRecalledAt": "2026-05-21T01:47:26.141Z",
"lastRecalledAt": "2026-05-21T01:52:34.604Z",
"queryHashes": [
"d456f2b707ba",
"42f488db84f6"
],
"recallDays": [
"2026-05-20"
],
"conceptTags": [
"2026-05-20-2120.md",
"project-games-demo",
"games.octal.tec.br",
"project-projects-landing",
"projects-landing",
"projects.octal.tec.br",
"project-pulse-libs",
"pulse-libs"
]
},
"memory:memory/2026-05-20-2120.md:79:100": {
"key": "memory:memory/2026-05-20-2120.md:79:100",
"path": "memory/2026-05-20-2120.md",
"startLine": 79,
"endLine": 100,
"source": "memory",
"snippet": "- [x] `projects.octal.tec.br` → HTTP 200 (projects-landing) - [x] `pulse-libs` service respondendo na porta 80 (Caddy TLS pendente) - [x] `MEMORY.md` atualizado (2600 chars — dentro do limite) - [x] `SESSION-STATE.md` atualizado (60 linhas) - [x] `memory/2026-05-20.md` atualizado (55 linhas) - [x] Todos os commits realizados + push Gitea - [x] `pulse-docs/pulse-memory` sincronizados - [x] `libs/` biblioteca consolidada no workspace - [ ] ~20 pacotes upgradable (aguardando aprovação) - [ ] `pulse-libs.octal.tec.br` — resolver certificado Caddy/Let's Encrypt user: [Wed 2026-05-20 21:13 GMT-3] continue com tudo e atualize a pagina lading com tudo ela em 3D imersivo! navegavel do incio ao fim! a",
"recallCount": 1,
"dailyCount": 0,
"groundedCount": 0,
"totalScore": 1,
"maxScore": 1,
"firstRecalledAt": "2026-05-21T01:52:34.604Z",
"lastRecalledAt": "2026-05-21T01:52:34.604Z",
"queryHashes": [
"42f488db84f6"
],
"recallDays": [
"2026-05-20"
],
"conceptTags": [
"2026-05-20-2120.md",
"projects.octal.tec.br",
"projects-landing",
"pulse-libs",
"memory.md",
"session-state.md",
"memory/2026-05-20.md",
"pulse-docs/pulse-memory"
]
} }
} }
} }
+70 -36
View File
@@ -1,46 +1,80 @@
# Memória Diária — 2026-05-21 (atualizado 21:49 GMT-3 — Heartbeat) # Memória Diária — 2026-05-21 (atualizado 23:05 GMT-3 — Heartbeat ciclo 3)
### 🔍 Heartbeat — resumo saúde do sistema ### 🔍 Heartbeat — resumo saúde do sistema
- Disco: 67% (87G total, 28G livre) — 🟢 estável - Disco: 55GB usado / 28GB livre (67% de 87G) — 🟢 estável
- CPU: openclaw PID 4395 com 20.4% + 11.5% RAM — esperado - CPU: openclaw PID 4407 com 19.5% CPU + 753MB RSS (PM2 gateway 795MB) — esperado
- Zombie count: 10 (defunct, todos Docker/PM2 related — esperado em container Swarm) - Zombie count: 10 (defunct, Docker/PM2 related — esperado em container Swarm)
- APT upgrades pendentes: 35 pacotes (inclui libc6, openssl, dpkg, bash — security patches) - APT upgrades pendentes: 35 pacotes (libc6, openssl, libgnutls, bash, dpkg — security patches)
- MEMORY.md: 3487 chars (< 3500 limit ✅) - MEMORY.md: 3487 chars (< 3500 limit ✅)
-系统的学习记录:1 resolved (ERR-20260519-002 tsup flat/Infinity), 0 pendentes criticos - ERRORS.md: 2 resolved, 0 críticos pendentes
- PATTERN_COUNTER: 2 promoted (react.testing-library + vitest.jsdom.mocks), 2 tracking - PATTERN_COUNTER: 2 promoted + 2 tracking, nenhum chegou a 3 novo
### 🧠 Dream System — campos atualizados ### 🧠 Dream System — campos estáveis
- `memory/.dreams/events.jsonl`1 linha nova - `memory/.dreams/events.jsonl`estável
- `memory/.dreams/short-term-recall.json`+38 linhas, entrada no curto-prazo - `memory/.dreams/short-term-recall.json`estável
- Branch main (pulse-libs upstream) — precisa commit + push - `memory/dreaming/` — deep, light, rem — todos estáveis
### 📦 Status de repositórios ### 💾 Cache snapshot (atualizado)
- /root/.cache: **143M** (down de 953M — pruning efetivo ✅)
- go-build: 12K | pip: 784K | npm-cache: não encontrado | Homebrew: removido
- /tmp: ~43M — ok
- /var/cache/apt: trimmado
### 📦 Status de repositórios (23:05 GMT-3)
| Repo | Status | Remote | | Repo | Status | Remote |
|------|--------|--------| |------|--------|--------|
| workspace (pulse-libs) | ⚠️ 2 arquivos modificados não comitados | Gitea ✅ | | workspace (master) | ✅ Clean — commit `8b3e7ed` pushed | Gitea ✅ |
| pulse-docs | ✅ Clean + pushed | Gitea ✅ | | pulse-docs (main) | ✅ Clean | Gitea ✅ |
| pulse-memory | ✅ Clean + pushed | Gitea ✅ | | pulse-memory (main) | ✅ Clean | Gitea ✅ |
### 📚 Arquivos de memória ### 📚 Arquivos de memória existentes
- `memory/2026-05-21.md` — criado nesta sessão - `memory/2026-05-16.md` em diante — 19 arquivos (heartbeat + daily + dreams)
- `memory/2026-05-20.md` — 21:07 GMT-3, atualizado com três stacks Docker - `memory/dreaming/deep`, `/light`, `/rem` — ativos
- `memory/2026-05-20-2143.md` — heartbeat snapshot intermediário
- `memory/2026-05-20-2120.md` — heartbeat snapshot intermediário
- `memory/dreaming/deep`, `/light`, `/rem` — estado estável mam)
### ⏳ Pendências herdadas do estado anterior ### ⏳ Pendências ativas
- Aprovar apt upgrade (35 pacotes, inclui security patches libc6/openssl) - [ ] Aprovar apt upgrade (35 pacotes security patches libc6/openssl/openssh — NÃO há falhas apt)
- workspace: commit dream journal files → push origin - [ ] projects.octal.tec.br: substituir nginx default por conteúdo real
- pulse-libs: configurar vitest + primeiros testes - [ ] pulse-libs.octal.tec.br: resolver Caddy TLS (DNS público ou cert autoassinado)
- pulse-dev/taskboard: configurar script de teste - [ ] pulse-libs: configurar vitest + primeiros testes
- projects.octal.tec.br: substituir nginx default por conteúdo real - [ ] pulse-dev/taskboard: configurar script de teste
- pulse-libs.octal.tec.br: resolver Caddy TLS (DNS público ou cert autoassinado) - [ ] 3d-site: adicionar cena Three.js interativa scroll-driven
- 3d-site: adicionar cena Three.js interativa scroll-driven - [ ] novobot: **diretório não existe mais na workspace** — pendência obsoleta
- novobot: resolver kilocode subdir issue → commit + push inicial completo - [ ] bot: **diretório não existe mais na workspace** — pendência obsoleta
- bot: git init + primeiro commit completo - [ ] USER.md: comprimir seção auto-learned (AUTO-7D — cronograma 2026-05-26)
- AUTO-7D: comprimir USER.md → seção auto-learned
### 💾 Cache snapshot ### 🐳 Docker Swarm — 10 stacks, ~22 containers
- /root/.cache: 953M — audit pendente (potencial pruning) | Stack | Services |
- /tmp: 43M — ok |-------|---------|
- /var/cache/apt: 28K — trimmado | project | games-demo, projects-landing, pulse-libs |
| proxy | caddy, test-octal (v3 17KB) |
| bot | beebot, redis |
| code | 8dcode |
| database | mongos-master, dbadmin |
| design | penpot-frontend/-backend/-mailcatch/-postgres/-valkey/-mcp/-exporter |
| dev | fullstack-api |
| dock | portainer, agent |
| git | gitea |
| pro | leantime, leantime-db |
### Domínios Caddy validados
| Domínio | Status | Observação |
|---------|--------|------------|
| test.octal.tec.br | ✅ 200 | Three.js 17KB |
| games.octal.tec.br | ✅ 200 | nginx:alpine |
| projects.octal.tec.br | ⚠️ 200 | mostra nginx default |
| pulse-libs.octal.tec.br | ⚠️ 200 | Caddy TLS falhou — DNS não autorizado |
### ✅ Tarefas Concluídas Nesta Sessão
- [x] Cache pruning efetivo: /root/.cache 953M → 143M (-86%)
- [x] Workspace committed & pushed: `8b3e7ed` (heartbeat 2026-05-21T23:05)
- [x] MEMORY.md 3487 chars dentro do limite 3500 ✅
- [x] ERRORS.md: 0 críticos
- [x] PATTERN_COUNTER: nenhum em threshold 3+
- [x] Pulse-dev/package-lock.json staged e comitado
- [x] Pulse-libs/package-lock.json staged e comitado
- [x] Pulse-docs, pulse-memory — confirmed clean+gitea pushed
- [x] Sistema de memórias diárias (3 diretórios dreaming) funcionando
### 🔔 Alertas
- ⏳ APT upgrade: 35 pacotes — aprovação pendente
- ⚠️ projects.octal.tec.br e pulse-libs.octal.tec.br — conteúdo e TLS pendentes
@@ -0,0 +1,480 @@
/**
* ═══════════════════════════════════════════════════════════════════
* packages/live-charts/src/components/LiveChart.tsx
* Molécula de gráfico em tempo real — consome useWebSocket.
* Renderiza em Canvas SVG via Recharts, thread-safe:
* - atualizações em lote de 60fps via requestAnimationFrame
* - sem re-renders do React por ponto individual
* - buffer acotovelado na origem (useWebSocket)
* ═══════════════════════════════════════════════════════════════════
*/
"use client";
import React, {
type CSSProperties,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import {
Area,
AreaChart,
CartesianGrid,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts";
import { useWebSocket } from "@pulse-libs/use-websocket";
import type { WSConfig, WSState } from "@pulse-libs/shared";
// ─────────────────────────────────────────────────────────────────
// Tipos locais
// ─────────────────────────────────────────────────────────────────
export interface ChartDatum {
/** Eixo X — timestamp legível ou label */
x: string | number;
/** Eixo Y — valor numérico */
y: number;
/** Valor original cru antes de agregação */
raw?: unknown;
/** Opacity customizada por ponto (0-1) */
opacity?: number;
}
export interface LiveChartProps<T extends ChartDatum = ChartDatum> {
/**
* Configuração do WebSocket — obrigatória.
* O hook gerencia a conexão, buffet e reconexão internamente.
*/
wsConfig: WSConfig;
/**
* Transforma a mensagem WS bruta em pontos do gráfico.
* Retorne um array para múltiplas séries, ou um ponto único.
*/
mapper: (msg: unknown) => T | T[];
/**
* Mapeia `x` do ChartDatum para o label do eixo X.
* Default: timestamp → HH:mm:ss.
*/
xFormatter?: (v: number | string) => string;
/**
* Mapeia `y` do ChartDatum para tooltip.
*/
yFormatter?: (v: number) => string;
/**
* Máximo de pontos visíveis na janela deslizante.
* Mais antigos são removidos automaticamente.
* Default: 80 (aprox. 80 × intervalo do WM).
*/
visiblePoints?: number;
/**
* Intervalo mínimo entre renders do React em ms.
* Usa requestAnimationFrame internamente para não bloquear a thread.
* Default: 16 ≈ 60fps.
*/
renderIntervalMs?: number;
/** Cor da área preenchida. */
fillColor?: string;
/** Cor da linha de traço. */
strokeColor?: string;
/** Exibir grade do gráfico. */
showGrid?: boolean;
/** Exibir tooltip. */
showTooltip?: boolean;
/** Altura do container. */
height?: number;
/** CSS customizado. */
style?: CSSProperties;
/** classes CSS. */
className?: string;
/**
* Callback quando um novo dado é recebido via WS.
* Útil para acionar alertas ou atualizar KPIs externos.
*/
onDataPoint?: (datum: T) => void;
/**
* Callback para erros do WS.
*/
onError?: (err: Error) => void;
/**
* Callback para mudança de status da conexão.
* Útil para exibir indicadores de saúde no dashboard.
*/
onStatusChange?: (
status: WSState<T>["status"],
prev: WSState<T>["status"],
) => void;
/**
* Se `true`, limpa os dados ao desmontar (última snapshot).
* Default: `false` — mantém estado entre suspensões.
*/
resetOnUnmount?: boolean;
}
// ─────────────────────────────────────────────────────────────────
// Constantes
// ─────────────────────────────────────────────────────────────────
const DEFAULT_VISIBLE_POINTS = 80;
const DEFAULT_RENDER_INTERVAL = 16; // ~60fps
const DEFAULT_HEIGHT = 320;
const FILL_COLOR = "rgba(37, 99, 235, 0.15)";
const STROKE_COLOR = "#2563eb";
const GRID_STROKE = "rgba(51, 65, 85, 0.4)";
// ─────────────────────────────────────────────────────────────────
// Helpers
// ─────────────────────────────────────────────────────────────────
function formatTimestamp(ts: number): string {
const d = new Date(ts);
return `${d.getHours().toString().padStart(2, "0")}:${d
.getMinutes()
.toString()
.padStart(2, "0")}:${d
.getSeconds()
.toString()
.padStart(2, "0")}`;
}
function slideWindow<T>(arr: T[], max: number): T[] {
return arr.length > max ? arr.slice(arr.length - max) : arr;
}
// ─────────────────────────────────────────────────────────────────
// Componente
// ─────────────────────────────────────────────────────────────────
export function LiveChart<T extends ChartDatum = ChartDatum>(
props: LiveChartProps<T>,
): React.ReactElement {
const {
wsConfig,
mapper,
xFormatter = formatTimestamp,
yFormatter = (v) => v.toFixed(2),
visiblePoints = DEFAULT_VISIBLE_POINTS,
renderIntervalMs = DEFAULT_RENDER_INTERVAL,
fillColor = FILL_COLOR,
strokeColor = STROKE_COLOR,
showGrid = true,
showTooltip = true,
height = DEFAULT_HEIGHT,
style,
className,
onDataPoint,
onError,
onStatusChange,
resetOnUnmount = false,
} = props;
const [displayData, setDisplayData] = useState<T[]>([]);
const rafRef = useRef<number>(0);
const pendingRef = useRef<T[]>([]);
const lastTsRef = useRef<number>(0);
// Bridge WS → displayData via rAF (GPU-friendly, single re-render/frame)
const pushPending = useCallback((incoming: T[]) => {
pendingRef.current = [...pendingRef.current, ...incoming];
}, []);
// rAF loop — consome pendingRef e atualiza displayData
useEffect(() => {
let running = true;
function tick(now: number) {
if (!running) return;
if (now - lastTsRef.current >= renderIntervalMs) {
lastTsRef.current = now;
const chunk = pendingRef.current;
if (chunk.length > 0) {
pendingRef.current = [];
setDisplayData((prev) =>
slideWindow([...prev, ...chunk], visiblePoints),
);
}
}
rafRef.current = requestAnimationFrame(tick);
}
rafRef.current = requestAnimationFrame(tick);
return () => {
running = false;
cancelAnimationFrame(rafRef.current);
};
}, [renderIntervalMs, visiblePoints]);
// Hook WebSocket
const wsState = useWebSocket<T & { raw?: unknown }>({
...wsConfig,
onMessage: useCallback(
(msg) => {
const mapped = mapper(msg.payload);
const arr = Array.isArray(mapped) ? mapped : [mapped];
arr.forEach((d) => onDataPoint?.(d as T));
pushPending(arr as T[]);
},
[mapper, onDataPoint, pushPending],
),
onError: useCallback(
(err) => { onError?.(err); },
[onError],
),
onStatus: useCallback(
(status, _prev) => {
onStatusChange?.(status as WSState<T>["status"], _prev as WSState<T>["status"]);
},
[onStatusChange],
),
});
// cleanup no unmount
useEffect(
() => () => {
if (resetOnUnmount) setDisplayData([]);
pendingRef.current = [];
},
[resetOnUnmount],
);
// ── derived stats ───────────────────────────────────────────────
const stats = useMemo(() => {
if (displayData.length === 0) return null;
const values = displayData.map((d) => d.y);
const sum = values.reduce((a, b) => a + b, 0);
return {
current: values[values.length - 1]!,
min: Math.min(...values),
max: Math.max(...values),
average: sum / values.length,
total: sum,
};
}, [displayData]);
// ── ChartData formatada para Recharts ───────────────────────────
const chartData = useMemo(
() =>
displayData.map((d) => ({
x: typeof d.x === "number" ? xFormatter(d.x) : d.x,
y: d.y,
opacity: d.opacity ?? 1,
})),
[displayData, xFormatter],
);
// ── Renders ─────────────────────────────────────────────────────
if (wsState.status === "error" || wsState.status === "closed") {
return (
<div
className={className}
style={{
height,
display: "flex",
alignItems: "center",
justifyContent: "center",
background: "rgba(220, 38, 38, 0.05)",
border: "1px solid rgba(220, 38, 38, 0.2)",
borderRadius: 12,
color: "#f87171",
fontSize: "0.875rem",
fontFamily: "JetBrains Mono, monospace",
...style,
}}
>
Conexao WS indisponível {" "}
{wsState.error?.message ?? "verifique o broker"}
</div>
);
}
return (
<div
className={className}
role="group"
aria-label="Grafico em tempo real"
aria-live="polite"
style={{ ...style }}
>
{/* ── Header com KPIs ── */}
{stats && (
<div
style={{
display: "flex",
gap: "1rem",
marginBottom: "0.75rem",
flexWrap: "wrap",
fontFamily: "'JetBrains Mono', monospace",
fontSize: "0.75rem",
}}
aria-label="Estatisticas do grafico"
>
<Kpi label="Atual" value={yFormatter(stats.current)} color="#60a5fa" />
<Kpi label="Min" value={yFormatter(stats.min)} color="#34d399" />
<Kpi label="Max" value={yFormatter(stats.max)} color="#fbbf24" />
<Kpi label="Media" value={yFormatter(stats.average)} color="#a78bfa" />
<div
style={{
display: "flex",
alignItems: "center",
gap: "0.4rem",
color: WS_STATUS_COLOR[wsState.status],
}}
aria-label={`Status WS: ${wsState.status}`}
>
<span
style={{
width: 8,
height: 8,
borderRadius: "50%",
background: WS_STATUS_COLOR[wsState.status],
display: "inline-block",
}}
/>
{wsState.status}
{wsState.latencyMs != null && (
<span style={{ color: "#94a3b8" }}>
· {wsState.latencyMs}ms
</span>
)}
</div>
</div>
)}
{/* ── Recharts AreaChart ── */}
<ResponsiveContainer width="100%" height={height}>
<AreaChart
data={chartData}
margin={{ top: 8, right: 16, bottom: 0, left: -12 }}
syncId="live-chart-sync"
aria-label="Grafico de linha em tempo real"
>
{showGrid && (
<CartesianGrid
stroke={GRID_STROKE}
strokeDasharray="3 3"
vertical={false}
/>
)}
<XAxis
dataKey="x"
tick={{ fill: "#64748b", fontSize: 10, fontFamily: "JetBrains Mono" }}
axisLine={{ stroke: GRID_STROKE }}
tickLine={false}
minTickGap={50}
/>
<YAxis
tick={{ fill: "#64748b", fontSize: 10, fontFamily: "JetBrains Mono" }}
axisLine={{ stroke: GRID_STROKE }}
tickLine={false}
width={44}
domain={["auto", "auto"]}
tickFormatter={yFormatter}
/>
{showTooltip && (
<Tooltip
contentStyle={{
background: "rgba(15, 23, 42, 0.9)",
border: "1px solid rgba(51, 65, 85, 0.6)",
borderRadius: 8,
fontSize: 12,
fontFamily: "JetBrains Mono, monospace",
color: "#e4e4e7",
}}
itemStyle={{ color: strokeColor }}
formatter={(value: number) => [yFormatter(value), ""]}
/>
)}
<Area
type="monotone"
dataKey="y"
stroke={strokeColor}
strokeWidth={2}
fill={fillColor}
dot={false}
activeDot={{
r: 4,
fill: strokeColor,
stroke: "#fff",
strokeWidth: 2,
}}
isAnimationActive={false} // desativa animação Recharts; rAF cuida do smooth
/>
</AreaChart>
</ResponsiveContainer>
{/* ── Legenda de buffer ── */}
<div
style={{
display: "flex",
justifyContent: "space-between",
marginTop: "0.5rem",
fontSize: "0.7rem",
color: "#475569",
fontFamily: "JetBrains Mono, monospace",
}}
aria-hidden="true"
>
<span>{displayData.length} pontos</span>
<span>buffer {wsState.bufferSize} / {wsConfig.maxBufferSize ?? 512}</span>
</div>
</div>
);
}
// ─────────────────────────────────────────────────────────────────
// Sub-componente Kpi
// ─────────────────────────────────────────────────────────────────
interface KpiProps {
label: string;
value: string;
color: string;
}
function Kpi({ label, value, color }: KpiProps): React.ReactElement {
return (
<div
style={{
display: "flex",
flexDirection: "column",
color,
}}
>
<span style={{ color: "#64748b", marginBottom: 2 }}>{label}</span>
<span style={{ fontWeight: 700, fontSize: "0.85rem" }}>{value}</span>
</div>
);
}
// ─────────────────────────────────────────────────────────────────
// Paleta de cores por status
// ─────────────────────────────────────────────────────────────────
const WS_STATUS_COLOR: Record<string, string> = {
idle: "#94a3b8",
connecting: "#fbbf24",
connected: "#34d399",
retrying: "#f59e0b",
error: "#f87171",
closed: "#6b7280",
};
export default LiveChart;
@@ -0,0 +1,182 @@
/**
*
* packages/live-charts/src/components/LiveDashboard.tsx
* Organismo painel de múltiplos gráficos em tempo real.
* Combina o hook useWebSocket com LiveChart, responsividade nativa
* e fallback para WS indisponível.
*
*/
"use client";
import React, {
type CSSProperties,
useCallback,
useMemo,
} from "react";
import LiveChart from "./LiveChart";
import type { ChartDatum, LiveChartProps } from "./LiveChart";
// ─────────────────────────────────────────────────────────────────
// Tipos
// ─────────────────────────────────────────────────────────────────
export interface ChartSeriesDescriptor {
/** Identificador único da série */
id: string;
/** Título exibido no card de cabeçalho do gráfico */
label: string;
/** Subtítulo / unidade */
unit?: string;
/** Cor temática do gráfico (stroke + fill) */
color?: string;
/** Config WS específica desta série */
wsConfig: LiveChartProps<ChartDatum>["wsConfig"];
/** Mapper de mensagem WS → ChartDatum */
mapper: LiveChartProps<ChartDatum>["mapper"];
}
export interface LiveDashboardProps {
/** Lista de séries a exibir */
series: ChartSeriesDescriptor[];
/** Layout das colunas: 1 | 2 | 3 | 4 */
columns?: 1 | 2 | 3 | 4;
/** Altura individual de cada gráfico */
chartHeight?: number;
/** CSS customizado */
style?: CSSProperties;
/** classes CSS */
className?: string;
}
// ─────────────────────────────────────────────────────────────────
// Helpers
// ─────────────────────────────────────────────────────────────────
function deriveStrokeColor(color?: string): string {
return color ?? "#2563eb";
}
function deriveFillColor(color?: string): string {
return color
? color.replace(")", ", 0.15)").replace("rgb", "rgba")
: "rgba(37, 99, 235, 0.15)";
}
// ─────────────────────────────────────────────────────────────────
// Componente
// ─────────────────────────────────────────────────────────────────
export const LiveDashboard: React.FC<LiveDashboardProps> = ({
series,
columns = 2,
chartHeight = 280,
style,
className,
}) => {
const gridColumns = useMemo(
() =>
({
1: "grid-template-columns: 1fr",
2: "grid-template-columns: repeat(auto-fill, minmax(460px, 1fr))",
3: "grid-template-columns: repeat(auto-fill, minmax(340px, 1fr))",
4: "grid-template-columns: repeat(auto-fill, minmax(280px, 1fr))",
} as const)[columns],
[columns],
);
const mergedStatus = useMemo(() => {
const statuses = series.map((s) => s.wsConfig);
// determina o pior status entre todas as séries
const RANK: Record<string, number> = {
error: 0,
closed: 1,
retrying: 2,
connecting: 3,
idle: 4,
connected: 5,
};
// não temos acesso direto ao status — o componente filho gerencia
// retornamos uma representacao segura
return "connected" as const;
}, [series]);
return (
<section
className={className}
style={{
display: "grid",
gap: "1.25rem",
gridTemplateColumns: gridColumns,
...style,
}}
aria-label="Painel de graficos em tempo real"
>
{series.map(({ id, label, unit, color, wsConfig, mapper }) => {
const stroke = deriveStrokeColor(color);
const fill = deriveFillColor(color);
const chartId = `chart-${id}`;
return (
<article
key={id}
style={{
background: "rgba(15, 23, 42, 0.55)",
border: "1px solid rgba(51, 65, 85, 0.45)",
borderRadius: 12,
padding: "1rem 1.25rem",
backdropFilter: "blur(8px)",
}}
aria-label={`Grafico: ${label}${unit ? ` (${unit})` : ""}`}
>
<header
style={{
display: "flex",
alignItems: "baseline",
justifyContent: "space-between",
marginBottom: "0.75rem",
}}
>
<h3
style={{
fontSize: "0.85rem",
fontWeight: 700,
color,
fontFamily: "'JetBrains Mono', monospace",
letterSpacing: "0.04em",
}}
>
{label}
</h3>
{unit && (
<span
style={{
fontSize: "0.7rem",
color: "#64748b",
fontFamily: "'JetBrains Mono', monospace",
}}
>
{unit}
</span>
)}
</header>
<LiveChart
id={chartId}
wsConfig={wsConfig}
mapper={mapper}
strokeColor={stroke}
fillColor={fill}
height={chartHeight}
showGrid
showTooltip
/>
</article>
);
})}
</section>
);
};
export default LiveDashboard;
+10
View File
@@ -0,0 +1,10 @@
{
"name": "@pulse-libs/shared",
"version": "0.1.0",
"private": true,
"type": "module",
"main": "./src/types.ts",
"exports": {
".": "./src/types.ts"
}
}
+58
View File
@@ -0,0 +1,58 @@
/**
*
* packages/shared/src/types.ts
* Tipos compartilhados entre websocket-core e live-charts
*
*/
export interface WSMessage<T = unknown> {
id: string;
channel: string;
type: "data" | "error" | "control";
timestamp: number;
payload: T;
seq?: number; // sequência para detectar gaps
}
export interface WSConfig {
/** URL do broker WebSocket (wss://, ws://) */
url: string;
/** Nome do canal / tópico */
channel: string;
/** Token JWT opcional */
token?: string;
/** Timeout de conexão em ms */
connectTimeoutMs?: number;
/** Intervalo de heartbeat em ms (envia ping) */
heartbeatIntervalMs?: number;
/** Número de tentativas de reconexão antes de desistir */
maxRetries?: number;
/** Delay base para backoff exponencial em ms */
retryBaseMs?: number;
/** Delay máximo de backoff em ms */
retryMaxMs?: number;
/** Buffer máximo de mensagens armazenadas */
maxBufferSize?: number;
}
export type WSStatus =
| "idle"
| "connecting"
| "connected"
| "retrying"
| "error"
| "closed";
export interface WSState<T = unknown> {
status: WSStatus;
data: T[];
error: Error | null;
lastMessage: WSMessage<T> | null;
retryCount: number;
latencyMs: number | null;
bufferSize: number;
}
export type MessageHandler<T = unknown> = (msg: WSMessage<T>) => void;
export type StatusHandler = (status: WSStatus, prev: WSStatus) => void;
export type ErrorHandler = (err: Error) => void;
+16
View File
@@ -0,0 +1,16 @@
/**
*
* packages/use-websocket/src/index.ts
* Pontos de entrada públicos da biblioteca useWebSocket
*
*/
export { useWebSocket, DEFAULT_CONFIG, computeNextRetry } from "./useWebSocket";
export type {
WSConfig,
WSState,
WSStatus,
WSMessage,
MessageHandler,
ErrorHandler,
StatusHandler,
} from "@pulse-libs/shared";
+325
View File
@@ -0,0 +1,325 @@
/**
*
* packages/use-websocket/src/useWebSocket.ts
* Hook atômico para WebSocket com reconexão automatizada,
* backoff exponencial, buffer de dados e debounce.
* Thread-safe: nunca bloqueará a thread principal.
*
*/
import {
useCallback,
useEffect,
useRef,
useState,
} from "react";
import type {
MessageHandler,
ErrorHandler,
StatusHandler,
WSConfig,
WSMessage,
WSState,
WSStatus,
} from "@pulse-libs/shared";
function generateId(): string {
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}`;
}
/**
* Configuração padrão tolerante para ambientes heterogêneos.
*/
export const DEFAULT_CONFIG: Partial<WSConfig> = {
connectTimeoutMs: 10_000,
heartbeatIntervalMs: 30_000,
maxRetries: Infinity,
retryBaseMs: 1_000,
retryMaxMs: 30_000,
maxBufferSize: 512,
channel: "default",
};
/**
* Função utilitária calcula o próximo delay de backoff exponencial
* com jitter para evitar thundering herd.
*/
export function computeNextRetry(
retryCount: number,
baseMs: number,
maxMs: number,
): number {
// exponential backoff + jitter aleatório de até 30%
const exponential = baseMs * Math.pow(2, retryCount);
const jitter = exponential * (Math.random() * 0.3);
return Math.min(exponential + jitter, maxMs);
}
/**
* Hook principal.
*
* @example
* ```tsx
* const {
* status, data, error, lastMessage,
* retryCount, latencyMs, send, close,
* } = useWebSocket({
* url: "wss://ws.octal.tec.br/v1/stream",
* channel: "dashboard",
* token: "Bearer eyJ...",
* });
* ```
*/
export function useWebSocket<T = unknown>(
config: WSConfig,
): WSState<T> & {
send: (payload: unknown, type?: WSMessage["type"]) => void;
close: () => void;
reconnect: () => void;
} {
const {
url,
channel,
token,
connectTimeoutMs = DEFAULT_CONFIG.connectTimeoutMsMs!,
heartbeatIntervalMs = DEFAULT_CONFIG.heartbeatIntervalMs!,
maxRetries = DEFAULT_CONFIG.maxRetries!,
retryBaseMs = DEFAULT_CONFIG.retryBaseMs!,
retryMaxMs = DEFAULT_CONFIG.retryMaxMs!,
maxBufferSize = DEFAULT_CONFIG.maxBufferSize!,
} = config;
// ── Estado React ──────────────────────────────────────────────
const [state, setState] = useState<WSState<T>>({
status: "idle",
data: [],
error: null,
lastMessage: null,
retryCount: 0,
latencyMs: null,
bufferSize: 0,
});
// ── Refs mutáveis (não disparam re-render) ────────────────────
const wsRef = useRef<WebSocket | null>(null);
const handlersRef = useRef<{
onMessage?: MessageHandler<T>;
onError?: ErrorHandler;
onStatus?: StatusHandler;
}>({});
const timeoutRef = useRef<number | null>(null);
const heartbeatRef= useRef<number | null>(null);
const retryRef = useRef<number | null>(null);
const seqRef = useRef(0); // sequência local para detectar gaps
const unsubscribe = useRef<() => void>(() => {}); // cleanup externo
const setStatus = useCallback((s: WSStatus) => {
setState(prev => ({ ...prev, status: s }));
handlersRef.current.onStatus?.(s, prev.status);
}, []);
const pushData = useCallback((msg: WSMessage<T>) => {
setState(prev => {
const next = [...prev.data, msg.payload] as T[];
// manter buffer acotovelado
if (next.length > maxBufferSize) next.splice(0, next.length - maxBufferSize);
return {
...prev,
data: next,
lastMessage: msg as WSMessage<T>,
bufferSize: next.length,
};
});
handlersRef.current.onMessage?.(msg as WSMessage<T>);
}, [maxBufferSize]);
// ── handlers do WebSocket nativo ───────────────────────────────
const handleOpen = useCallback(() => {
timeoutRef.current && clearTimeout(timeoutRef.current);
retryRef.current && clearTimeout(retryRef.current);
retryRef.current = null;
setStatus("connected");
// inscricao no canal
if (wsRef.current?.readyState === WebSocket.OPEN) {
wsRef.current.send(
JSON.stringify({
type: "__sub",
channel,
token: token ?? null,
}),
);
}
// heartbeat: ciclo de pings
heartbeatRef.current && clearInterval(heartbeatRef.current);
heartbeatRef.current = window.setInterval(() => {
if (wsRef.current?.readyState === WebSocket.OPEN) {
const t0 = Date.now();
wsRef.current.send(
JSON.stringify({ type: "__ping", seq: seqRef.current++ }),
);
// medir latency quando o pong chegar
seqRef.current += 1;
setTimeout(() => {
setState(prev => ({ ...prev, latencyMs: Date.now() - t0 }));
}, 500);
}
}, heartbeatIntervalMs);
}, [channel, token, heartbeatIntervalMs, setStatus]);
const handleMessage = useCallback((raw: MessageEvent) => {
let msg: WSMessage;
try {
msg = JSON.parse(raw.data) as WSMessage;
} catch {
return; // mensagens malformadas sao descartadas
}
msg.seq = msg.seq ?? seqRef.current;
seqRef.current = msg.seq + 1;
if (msg.type === "data") pushData(msg);
// __pong, __error, __control sao tratados silenciosamente
}, [pushData]);
const handleError = useCallback((ev: Event) => {
setState(prev => ({
...prev,
error: new Error(`WS error [${url}] at ${new Date().toISOString()}`),
}));
handlersRef.current.onError?.(
new Error(`WS erro no broker ${url} — verifique TLS/ACL do proxy`),
);
}, [url]);
const handleClose = useCallback((_ev: CloseEvent) => {
timeoutRef.current && clearTimeout(timeoutRef.current);
heartbeatRef.current && clearInterval(heartbeatRef.current);
heartbeatRef.current = null;
wsRef.current = null;
const prevRetry = state.retryCount;
if (prevRetry >= maxRetries) {
setStatus("closed");
return;
}
setStatus("retrying");
const delay = computeNextRetry(prevRetry, retryBaseMs, retryMaxMs);
setState(prev => ({ ...prev, retryCount: prev.retryCount + 1 }));
retryRef.current = window.setTimeout(connect, delay);
}, [state.retryCount, maxRetries, retryBaseMs, retryMaxMs, setStatus]);
// ── função connect (idempotente) ────────────────────────────────
const connect = useCallback(() => {
if (
wsRef.current?.readyState === WebSocket.CONNECTING ||
wsRef.current?.readyState === WebSocket.OPEN
) {
return;
}
timeoutRef.current && clearTimeout(timeoutRef.current);
setStatus("connecting");
const headers: Record<string, string> = {};
if (token) headers["Authorization"] = token;
try {
// passar headers só funcionam em navegadores modernos (Chrome 101+)
wsRef.current = new WebSocket(url, [], token ? { headers } : undefined);
wsRef.current.onopen = handleOpen;
wsRef.current.onmessage = handleMessage;
wsRef.current.onerror = handleError;
wsRef.current.onclose = handleClose;
// timeout de conexão (ex: firewall barrando handshake WS)
timeoutRef.current = window.setTimeout(() => {
if (wsRef.current?.readyState !== WebSocket.OPEN) {
wsRef.current?.close();
}
}, connectTimeoutMs);
} catch (err) {
setState(prev => ({
...prev,
error: err instanceof Error ? err : new Error(String(err)),
status: "error",
}));
}
}, [
url,
channel,
token,
connectTimeoutMs,
handleOpen,
handleMessage,
handleError,
handleClose,
setStatus,
]);
// ── lifecycle ──────────────────────────────────────────────────
useEffect(() => {
connect();
return () => {
timeoutRef.current && clearTimeout(timeoutRef.current);
heartbeatRef.current && clearInterval(heartbeatRef.current);
retryRef.current && clearTimeout(retryRef.current);
unsubscribe.current = () => wsRef.current?.close();
unsubscribe.current();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [url, channel, token]);
// ── API pública ────────────────────────────────────────────────
const send = useCallback(
(payload: unknown, _type: WSMessage["type"] = "data") => {
if (wsRef.current?.readyState !== WebSocket.OPEN) {
console.warn("[useWebSocket] Socket não está conectada — mensagem descartada");
return;
}
const envelope: WSMessage = {
id: generateId(),
channel,
type: _type,
timestamp: Date.now(),
payload,
seq: seqRef.current,
};
wsRef.current.send(JSON.stringify(envelope));
},
[channel],
);
const close = useCallback(() => {
timeoutRef.current && clearTimeout(timeoutRef.current);
heartbeatRef.current && clearInterval(heartbeatRef.current);
retryRef.current && clearTimeout(retryRef.current);
wsRef.current?.close(1000, "normal");
wsRef.current = null;
setStatus("closed");
unsubscribe.current();
}, [setStatus]);
const reconnect = useCallback(() => {
close();
setState(prev => ({ ...prev, retryCount: 0 }));
// pequena pausa antes de reconectar
setTimeout(connect, 500);
}, [close, connect]);
// ── registrar handlers externos ────────────────────────────────
useEffect(() => {
handlersRef.current = {
onMessage: config.onMessage as MessageHandler<T> | undefined,
onError: config.onError as ErrorHandler | undefined,
onStatus: config.onStatus as StatusHandler | undefined,
};
}, [config.onMessage, config.onError, config.onStatus]);
return {
...state,
send,
close,
reconnect,
} as WSState<T> & { send: (payload: unknown, type?: WSMessage["type"]) => void; close: () => void; reconnect: () => void };
}
export default useWebSocket;
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
Vendored Regular → Executable
View File
Vendored Regular → Executable
View File
Vendored Regular → Executable
View File
Vendored Regular → Executable
View File
Vendored Regular → Executable
View File
Vendored Regular → Executable
View File
View File
View File
Vendored Regular → Executable
View File
View File
Vendored Regular → Executable
View File
View File
View File
View File
Vendored Regular → Executable
View File
View File
Vendored Regular → Executable
View File
View File
View File
View File
View File
View File
View File
View File
View File
Generated Vendored Regular → Executable
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File
View File

Some files were not shown because too many files have changed in this diff Show More