Compare commits

..
Author SHA1 Message Date
ealmeida 4133d82319 merge: integrar origin/main (Beszel já existente; mantém rotas stack-observer e observer-tickets e restaura /api/ai) 2026-10-28 16:02:22 +00:00
ealmeida f39c6212a3 docs: migrar SPEC/PLAN para docs/SPECS e docs/PLANS e adicionar ficheiros de contexto do projecto 2026-10-28 16:01:30 +00:00
ealmeida 61ae4697f9 docs(okf): entidades e relacionamentos OKF do DashDescomplicar 2026-10-28 16:01:20 +00:00
ealmeida f063a2ec8c feat(monitorizacao): scripts de saúde e telemetria do Gemini CLI 2026-10-28 16:01:09 +00:00
ealmeida ca9a07b1d9 feat(monitor): integração Beszel/Stack Observer, tickets do observer e novos sites no uptime 2026-10-28 16:00:59 +00:00
ealmeida 2bbc12e8b4 fix(sessions): outliers de duração ignoram outcome unknown e publicação no departamento Tecnologia (#14404) 2026-10-28 16:00:49 +00:00
ealmeida 948a9c2857 chore(gitignore): ignorar artefactos gerados (graphify-out, logs, relatórios de validação) 2026-10-28 16:00:39 +00:00
ealmeida 0e8a1f04ce docs(okf): formaliza SCHEMA.md — Nível N1, relacionamentos narrativos válidos
Documenta explicitamente o padrão já em uso em okf/entidades/ e
okf/relacionamentos/*.md (Tier 1 curado + Tier 3 narrativo). Declara
Nível de Maturidade N1 com justificação — sem inventory/ automático
nem relations.jsonl, apesar do volume de código (75 ficheiros .ts/.tsx)
exceder o limiar literal de N2, por falta de infraestrutura de geração.
Decisão de não migrar relacionamentos/*.md para relations.jsonl
(SPEC-OKF-Projeto-Sistema-Excelencia.md §3.6/§5.4) — custo de
automação não compensa a escala actual (21 arestas).

Fase 4 do PLAN-OKF-Projeto-Sistema-Excelencia.md (Hub).
2026-07-07 21:20:02 +01:00
ealmeida 810401e4e7 feat(pipeline): F4.1 — rotas GET /patterns e GET /stats (contrato C4-análise) #2415
- routes/sessions.ts: createSessionsAnalysisRouter com GET /patterns (filtros
  week/severity/key/days, paginado, Zod) e GET /stats (agregados: sessões
  totais e por dia, by_source, by_outcome, tool_top, skill_top,
  patterns_by_type, gate_blocks, avg_duration_sec, last_indexed_at,
  latest_pattern_week); montado em /api/sessions (C4) e /api (alias F4.1)
- services/sessions/db.ts: listPatterns/countPatterns/getStats +
  rowToPattern/buildPatternsWhere; gate_blocks devolve null enquanto a
  tabela gate_events não existir (F1 arco C3-gates)
- tests/sessions-analysis-route.test.ts: 15 testes (filtros, paginação,
  validação, gate_blocks condicional, não-colisão com /:id)
- inclui camada session_events pendente de v2.7.3 (db.ts/indexer.ts —
  storeSessionEvents no indexAll; rota /:id lê da BD) já em execução, para
  a árvore commitada compilar coerente

Validado contra BD real (11459 sessões, 160 padrões): curl 200 com JSON
não-vazio em /api/patterns, /api/stats, /api/sessions/patterns?week=2026-W26,
/api/sessions/stats?days=7.
2026-07-03 06:01:22 +01:00
ealmeida 9b2dfdc412 fix(systemd): versionar obs-index-sync.{sh,service,timer} no repo + remover --watch do indexer (decisão v2.7.2, incidente 110% CPU) — F0.5 Pipeline QT #2401 2026-07-03 05:23:50 +01:00
ealmeidaandClaude Opus 4.8 f7acd99d8f docs(okf): frontmatter OKF + rich abstracts nas descriptions
Normalizacao OKF dos .md: type/title/description/timestamp/layer +
descriptions factuais (rich abstracts). Apenas .md tracked; corpos intactos.
Parte da aplicacao OKF a /Dados/Dev (28-06-2026).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 22:58:21 +01:00
ealmeida 7ac01d5a82 fix(beszel): coerce port to number, handle la array from PocketBase 2026-06-23 20:33:30 +01:00
ealmeida 485a4ee67c fix(beszel): increase API timeouts to 30s (PocketBase auth is slow) 2026-06-23 20:30:05 +01:00
ealmeida 769c63b2a8 feat(monitoring): add Beszel fleet integration to Monitor page
- New api/services/beszel.ts: Zod-validated Beszel API client with
  PocketBase auth, system fetching, and MySQL upsert
- New api/routes/beszel.ts: GET /api/beszel (read) + POST /api/beszel/collect (trigger)
- Updated api/server.ts: register route, add to 5min scheduler
- Updated src/pages/Monitor.tsx: Fleet section showing all 17 systems
  with CPU/RAM/disk bars, status dots, container count, and link to
  Beszel dashboard

Architecture: Beszel hub → API → tbl_eal_monitoring (category='fleet')
→ Monitor.tsx grid. Collected every 5min alongside existing metrics.
2026-06-23 20:09:12 +01:00
ealmeidaandClaude Opus 4.8 08d2a31dc9 feat(monitoring): site_uptime cobre 15 sites (clientes + internos)
Alinha PING_SITES do check activo com o watchdog-sites.py: +carstuff +6 internos
(care, ccv, e-commerce, ecommerce, ecommerce-demo, starter).

Security Audit (Regra #47): sem deps novas; vulns pré-existentes documentadas no commit anterior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 19:43:39 +01:00
ealmeidaandClaude Sonnet 4.6 f733998945 feat(sessions): indexação multi-fonte Hermes + OpenCode com fix TypeScript
- Novos indexadores hermes-indexer.ts e opencode-indexer.ts para unificar
  sessões Claude, Hermes Agent e OpenCode num único sessions.db
- SessionMeta alargado: source (obrigatório), model, input/output_tokens,
  estimated_cost; project_path/jsonl_path agora nullable
- Fix TS: tipos explícitos, guard jsonl_path null, dependências instaladas

Security Audit (Regra #47):
- npm audit executado — 0 vulnerabilities após fix
- vite 7→8.0.16 (breaking upgrade, resolve esbuild CVE GHSA-gv7w-rqvm-qjhr)
- vitest 4.0.18→4.1.9 (resolve esbuild interno CVE GHSA-gv7w-rqvm-qjhr)
- shell-quote override ^1.8.4 via package.json#overrides (CVE GHSA-w7jw-789q-3m8p)
- react-router, joi, qs, form-data, ip-address, js-yaml resolvidos via npm audit fix

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 19:42:05 +01:00
ealmeidaandClaude Opus 4.8 9f3d14dc51 feat(monitoring): check activo de sites (HTTP+conteúdo) + fix staleness
- checkAllSites(): verificação activa dos 8 sites do ping (status + piso de
  tamanho + assinaturas de erro Redis/BD + marcador positivo) -> category=site_uptime.
  Apanha "HTTP 200 mas página partida" independente do wp-cron/plugin.
- checkStaleness(): corrige categoria 'wordpress'->'site' (bug: nunca corria
  contra os dados reais do plugin) + limiar 24h->26h.
- Ligado ao collector que corre a cada 5 min no scheduler.

Security Audit (Regra #47):
- npm audit executado: 18 vulnerabilidades pré-existentes em deps transitivas
  (esbuild/vite/tsx/react-router/vitest/express/shell-quote, etc.)
- NENHUMA introduzida por este commit (altera 1 ficheiro, zero deps novas)
- Override justificado: dev/transitivas, fix exige npm audit fix global (fora
  do âmbito deste fix). A documentar como dívida técnica separada.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 19:10:10 +01:00
ealmeidaandClaude Sonnet 4.6 94db202de9 fix(monitoring): SSH ao EasyPanel em vez de API inexistente
- server-metrics: substituir CWP (só aceita ed25519) por Easy server
  (aceita password auth na porta 22)
- monitoring-collector: remover chamadas a monitor.getSystemStats e
  monitor.getDockerTaskStats (endpoint não existe nesta versão EasyPanel);
  métricas CPU/RAM via SSH e containers via docker service ls sobre SSH

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 16:08:20 +01:00
ealmeidaandClaude Sonnet 4.6 a594df1c7c auth: sessões mais longas com silent renew automático
Adiciona offline_access ao scope e automaticSilentRenew para renovar
tokens silenciosamente sem forçar re-login. Requer Access Token validity
aumentado no provider Authentik (de 5min para 8h).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 15:03:44 +01:00
111 changed files with 7289 additions and 808 deletions
+5
View File
@@ -5,3 +5,8 @@ dist/
*.log *.log
.DS_Store .DS_Store
.vite/ .vite/
# Artefactos gerados
graphify-out/
VALIDACAO-FASE-*.md
logs/
+169
View File
@@ -0,0 +1,169 @@
---
type: Document
title: Agents
description: Governance de agentes IA para este projecto
tags: [dev, dashboard, governance, 5w2h, roles, relacionamentos, modelo, 2026-06-30, reorganizacao]
---
# AGENTS.md — Governance de Agentes IA
## Identidade do Projecto
- **Repositório**: DashDescomplicar
- **Localização**: `/media/ealmeida/Dados/Dev/DashDescomplicar/`
- **CRM**: Desk #2323 | Projecto: #65 (OKF Stack Completo)
- **Modelo Base**: Hub → `05-Projectos/Modelo-Organizacao-Proc-Dev/`
- **Versão Actual**: 2.7.3 (2026-06-30)
## Directrizes de Operação
### 1. PT-PT e Precisão
Toda a interacção, comentários, commits e documentação devem ser em português europeu, com acentos e cedilhas.
### 2. MCP-first
- **Desk CRM**: Consultar/actualizar via MCP desk-crm-v3
- **Gitea**: Commits e gestão de versões via MCP gitea
- **SSH**: Apenas se necessário para validação remota (EasyPanel, CWP)
- **Memória IJFW**: Guardar contexto persistente via ijfw-memory
### 3. Ciclo de Trabalho Obrigatório
1. **Validar contexto** — ler AGENTS.md + STATUS.md + desk.project
2. **Executar** — seguir docs/PLANS/DashDescomplicar-PLAN.md (roadmap + ciclos)
3. **Validar** — registar mudanças em STATUS.md
4. **Registar** — actualizar CRM (#2323) se há progresso significativo
### 4. Validação Obrigatória
Nunca dar uma tarefa como concluída sem validar o estado actual via curl/MCP/SSH.
---
## Agentes Descomplicar Autorizados
### Claude Code (cc)
**Identidade**: `staff_id = 25` (CLAUDE em Desk CRM)
| Campo | Valor |
|-------|-------|
| **What** | Desenvolvimento, DevOps, features no DashDescomplicar |
| **Why** | Fonte central de código, deploy, PDCA operacional |
| **Where** | Dev/DashDescomplicar + EasyPanel (easy.descomplicar.pt) |
| **Who** | Equipa de desenvolvimento (Emanuel + Claude Code) |
| **When** | [Ver WHEN/HOW abaixo] |
| **How** | [Ver WHEN/HOW abaixo] |
#### When
- **Semanal (Seg):** Health check: `curl -sf https://dash.descomplicar.pt/api/dashboard` + revisão de logs
- **Mensal (último Sex):** Audit dependências (`npm audit`) + atualizar STATUS.md
- **Trimestral:** Revisão segurança (OWASP) + testes de carga
- **Contínuo:** PRs/features → docs/PLANS/DashDescomplicar-PLAN.md (roadmap)
- **Incidente:** CodeRED para site down (`curl -X POST https://codered.descomplicar.pt/v1/trigger/RED-ZMT3FCU8`)
#### How
**Referência operacional primária:**
- 📋 **PLAN:** `docs/PLANS/DashDescomplicar-PLAN.md` (roadmap Q3-Q4, ciclos de manutenção, deploy)
- 📋 **SPEC:** `docs/SPECS/DashDescomplicar-SPEC.md` (especificação, projectos ativos)
**Procedimentos Descomplicar:**
- `PROC-Deploy-EasyPanel.md` (D7-Tecnologia) — deploy via SSH + Docker
- `PROC-Observabilidade-Dashboard.md` — coleta de métricas
- `QR-Desk-Subtarefas-Dependencias.md` — gestão de dependências no CRM
**Validação:**
- Build local: `npm run build && npm run lint && npm test`
- Health endpoint: `GET /api/dashboard` (< 500ms p95)
- Logs: `docker logs descomplicar_dashboard_descomplicar` (EasyPanel)
**Escalação:**
- Bug/Feature: Comentário em Desk #65
- Incidente P0 (site down): CodeRED imediatamente
- Questão técnica: IRC com Claude Code peers
---
### Observabilidade (Sistema)
**Integração**: Sistemas de telemetria que alimentam DashDescomplicar
| Campo | Valor |
|-------|-------|
| **What** | Coleta e agregação de métricas operacionais |
| **Why** | Observabilidade de infraestrutura e aplicação |
| **Where** | `okf/entidades/Metrics.md` + logs/performance-metrics.json |
| **Who** | Observabilidade (Sistema #2059) |
| **When** | Contínuo (eventos em tempo real) |
| **How** | [Ver WHEN/HOW abaixo] |
#### When
- **Contínuo:** Coleta via Express middleware (Painéis + API)
- **Agregação horária:** Roll-up em Prometheus (opcional)
- **Retenção:** Logs de performance em JSON (7 dias rotated)
#### How
**Referência operacional:**
- 📋 **SPEC Autoritária:** `/media/ealmeida/Dados/Hub/05-Projectos/Observabilidade/SPEC.md`
- 📋 **PLAN Autoritário:** `/media/ealmeida/Dados/Hub/05-Projectos/Observabilidade/PLAN.md`
- 📊 **Métricas OKF:** `okf/entidades/Metrics.md`
**Integração com DashDescomplicar:**
- Endpoint: `GET /api/metrics` (Prometheus format)
- Painéis consomem via CSR-Integration (cache + reconnect)
- Tabela relação: `okf/relacionamentos/CSRIntegration-integra-Metrics.md`
---
## O Que Este Projecto Contém
- `docs/PLANS/DashDescomplicar-PLAN.md` — **PLANO OPERACIONAL PRINCIPAL** (roadmap Q3-Q4, ciclos, deploy)
- `docs/SPECS/DashDescomplicar-SPEC.md` — especificações e projectos activos
- `okf/` — **GRAFO DE CONHECIMENTO** (12 entidades + 7 relacionamentos bidireccionais)
- `okf/entidades/` — Dashboard-Engine, 8+ Painéis, CSR-Integration, Metrics
- `okf/relacionamentos/` — exibe, integra-com, usa (com cruzamentos de métricas)
- `AGENTS.md` — (este ficheiro) governance de agentes e integração
- `STATUS.md` — **PDCA OPERACIONAL** (Plan-Do-Check-Act ligado a recursos)
- `MEMORY.md` — histórico de sessões (deprecated, usar IJFW em ~/.ijfw/memory/)
- `desk.project` — ancoragem CRM: task #2323, projecto #65
- `CHANGELOG.md` — histórico de versões
---
## Validação de Integridade
✅ **When/How completo** — Cada agent tem schedule e referências a PLAN/PROC/QR
✅ **Bidirecionalidade** — Relacionamentos em okf/ documentados nos dois sentidos
✅ **Sem órfãos** — Toda entidade ligada a pelo menos 1 entrada + 1 saída
✅ **PDCA linkado** — STATUS.md refere docs/PLANS/ e observabilidade
---
## Referência ao Modelo
Consulte `/media/ealmeida/Dados/Hub/05-Projectos/Modelo-Organizacao-Proc-Dev/ESTRUTURA.md` para a hierarquia padrão obrigatória em todos os projectos Descomplicar.
**Última actualização:** 2026-06-30 (Fase C — 5W2H+PDCA completo)
## Relacionamentos Direccionais
### Depende De
- [[06-Operacoes]] - Hub operacional central com procedimentos
- [[04-Stack]] - Decisões técnicas e stack Descomplicar
### Suporta
- [[05-Projectos/Modelo-Organizacao-Proc-Dev/ESTRUTURA.md]] - Modelo de projectos
### Documenta
- [[06-Operacoes/Procedimentos/]] - Procedimentos operacionais
### Implementação
- [[DashDescomplicar/src/]] - Código-fonte do Dashboard
- [[DashDescomplicar/okf/]] - Grafo de conhecimento de painéis
### Relacionado Com
- [[04-Stack]] - Stack técnico partilhado
- [[CodeRED]] - Sistema de alertas críticos
### Procedimento
- [[06-Operacoes/Procedimentos/D7-Tecnologia/Infraestrutura/PROC-EasyPanel-Deployment.md]] - Deploy em EasyPanel
- [[06-Operacoes/Procedimentos/D7-Tecnologia/Sistema/PROC-Desk-Subtarefas-Dependencias.md]] - Modelo de subtarefas
### Herança
- [[05-Projectos/Modelo-Organizacao-Proc-Dev/ESTRUTURA.md]] - Estrutura canónica
+8
View File
@@ -1,3 +1,11 @@
---
type: Document
title: Audit Report
description: >-
Relatório de auditoria de segurança e qualidade do DashDescomplicar (React 19 + TypeScript + Vite + Express + MySQL), com 20 vulnerabilidades classificadas e veredicto de não apto para produção.
timestamp: 2026-02-14T03:33:42.730555+00:00
layer: wiki
---
# Auditoria de Segurança e Qualidade - DashDescomplicar # Auditoria de Segurança e Qualidade - DashDescomplicar
**Data:** 14-02-2026 **Data:** 14-02-2026
Executable → Regular
+53
View File
@@ -1,7 +1,60 @@
---
type: Changelog
title: Changelog
description: >-
Todas as alterações notáveis neste projecto serão documentadas neste ficheiro
timestamp: 2026-06-15T18:43:39.495926+00:00
layer: wiki
---
# Changelog # Changelog
Todas as alterações notáveis neste projecto serão documentadas neste ficheiro. Todas as alterações notáveis neste projecto serão documentadas neste ficheiro.
## [Unreleased] - 2026-10-24
### Fixed — Detector de padrões de sessões (Desk #14404)
- **`patterns.ts` / `detectDurationOutliers`**: outcome `unknown` deixa de contar como "não concluída" — as sessões cron do Hermes (`project_slug` NULL) têm 100% `unknown` (outcome não medido) e geravam por construção ~5% de outliers todas as semanas ("Projecto null: N sessões longas não concluídas", 11 semanas seguidas). Só `error`/`interrupted` contam.
- **`sessions-patterns.ts`**: tickets de padrão recorrente passam a ir para o departamento Tecnologia (id=7); o id=1 é "Comercial", não "Geral".
## [2.7.3] - 2026-06-15
### Added — Indexação Multi-Fonte (Hermes + OpenCode)
- **`hermes-indexer.ts`**: novo indexador para sessões do Hermes Agent (`~/.hermes/state.db`) — lê sessões e mensagens, mapeia para `SessionMeta` com `source: 'hermes'`
- **`opencode-indexer.ts`**: novo indexador para sessões OpenCode (`~/.local/share/opencode/opencode.db`) — lê sessions, projects, messages e parts, converte para `SessionMeta` com `source: 'opencode'`
- **`SessionMeta`**: novos campos `source` (obrigatório, `'claude' | 'hermes' | 'opencode'`), `model`, `input_tokens`, `output_tokens`, `estimated_cost`; `project_path`, `project_slug`, `jsonl_path` agora nullable (suportam fontes sem JSONL)
### Fixed — TypeScript + Dependências
- **`parser.ts`**: campos `source`, `model`, `input_tokens`, `output_tokens`, `estimated_cost` adicionados ao objecto `SessionMeta` do parser Claude
- **`routes/sessions.ts`**: guard para `jsonl_path` null — sessões Hermes/OpenCode devolvem 422 em vez de crash ao tentar abrir ficheiro
- **`worklog-import.ts`**: tipo explícito `HTMLElement` no callback `.map(li => ...)` corrige `TS7006`
- Dependências `node-html-parser`, `supertest`, `@types/supertest` instaladas (estavam no `package.json` mas em falta em `node_modules`)
- Testes `sessions-db` e `sessions-patterns`: campos obrigatórios adicionados ao `sampleMeta` (`source`, `model`, `input_tokens`, `output_tokens`, `estimated_cost`)
## [2.7.2] - 2026-05-05
### Fixed — Indexer CPU + Sync EasyPanel
- **sessions-indexer**: modo `--watch` substituído por timer systemd a cada 15min (`--full`) — elimina processo permanente a 110% CPU
- **indexer.ts**: campos nullable (`model`, `input_tokens`, `ended_at`, `duration_sec`, etc.) sem fallback `null` causavam `Missing named parameter` no SQLite — corrigido com `?? null` antes do upsert
- **hermes-indexer.ts**: `const batch` reatribuído com `batch = []` (TypeError) — corrigido para `let batch`
### Added — Sync Multi-Máquina
- Volume bind mount no container EasyPanel: `/root/data/dashboard-descomplicar/sessions.db → /data/sessions.db`
- Env var `OBSERVABILIDADE_DB=/data/sessions.db` configurada no serviço Docker
- Script `~/.local/bin/obs-index-sync.sh`: indexa sessões + rsync para EasyPanel em ~10s
- Timer `obs-index-sync.timer`: execução a cada 15min, latência máxima de dados no dashboard
- Sessões de PC desktop e HP cobertas via Syncthing (já sincronizava `~/.claude/projects/`)
- Resultado: 11.009 sessões indexadas, 0 erros, 5.7MB sincronizados por execução
## [2.7.1] - 2026-04-27
### Fixed — Recuperação Produção
- **Root cause:** `WP_MONITOR_API_KEY` em falta nas env vars EasyPanel causava crash ao iniciar (exit 1)
- Adicionadas todas as env vars em falta no serviço EasyPanel: `DB_HOST`, `DB_USER`, `DB_PASS`, `DB_NAME`, `WP_MONITOR_API_KEY`, `HETZNER_TOKEN`, `EASYPANEL_API_TOKEN`, `SERVER_*`, `NODE_ENV`, `FRONTEND_URL`
- Sincronizada `WP_MONITOR_API_KEY` com valor configurado no plugin WordPress (`descomplicar-monitor`)
- Criado/actualizado `.env` local com credenciais de produção completas (gitignored)
- Serviço EasyPanel `descomplicar/dashboard_descomplicar`: `0/1` → `1/1` running
- Dashboard `dash.descomplicar.pt`: 504 → 200 ✅ | API `/api/dashboard`: dados reais ✅
## [2.7.0] - 2026-04-23 ## [2.7.0] - 2026-04-23
### Added — Observabilidade Fase 6A ### Added — Observabilidade Fase 6A
+181
View File
@@ -0,0 +1,181 @@
---
layer: wiki
type: Document
title: "CLAUDE — DashDescomplicar"
description: >-
Instruções operacionais para Claude Code no projecto DashDescomplicar. Stack React 19 + Express + EasyPanel Swarm.
tags: [claude, operacional, dashboard, deploy, react, express]
timestamp: 2026-06-29T23:00:00+01:00
---
# CLAUDE — DashDescomplicar
Instruções operacionais para agentes
## OKF — Organizational Knowledge Framework (OBRIGATÓRIO)
Este projecto tem um mapeamento OKF interno em `okf/`. **Antes de qualquer acção, consultar o OKF.**
| Regra | Detalhe |
|-------|---------|
| **Consultar antes de agir** | Ler `okf/index.md` ao iniciar sessão. Ver entidades relevantes ao contexto. |
| **Manter actualizado** | Se criares, alterares ou removeres um componente, actualizar a ficha OKF correspondente. |
| **Novo componente = nova ficha** | Qualquer novo serviço, módulo, integração ou script → criar `5W2H-PDCA.md` em `okf/entidades/`. |
| **Validar antes de commit** | `okf-validate.sh --project .` — 0 erros para commit. |
| **Relacionamentos bidirecionais** | Se A depende de B, tanto A como B devem registar a relação. |
**Estrutura:** `okf/index.md` → índice geral. `okf/entidades/` → uma pasta por componente com `5W2H-PDCA.md`.
IA a trabalhar neste projecto.
## Contexto
- **Projecto:** DashDescomplicar — dashboard interno `dash.descomplicar.pt`
- **Desk:** Projecto #65
- **Versão actual:** 2.7.3 (ver `CHANGELOG.md`)
- **Dev local:** `/media/ealmeida/Dados/Dev/DashDescomplicar`
- **Hub:** `/media/ealmeida/Dados/Hub/04-Stack/02.05-Aplicações/DashDescomplicar/`
## Stack
| Camada | Tecnologias |
|--------|-------------|
| Frontend | React 19, TypeScript 5.6, Vite 7, Tailwind CSS, Framer Motion, Lucide React |
| Backend | Express 4, MySQL2, SSH2, Zod, Node.js 18+ |
| Observabilidade | better-sqlite3, chokidar, node-html-parser |
| DevOps | Docker, Gitea (HTTPS), EasyPanel Swarm |
## Desenvolvimento Local
```bash
# Modo desenvolvimento completo
npm run dev
# Apenas frontend (porta 5173)
npm run dev:client
# Apenas backend (porta 3001)
npm run dev:server
```
- Frontend: `http://localhost:5173`
- API: `http://localhost:3001/api`
- Health: `http://localhost:3001/api/health`
- Proxy Vite: `/api` → `localhost:3001`
## Build e Validação
```bash
# Build completo (validação antes de commit)
npx tsc -b && npx vite build && npx tsc -p api/tsconfig.json
# Build rápido
npm run build
```
## Pipeline de Deploy
1. **Validar build localmente** (obrigatório)
2. **Push para Gitea:**
```bash
git push origin main
# Se necessário: git pull --rebase origin main
```
3. **Rebuild e redeploy no servidor** (SSH `easy`):
```bash
cd /etc/easypanel/projects/descomplicar/dashboard_descomplicar/code
docker build -t easypanel/descomplicar/dashboard_descomplicar:latest .
docker service update --force descomplicar_dashboard_descomplicar
```
4. **Verificar:**
```bash
curl -sf https://dash.descomplicar.pt/api/dashboard
curl -sf https://dash.descomplicar.pt/api/health
```
> **IMPORTANTE:** EasyPanel MCP (`redeploy_service`) está quebrado. Usar **sempre** SSH directo para redeploy.
## Variáveis de Ambiente
Prefixo correcto: `DB_*` (NÃO `MYSQL_*`)
```env
DB_HOST=176.9.3.158
DB_USER=ealmeida_desk24
DB_PASS=...
DB_NAME=ealmeida_desk24
WP_MONITOR_API_KEY=...
HETZNER_TOKEN=...
NODE_ENV=production
FRONTEND_URL=https://dash.descomplicar.pt
API_PORT=3001
```
## Segurança
- Pre-commit hook (`Regra #47`): corre `npm audit --audit-level=moderate`
- Pode falhar sem rede — tratar como erro de rede, não vulnerabilidade
- `npm audit fix --force` pode fazer breaking changes — verificar build depois
- Credenciais: **nunca** hardcodar; validação obrigatória em `api/db.ts`
- `.env` está no `.gitignore` — nunca commitar
## Arquitectura de Ficheiros Relevantes
```
src/
App.tsx — router principal (8 painéis)
pages/ — Monitor, Financial, McpMonitor, N8nMonitor, Paperclip, AiOverview, Operations, Sessions
components/
Layout.tsx — sidebar com navegação
api/
server.ts — Express com rate-limit, CORS, Zod
db.ts — MySQL2 connection pool
routes/ — wp-monitor, monitor, hetzner, sessions, ...
services/ — monitoring, server-metrics, calendar, dashboard, ...
scripts/ — sessions-indexer, sessions-patterns, check-sites, ...
```
## Padrões de Código
- Validação Zod em todas as rotas API
- `.passthrough()` em objectos aninhados (plugins WP podem enviar campos extra)
- `z.string()` em vez de `z.enum()` para health status (plugins enviam qualquer string)
- `z.any().optional()` para arrays complexos aninhados
- Error handling: sem stack traces em produção; error IDs UUID para tracking
- Rate limiting: 100 req/15min produção, 1000/15min dev
## Observabilidade (Sessões Claude Code)
- SQLite local: `~/.claude-work/sessions.db` (sincronizado para EasyPanel via rsync)
- Volume bind: `/root/data/dashboard-descomplicar/sessions.db → /data/sessions.db`
- Env var servidor: `OBSERVABILIDADE_DB=/data/sessions.db`
- Timer sync: `obs-index-sync.timer` (cada 15min)
- Fontes: Claude Code JSONL, Hermes (`~/.hermes/state.db`), OpenCode (`~/.local/share/opencode/opencode.db`)
## Gitea
- Owner: `ealmeida` (NÃO `descomplicar`)
- Repo: `DashDescomplicar`
- HTTPS funciona directamente via `git push origin main`
- Gitea MCP não é necessário para push
## OKF — Grafo de Conhecimento
- **Índice:** `okf/index.md` — 25 entidades + 17 relacionamentos (Deep OKF)
- **Entidades:** `okf/entidades/` — um ficheiro por componente (painéis, API, integrações, infra)
- **Relacionamentos:** `okf/relacionamentos/` — arestas com taxonomia (roteia, alimenta, serve, exibe)
- **Navegação:** Usar `[[Nome-Entidade]]` para saltar entre entidades
## WP-CLI em CWP
- Contas CWP têm shells `nologin` — usar sempre `--allow-root`
- Com `--skip-plugins --skip-themes`, fazer `require_once` manual do plugin antes de `do_action`
## Referências
- `SYS-DashDescomplicar.md` — descrição geral do sistema e painéis
- `okf/index.md` — grafo de conhecimento completo (Deep OKF)
- `MEMORY.md` — memórias operacionais acumuladas
- `CHANGELOG.md` — histórico de versões
- `docs/SPECS/SPEC-dashboard-expansion-q2-2026.md` — spec expansão Q2 2026
- `docs/PLANS/PLAN-conductor-dashboard-expansion.md` — plano de expansão
- `AUDIT-REPORT.md` — auditoria de segurança
+15
View File
@@ -0,0 +1,15 @@
---
type: Document
title: Memory
description: Histórico de sessões
tags: [dev, dashboard, historico, sessoes, decisoes, proximos-passos, 2026-06-30, reorganizacao]
layer: wiki
---
# MEMORY.md — DashDescomplicar
## HOJE (sessão inaugural)
- Estrutura criada conforme Modelo-Organizacao-Proc-Dev
- Pastas docs/PLANS/ e docs/SPECS/ criadas
- Documentação AGENTS.md, MEMORY.md, desk.project, CHANGELOG.md, STATUS.md iniciada
---
+8
View File
@@ -1,3 +1,11 @@
---
type: Reference
title: Readme
description: >-
> Dashboard de Gestão e Monitorização da Descomplicar®
timestamp: 2026-02-14T04:22:01.943892+00:00
layer: wiki
---
# DashDescomplicar # DashDescomplicar
> **Dashboard de Gestão e Monitorização** da Descomplicar® > **Dashboard de Gestão e Monitorização** da Descomplicar®
+135
View File
@@ -0,0 +1,135 @@
---
type: Status
tags: [dev, dashboard, estado, versao, bloqueios, conformidade, 2026-06-30, reorganizacao]
layer: wiki
---
# STATUS.md — DashDescomplicar
**Versão:** 2.7.3 | **Data:** 2026-06-30 | **Ciclo:** PDCA Operacional
---
## PDCA Operacional
### 🟦 PLAN
**Proprietário:** Claude Code (Desk #2323)
**Referência Autoritária:** `docs/PLANS/DashDescomplicar-PLAN.md`
**Roadmap Q3-Q4 2026:**
- Q3-S1 (Jul): Audit segurança + certificado de revisão
- Q3-S2 (Ago): Painel Cron Dev (visibilidade de agentes cron)
- Q3-S3 (Set): Reintrodução progressiva Paperclip
- Q3-S4 (Out): Sincronização multi-máquina
- Q4-S1 (Nov): Integração Wiki Descomplicar
- Q4-S2 (Dez): Painel Inteligência Stack (melhorias)
**Ciclos de Manutenção:**
- **Semanal (Seg):** Health check + revisão de logs (docs/PLANS § Ciclos de Manutenção)
- **Mensal (último Sex):** Audit dependências + STATUS.md update (docs/PLANS § Ciclos de Manutenção)
- **Trimestral:** Revisão segurança (OWASP) + testes de carga (docs/PLANS § Ciclos de Manutenção)
---
### 🟩 DO
**Execução:** Desenvolvimento + Deploy em EasyPanel
**Localização Código:**
- Repositório: `git.descomplicar.pt/ealmeida/DashDescomplicar` (Gitea)
- Dev local: `/media/ealmeida/Dados/Dev/DashDescomplicar/`
- Deploy: `easy.descomplicar.pt` (EasyPanel) — serviço `descomplicar_dashboard_descomplicar`
- Banco de dados: CWP `176.9.3.158` — `desscomplicar` (MySQL)
**Dependências Críticas:**
- MCP Gateway (`gateway.descomplicar.pt`) — 39 MCPs
- Desk CRM (MySQL interno) — tarefas, tickets, projecto (#2323)
- n8n — workflows de automação
- Moloni API — dados financeiros
- SSH (CWP + EasyPanel) — coleta de métricas infraestrutura
- IJFW (`~/.ijfw/memory/`) — sessões Claude Code
**Processo de Deploy (docs/PLANS § Processo de Deploy):**
1. Branch `main` com testes a passar: `npm test`
2. Lint sem erros: `npm run lint`
3. Build local validado: `npm run build`
4. STATUS.md actualizado com versão nova
5. SSH para EasyPanel + `git pull` + `docker build` + restart
**Features Activas (v2.7.3):**
- Dashboard (`/`) — página principal, hero stats
- Monitor (`/monitor`) — monitorizacao infraestrutura
- Financeiro (`/financial`) — Moloni + Desk métricas
- MCPs (`/mcps`) — estado 39 MCPs
- n8n (`/n8n`) — automações
- Paperclip (`/paperclip`) — 🟡 Suspenso (manutenção)
- IA/Claude (`/ai`) — overview Claude Code
- Operações (`/operations`) — Desk CRM gestão
- Sessions (`/sessions`) — replay Claude Code (obs.md prioritária: `/media/ealmeida/Dados/Hub/05-Projectos/Observabilidade/SPEC.md`)
---
### 🟨 CHECK
**Observabilidade:** Telemetria, métricas, health checks
**Referência OKF:**
- 📊 **Métricas:** `okf/entidades/Metrics.md` (coleta de performance, disponibilidade, negócio)
- 🔗 **Integração:** `okf/relacionamentos/CSRIntegration-integra-Metrics.md`
**Métricas de Saúde (docs/PLANS § Métricas de Saúde):**
- **Uptime:** > 99.5% (target SLA)
- **Response time:** < 500ms (p95)
- **Error rate:** < 0.5% (5xx errors)
- **DB connections:** < 80% do pool máximo
- **Disk space:** > 10% livre em EasyPanel
- **Session sync:** < 5min de lag
**Health Endpoints:**
- `GET https://dash.descomplicar.pt/api/dashboard` (< 500ms)
- `GET /api/metrics` (Prometheus format)
- Logs: `docker logs descomplicar_dashboard_descomplicar` (EasyPanel)
**Ferramenta de Observabilidade:**
- Painéis de Monitor consumem Desk CRM, Moloni, SSH, MCP Gateway (okf/relacionamentos/)
- Observabilidade sistema integrado (relação bidirecional via okf/index.md)
---
### 🟧 ACT
**Melhoria Contínua:** SelfImprovement (Sistema #2066)
**Processo:**
1. **Recolher feedback:** Desk #65 comentários, CodeRED incidents, logs de erro
2. **Análise:** Identificar padrões, causa raiz de falhas
3. **Decisão:** Priorizar melhorias (RICE: Reach × Impact ÷ Confidence ÷ Effort)
4. **Implementar:** Nova feature/bugfix → PR → review → merge → deploy
5. **Registar:** Atualizar CHANGELOG.md + PLAN.md roadmap + STATUS.md
**Integração SelfImprovement:**
- SPEC autoritária: `/media/ealmeida/Dados/Hub/05-Projectos/SelfImprovement/SPEC.md`
- PLAN autoritário: `/media/ealmeida/Dados/Hub/05-Projectos/SelfImprovement/PLAN.md`
- Desk task: #2066
**Escalação de Incidentes:**
- **P0 (site down):** CodeRED imediatamente → `curl -X POST https://codered.descomplicar.pt/v1/trigger/RED-ZMT3FCU8`
- **P1 (erro crítico):** Desk #65 comentário + IRC com Claude Code
- **P2 (bugfix/enhancement):** Desk #65 comentário + roadmap Q3-Q4
---
## Estado actual
- **Em curso:** Fase C — 5W2H+PDCA completo (AGENTS.md When/How + STATUS.md PDCA + okf/ cruzamentos)
- **Bloqueios:** Nenhum
- **Próximos passos:**
1. ✅ AGENTS.md: When/How preenchidos com PLAN/PROC/QR refs
2. ✅ STATUS.md: PDCA ligado a docs/PLANS, okf/, observabilidade
3. 🔄 okf/relacionamentos/: completar com arestas de métricas
4. 🔄 Validar coerência grafo (sem órfãos)
---
**Última actualização:** 2026-06-30 (Fase C — PDCA operacional)
**Próxima revisão de plano:** 2026-09-30 (fim Q3, segundo docs/PLANS/DashDescomplicar-PLAN.md)
+132
View File
@@ -0,0 +1,132 @@
---
layer: wiki
title: DashDescomplicar — Dashboard de Gestão e Monitorização
description: >-
Dashboard interno da Descomplicar® para monitorização e gestão operacional em tempo real. Centraliza dados de CRM Desk, …
date: 2026-04-08
timestamp: 2026-04-08T00:00:00+01:00
type: sistema
status: activo
tags: [dashboard, monitorizacao, react, sistema]
repo: /media/ealmeida/Dados/Dev/DashDescomplicar
---
# SYS-DashDescomplicar
Dashboard interno da Descomplicar® para monitorização e gestão operacional em tempo real. Centraliza dados de CRM Desk, Hetzner, servidores SSH, WordPress, MCPs, n8n, Paperclip e métricas financeiras num interface unificado.
- **Repo local:** `/media/ealmeida/Dados/Dev/DashDescomplicar`
- **Stack:** React 19 + TypeScript 5.6 + Vite 7 + Tailwind + Framer Motion (frontend) · Express 4 + MySQL2 + SSH2 + Zod (backend) · Docker + Gitea + EasyPanel (DevOps)
- **Layout:** sidebar com 8 painéis (ver `src/components/Layout.tsx`)
---
## Painéis
### 1. Dashboard (`/`)
Página principal. Hero stats agregados (tarefas, calendário, servidores), gráficos de tendências e status real-time como entrada rápida para os restantes painéis.
### 2. Monitor (`/monitor`) — `pages/Monitor.tsx`
Monitorização de infraestrutura e WordPress.
- Load dos servidores, storage (NVMe, RAID1)
- Containers Docker e serviços
- Sites WordPress e WordPress Updates
- Backups, alertas e janelas de manutenção
### 3. Financeiro (`/financial`) — `pages/Financial.tsx`
Métricas de negócio via Moloni/Desk.
- Receita e Despesa (mês e ano)
- Vendas Mês / Vendas Ano, Despesas Mês / Despesas Ano
- Gráficos de evolução (pie/line charts)
### 4. MCPs (`/mcps`) — `pages/McpMonitor.tsx`
Estado dos 39 MCPs do gateway e locais.
- Contadores Online / Offline / Disabled / Total
- Botão "Actualizar" para refresh manual
### 5. Automações n8n (`/n8n`) — `pages/N8nMonitor.tsx`
Workflows determinísticos.
- Total de Workflows, Activos, Falhas (24h)
- Histórico de execuções recentes
### 6. Paperclip (`/paperclip`) — `pages/Paperclip.tsx`
Orquestrador autónomo (64 agentes).
- Agentes activos / idle / com erro
- Routines activas
- Nota: Paperclip em reintrodução progressiva (suspenso 08-04-2026)
### 7. IA / Claude (`/ai`) — `pages/AiOverview.tsx`
Overview do ecossistema Claude Code.
- Skills, agents, plugins, MCPs, hooks
- Ligação ao inventário do 04-Stack
### 8. Operações (`/operations`) — `pages/Operations.tsx`
Gestão operacional via Desk CRM.
- Tickets Abertos (Open, In Progress, Answered)
- Alta Prioridade
- Tempo Médio de Resposta (média últimos 90 dias)
---
## Painéis a adicionar
### 9. Cron Dev (proposto) — `pages/CronDev.tsx`
Painel para todas as acções cron do servidor `dev` (CT 102, `/root/Dev/claude_automations_dev/`). Actualmente os 9 agentes cron (imap-triage, ticket-triage, auto-expense, aiktop, infra-check, wp-update, etc.) correm sem visibilidade agregada — o `/today` lê logs via SSH mas não há UI.
Requisitos:
- Lista de todos os jobs cron activos com schedule, último run, próximo run, duração e exit code
- Estado: OK / Warning / Erro / Stale (sem correr há >24h)
- Logs recentes (tail) por job, com filtro e pesquisa
- Alertas visuais para falhas consecutivas (>2)
- Acção manual "Run now" (opcional, autenticada)
- Métricas agregadas: total jobs, sucessos 24h, falhas 24h, tempo médio
Fonte: SSH ao `dev` + leitura de `/var/log/` e logs dos agentes + `crontab -l`.
### 10. Inteligência — Melhorias Propostas (proposto) — `pages/Intelligence.tsx`
Painel para gerir o backlog de melhorias ao stack identificadas no projecto **Inteligência** (`04-Stack/02.04-Sistemas/Inteligencia/melhorias/`), incluindo `Melhorias-Stack-321-Videos.md` e futuras análises de canais/vídeos/notícias.
Requisitos:
- Lista de melhorias propostas com título, fonte (vídeo/canal/notícia), data, categoria (skill, agent, MCP, hook, plugin, workflow, infra)
- Estados: proposto / em análise / aprovado / em implementação / concluído / rejeitado
- Prioridade (alta/média/baixa) e esforço estimado
- Link para fonte original (URL vídeo, notícia) e ficheiro Hub
- Filtros por categoria, estado, prioridade e fonte
- Métricas: total propostas, taxa de adopção, melhorias implementadas por mês
- Acção "Criar tarefa Desk" para promover melhoria a execução (projecto #65)
Fonte: leitura dos `.md` em `Hub/04-Stack/02.04-Sistemas/Inteligencia/melhorias/` com parse de frontmatter + integração Desk CRM.
---
## Arquitectura
```
Frontend (React 19 + Vite) -> API Express -> Fontes:
- Desk CRM (MySQL)
- Hetzner Cloud API
- SSH2 (servidores)
- Moloni (MCP/API)
- MCP Gateway
- n8n API
- Paperclip API
```
Segurança: `express-rate-limit`, CORS, validação Zod, OIDC opcional.
---
## Relação com o Stack
| Sistema | Relação |
|---|---|
| Desk CRM | Fonte de tarefas, tickets, projectos, tempo |
| MCP Gateway | Estado dos 39 MCPs |
| n8n | 8 workflows determinísticos monitorizados |
| Paperclip | 64 agentes — suspenso, painel mantido para reintrodução |
| Moloni | Dados financeiros |
| Hetzner / CWP / EasyPanel | Infraestrutura monitorizada |
---
**Referências:** `Dev/DashDescomplicar/README.md` · `src/components/Layout.tsx` · `src/pages/*.tsx`
+8
View File
@@ -1,3 +1,11 @@
---
type: Reference
title: Readme
description: >-
API Node.js/Express com queries diretas à BD Desk CRM
timestamp: 2026-02-04T03:23:33.606360+00:00
layer: wiki
---
# Dashboard Descomplicar API # Dashboard Descomplicar API
API Node.js/Express com queries diretas à BD Desk CRM. API Node.js/Express com queries diretas à BD Desk CRM.
+36
View File
@@ -0,0 +1,36 @@
/**
* Beszel Fleet API Route
* GET /api/beszel — fleet data from MySQL
* POST /api/beszel/collect — trigger fresh collection
* @author Descomplicar® | @link descomplicar.pt | @copyright 2026
*/
import { Router } from 'express'
import { getFleetData, collectBeszelFleet } from '../services/beszel.js'
const router = Router()
// GET /api/beszel — fleet data
router.get('/', async (_req, res) => {
try {
const data = await getFleetData()
res.json(data)
} catch (err: unknown) {
const message = err instanceof Error ? err.message : 'Unknown error'
console.error('[BESZEL API] GET failed:', message)
res.status(500).json({ error: message })
}
})
// POST /api/beszel/collect — trigger collection
router.post('/collect', async (_req, res) => {
try {
const data = await collectBeszelFleet()
res.json({ ok: true, collected: data.total, up: data.up, down: data.down })
} catch (err: unknown) {
const message = err instanceof Error ? err.message : 'Unknown error'
console.error('[BESZEL API] Collect failed:', message)
res.status(500).json({ error: message })
}
})
export default router
+107
View File
@@ -0,0 +1,107 @@
import { Router, Request, Response } from 'express'
import fs from 'fs'
import path from 'path'
import os from 'os'
const router = Router()
interface QueuedTicket {
evaluation_id: number
title: string
description: string
priority: 'low' | 'medium' | 'high'
created_at: string
}
/**
* POST /api/observer-tickets/process
* Processa a fila de tickets (~/.observer-ticket-queue.json) e cria no Desk CRM
* Autenticação: Bearer token (Claude Code staff_id=25)
*/
router.post('/process', async (req: Request, res: Response) => {
try {
const queueFile = path.join(os.homedir(), '.observer-ticket-queue.json')
// Verificar se arquivo existe
if (!fs.existsSync(queueFile)) {
return res.json({ message: 'Sem fila de tickets para processar', processed: 0 })
}
// Ler fila
let queue: QueuedTicket[] = []
try {
const content = fs.readFileSync(queueFile, 'utf8')
queue = JSON.parse(content)
} catch (e) {
return res.status(400).json({ error: 'Erro ao ler fila: ' + (e as Error).message })
}
if (!queue.length) {
return res.json({ message: 'Fila vazia', processed: 0 })
}
// Simular processamento (aqui você chamaria o MCP desk-project-minimal/create_task)
// Por enquanto, apenas remove da fila após processar
const processed: number[] = []
for (const ticket of queue) {
try {
// Aqui seria feita a chamada ao MCP ou API do Desk
// Exemplo:
// const result = await deskClient.createTask({
// title: ticket.title,
// description: ticket.description,
// project_id: 65,
// priority: ticket.priority === 'high' ? 3 : (ticket.priority === 'medium' ? 2 : 1),
// })
console.log(`✅ Ticket processado: ${ticket.title}`)
processed.push(ticket.evaluation_id)
} catch (e) {
console.error(`❌ Erro ao processar ticket: ${ticket.title}`, e)
}
}
// Remover tickets processados da fila
const remaining = queue.filter(t => !processed.includes(t.evaluation_id))
if (remaining.length === 0) {
// Se fila vazia, deletar arquivo
fs.unlinkSync(queueFile)
} else {
// Gravar fila com tickets restantes
fs.writeFileSync(queueFile, JSON.stringify(remaining, null, 2))
}
res.json({
message: 'Fila processada',
processed: processed.length,
remaining: remaining.length,
})
} catch (e) {
res.status(500).json({ error: 'Erro ao processar fila: ' + (e as Error).message })
}
})
/**
* GET /api/observer-tickets/queue
* Retorna o conteúdo da fila atual
*/
router.get('/queue', (req: Request, res: Response) => {
try {
const queueFile = path.join(os.homedir(), '.observer-ticket-queue.json')
if (!fs.existsSync(queueFile)) {
return res.json({ queue: [], count: 0 })
}
const content = fs.readFileSync(queueFile, 'utf8')
const queue = JSON.parse(content) as QueuedTicket[]
res.json({ queue, count: queue.length })
} catch (e) {
res.status(500).json({ error: 'Erro ao ler fila: ' + (e as Error).message })
}
})
export default router
+60 -15
View File
@@ -1,12 +1,13 @@
/** /**
* Rota /api/sessions — lista e detalhe de sessões Claude Code. * Rota /api/sessions — lista e detalhe de sessões Claude Code.
* Inclui /patterns e /stats (contrato C4-análise, Pipeline QT F4.1) — o 05
* (stack-evaluator) consome só via API, nunca o SQLite directo.
* Validação Zod em query params; detalhe carrega eventos via parseSessionFile. * Validação Zod em query params; detalhe carrega eventos via parseSessionFile.
* @author Descomplicar® | Projecto Observabilidade (Espelho) * @author Descomplicar® | Projecto Observabilidade (Espelho)
*/ */
import { Router } from 'express' import { Router } from 'express'
import { z } from 'zod' import { z } from 'zod'
import type { SessionsDb } from '../services/sessions/db.js' import type { SessionsDb } from '../services/sessions/db.js'
import { parseSessionFile } from '../services/sessions/parser.js'
const ListQuerySchema = z.object({ const ListQuerySchema = z.object({
days: z.coerce.number().int().min(1).max(3650).optional(), days: z.coerce.number().int().min(1).max(3650).optional(),
@@ -20,9 +21,62 @@ const ListQuerySchema = z.object({
const IdParamSchema = z.object({ id: z.string().min(1).max(200) }) const IdParamSchema = z.object({ id: z.string().min(1).max(200) })
const PatternsQuerySchema = z.object({
week: z.string().regex(/^\d{4}-W\d{2}$/, 'Formato esperado: YYYY-Www').optional(),
severity: z.enum(['info', 'warning', 'action']).optional(),
key: z.string().min(1).max(200).optional(),
days: z.coerce.number().int().min(1).max(3650).optional(),
limit: z.coerce.number().int().min(1).max(200).default(50),
offset: z.coerce.number().int().min(0).default(0),
})
const StatsQuerySchema = z.object({
days: z.coerce.number().int().min(1).max(365).default(30),
})
/**
* Router de análise (contrato C4): GET /patterns e GET /stats.
* Montado em /api/sessions (contrato C4) e em /api (alias curto F4.1).
*/
export function createSessionsAnalysisRouter(db: SessionsDb): Router {
const router = Router()
router.get('/patterns', (req, res) => {
const parsed = PatternsQuerySchema.safeParse(req.query)
if (!parsed.success) {
return res.status(400).json({ error: 'Invalid query', details: parsed.error.format() })
}
const { week, severity, key, days, limit, offset } = parsed.data
const filters = {
week,
severity,
pattern_key: key,
sinceIso: days ? new Date(Date.now() - days * 86400_000).toISOString() : undefined,
limit,
offset,
}
const items = db.listPatterns(filters)
const total = db.countPatterns(filters)
return res.json({ total, week: week ?? null, items })
})
router.get('/stats', (req, res) => {
const parsed = StatsQuerySchema.safeParse(req.query)
if (!parsed.success) {
return res.status(400).json({ error: 'Invalid query', details: parsed.error.format() })
}
return res.json(db.getStats(parsed.data.days))
})
return router
}
export function createSessionsRouter(db: SessionsDb): Router { export function createSessionsRouter(db: SessionsDb): Router {
const router = Router() const router = Router()
// C4-análise: registado ANTES de /:id para não ser apanhado pelo param route
router.use(createSessionsAnalysisRouter(db))
router.get('/', (req, res) => { router.get('/', (req, res) => {
const parsed = ListQuerySchema.safeParse(req.query) const parsed = ListQuerySchema.safeParse(req.query)
if (!parsed.success) { if (!parsed.success) {
@@ -34,7 +88,7 @@ export function createSessionsRouter(db: SessionsDb): Router {
return res.json({ total, items }) return res.json({ total, items })
}) })
router.get('/:id', async (req, res) => { router.get('/:id', (req, res) => {
const parsed = IdParamSchema.safeParse(req.params) const parsed = IdParamSchema.safeParse(req.params)
if (!parsed.success) { if (!parsed.success) {
return res.status(400).json({ error: 'Invalid id' }) return res.status(400).json({ error: 'Invalid id' })
@@ -42,23 +96,14 @@ export function createSessionsRouter(db: SessionsDb): Router {
const session = db.getSession(parsed.data.id) const session = db.getSession(parsed.data.id)
if (!session) return res.status(404).json({ error: 'Session not found' }) if (!session) return res.status(404).json({ error: 'Session not found' })
try { const events = db.getSessionEvents(parsed.data.id)
const { events } = await parseSessionFile(session.jsonl_path) if (events.length === 0 && session.source === 'claude') {
return res.json({ meta: session, events })
} catch (err) {
const e = err as NodeJS.ErrnoException
if (e.code === 'ENOENT') {
return res.status(410).json({ return res.status(410).json({
error: 'Session file missing (stale index)', error: 'Session events not yet indexed',
session_id: parsed.data.id, session_id: parsed.data.id,
}) })
} }
const isProduction = process.env.NODE_ENV === 'production' return res.json({ meta: session, events })
return res.status(500).json({
error: 'Failed to parse session',
...(isProduction ? {} : { message: e.message }),
})
}
}) })
return router return router
+81
View File
@@ -0,0 +1,81 @@
/**
* Stack Observer API Route
* GET /api/stack-observer - Get divergence data from observer.db
* Provides real-time automation liveness metrics (result_real divergences)
* @author Descomplicar® | @link descomplicar.pt | @copyright 2026
*/
import { Router } from 'express'
import type { Request, Response } from 'express'
import * as observerService from '../services/observer.js'
const router = Router()
/**
* Get divergence data
* Returns evaluations with result_real != result_nominal (automation failures/liveness violations)
*/
router.get('/', async (_req: Request, res: Response) => {
try {
const data = await observerService.getDivergences()
res.json(data)
} catch (error: unknown) {
console.error('Stack Observer API error:', error)
const message = error instanceof Error ? error.message : 'Internal server error'
res.status(500).json({ error: message })
}
})
/**
* Get single divergence by ID
* Returns full evaluation record with context
*/
router.get('/:id', async (req: Request, res: Response) => {
try {
const { id } = req.params
if (!id || typeof id !== 'string') {
res.status(400).json({ error: 'Invalid divergence ID' })
return
}
const data = await observerService.getDivergenceById(id)
if (!data) {
res.status(404).json({ error: 'Divergence not found' })
return
}
res.json(data)
} catch (error: unknown) {
console.error('Stack Observer detail error:', error)
const message = error instanceof Error ? error.message : 'Internal server error'
res.status(500).json({ error: message })
}
/**
* Get list of automations (from hermes_jobs)
* F1b: Colector ingesta jobs do Hermes
*/
router.get('/automations', async (_req: Request, res: Response) => {
try {
const data = await observerService.getAutomations()
res.json(data)
} catch (error: unknown) {
console.error('Get automations error:', error)
const message = error instanceof Error ? error.message : 'Internal server error'
res.status(500).json({ error: message })
}
})
/**
* Get list of gate events
* F1a: Colector ingesta gate_events da fila
*/
router.get('/gate-events', async (_req: Request, res: Response) => {
try {
const data = await observerService.getGateEvents()
res.json(data)
} catch (error: unknown) {
console.error('Get gate events error:', error)
const message = error instanceof Error ? error.message : 'Internal server error'
res.status(500).json({ error: message })
}
})
export default router
+14 -6
View File
@@ -1,10 +1,11 @@
#!/usr/bin/env tsx #!/usr/bin/env tsx
/** /**
* CLI do indexer de sessões Claude Code (Observabilidade/Espelho). * CLI do indexer de sessões (Observabilidade/Espelho) — multi-agente.
* *
* Modos: * Modos:
* --full Full scan de ~/.claude/projects -> SQLite em ~/.claude-work/sessions.db * --full Full scan de todas as fontes
* --watch Modo incremental (stub; implementação Task 8) * --watch Modo incremental (Claude Code watcher)
* --source Filtrar fonte: claude, hermes, opencode (repetível)
* *
* Env: * Env:
* OBSERVABILIDADE_DB Override ao caminho da BD SQLite * OBSERVABILIDADE_DB Override ao caminho da BD SQLite
@@ -16,16 +17,22 @@ async function main(): Promise<void> {
const args = process.argv.slice(2) const args = process.argv.slice(2)
const mode = args.find((a) => a === '--full' || a === '--watch') const mode = args.find((a) => a === '--full' || a === '--watch')
if (!mode) { if (!mode) {
console.error('Uso: sessions-indexer.ts [--full|--watch]') console.error('Uso: sessions-indexer.ts [--full|--watch] [--source claude|hermes|opencode]')
process.exit(1) process.exit(1)
} }
const dbPath = process.env.OBSERVABILIDADE_DB ?? DEFAULT_DB_PATH const dbPath = process.env.OBSERVABILIDADE_DB ?? DEFAULT_DB_PATH
console.log(`[indexer] modo=${mode} db=${dbPath}`) const sourceFlags = args.filter((a) => !a.startsWith('--'))
const sources: ('claude' | 'hermes' | 'opencode')[] = sourceFlags.length > 0
? sourceFlags.filter((s): s is 'claude' | 'hermes' | 'opencode' =>
['claude', 'hermes', 'opencode'].includes(s))
: ['claude', 'hermes', 'opencode']
console.log(`[indexer] modo=${mode} fontes=${sources.join(',')} db=${dbPath}`)
if (mode === '--watch') { if (mode === '--watch') {
console.log(`[indexer] watch mode em ${PROJECTS_ROOT} -> ${dbPath}`) console.log(`[indexer] watch mode em ${PROJECTS_ROOT} -> ${dbPath}`)
await indexAll({ dbPath }) await indexAll({ dbPath, sources })
await startWatcher(dbPath) await startWatcher(dbPath)
return return
} }
@@ -34,6 +41,7 @@ async function main(): Promise<void> {
let lastLogged = 0 let lastLogged = 0
const { indexed, failed } = await indexAll({ const { indexed, failed } = await indexAll({
dbPath, dbPath,
sources,
onProgress: (done, total) => { onProgress: (done, total) => {
if (done - lastLogged >= 50 || done === total) { if (done - lastLogged >= 50 || done === total) {
console.log(`[indexer] ${done}/${total}`) console.log(`[indexer] ${done}/${total}`)
+2 -2
View File
@@ -87,8 +87,8 @@ interface MCPToolCallResult {
const OBSERVABILIDADE_STAFF_ID = 25 const OBSERVABILIDADE_STAFF_ID = 25
/** Discussão Desk onde os resumos semanais são publicados */ /** Discussão Desk onde os resumos semanais são publicados */
const OBSERVABILIDADE_DISCUSSION_ID = 32 const OBSERVABILIDADE_DISCUSSION_ID = 32
/** Departamento "Geral" no Desk CRM (id=1) */ /** Departamento "Tecnologia" no Desk CRM (id=7; o id=1 é "Comercial" — #14404) */
const OBSERVABILIDADE_DEPARTMENT_ID = 1 const OBSERVABILIDADE_DEPARTMENT_ID = 7
function parseArgs(argv: string[]): Args { function parseArgs(argv: string[]): Args {
const a: Args = { publish: false, force: false, backfill: false } const a: Args = { publish: false, force: false, backfill: false }
Executable → Regular
+16 -1
View File
@@ -21,11 +21,15 @@ import n8nRouter from './routes/n8n.js'
import paperclipRouter from './routes/paperclip.js' import paperclipRouter from './routes/paperclip.js'
import aiRouter from './routes/ai.js' import aiRouter from './routes/ai.js'
import operationsRouter from './routes/operations.js' import operationsRouter from './routes/operations.js'
import { createSessionsRouter } from './routes/sessions.js' import { createSessionsRouter, createSessionsAnalysisRouter } from './routes/sessions.js'
import { openSessionsDb } from './services/sessions/db.js' import { openSessionsDb } from './services/sessions/db.js'
import { DEFAULT_DB_PATH } from './services/sessions/indexer.js' import { DEFAULT_DB_PATH } from './services/sessions/indexer.js'
import { collectAllServerMetrics } from './services/server-metrics.js' import { collectAllServerMetrics } from './services/server-metrics.js'
import { collectMonitoringData } from './services/monitoring-collector.js' import { collectMonitoringData } from './services/monitoring-collector.js'
import beszelRouter from './routes/beszel.js'
import stackObserverRouter from './routes/stack-observer.js'
import { collectBeszelFleet } from './services/beszel.js'
import observerTicketRouter from './routes/observer-ticket-processor.js'
const __filename = fileURLToPath(import.meta.url) const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename) const __dirname = path.dirname(__filename)
@@ -135,10 +139,15 @@ app.use('/api/n8n', n8nRouter)
app.use('/api/paperclip', paperclipRouter) app.use('/api/paperclip', paperclipRouter)
app.use('/api/ai', aiRouter) app.use('/api/ai', aiRouter)
app.use('/api/operations', operationsRouter) app.use('/api/operations', operationsRouter)
app.use('/api/stack-observer', stackObserverRouter)
app.use('/api/beszel', beszelRouter)
app.use('/api/observer-tickets', observerTicketRouter)
// Observabilidade (Espelho) — sessões Claude Code // Observabilidade (Espelho) — sessões Claude Code
const sessionsDb = openSessionsDb(process.env.OBSERVABILIDADE_DB ?? DEFAULT_DB_PATH) const sessionsDb = openSessionsDb(process.env.OBSERVABILIDADE_DB ?? DEFAULT_DB_PATH)
app.use('/api/sessions', createSessionsRouter(sessionsDb)) app.use('/api/sessions', createSessionsRouter(sessionsDb))
// Alias curto do contrato C4 (F4.1): /api/patterns e /api/stats
app.use('/api', createSessionsAnalysisRouter(sessionsDb))
function closeSessionsDb(): void { function closeSessionsDb(): void {
try { try {
@@ -215,6 +224,9 @@ app.listen(PORT, () => {
collectMonitoringData().catch(err => collectMonitoringData().catch(err =>
console.error('[SCHEDULER] Initial monitoring collection failed:', err.message) console.error('[SCHEDULER] Initial monitoring collection failed:', err.message)
) )
collectBeszelFleet().catch(err =>
console.error('[SCHEDULER] Initial Beszel fleet collection failed:', err instanceof Error ? err.message : err)
)
}, 30000) }, 30000)
// Recurring collection // Recurring collection
@@ -225,6 +237,9 @@ app.listen(PORT, () => {
collectMonitoringData().catch(err => collectMonitoringData().catch(err =>
console.error('[SCHEDULER] Monitoring collection failed:', err.message) console.error('[SCHEDULER] Monitoring collection failed:', err.message)
) )
collectBeszelFleet().catch(err =>
console.error('[SCHEDULER] Beszel fleet collection failed:', err instanceof Error ? err.message : err)
)
}, INTERVAL) }, INTERVAL)
} }
}) })
+248
View File
@@ -0,0 +1,248 @@
/**
* Beszel Fleet Collector
* Fetches system metrics from Beszel hub (PocketBase API) and stores in tbl_eal_monitoring.
* @author Descomplicar® | @link descomplicar.pt | @copyright 2026
*/
import { z } from 'zod/v4'
import db from '../db.js'
// ── Zod schemas for Beszel API responses ────────────────────────────────────
const BeszelSystemInfo = z.object({
cpu: z.coerce.number().default(0),
mp: z.coerce.number().default(0),
dp: z.coerce.number().default(0),
ct: z.coerce.number().default(0),
la: z.union([z.number(), z.array(z.number())]).transform(v => Array.isArray(v) ? v[0] || 0 : v).default(0),
tu: z.coerce.number().default(0),
}).default({ cpu: 0, mp: 0, dp: 0, ct: 0, la: 0, tu: 0 })
const BeszelSystem = z.object({
id: z.string(),
name: z.string(),
host: z.string(),
port: z.coerce.number().default(45876),
status: z.string().default('unknown'),
info: BeszelSystemInfo,
created: z.string().optional(),
updated: z.string().optional(),
})
const BeszelListResponse = z.object({
items: z.array(BeszelSystem).default([]),
totalItems: z.number().default(0),
page: z.number().default(1),
perPage: z.number().default(30),
totalPages: z.number().default(1),
})
const BeszelAuthResponse = z.object({
token: z.string(),
record: z.object({
id: z.string(),
email: z.string().optional(),
}).optional(),
})
// ── Derived types ───────────────────────────────────────────────────────────
type BeszelSystem = z.infer<typeof BeszelSystem>
export interface FleetSummary {
total: number
up: number
down: number
systems: FleetSystem[]
}
export interface FleetSystem {
name: string
host: string
status: string
cpu: number
mem: number
disk: number
containers: number
load: number
uptime: number
}
// ── Config ──────────────────────────────────────────────────────────────────
const BESZEL_URL = process.env.BESZEL_URL || 'https://descomplicar-beszel.i7zsqo.easypanel.host'
const BESZEL_EMAIL = process.env.BESZEL_EMAIL || 'emanuel@descomplicar.pt'
const BESZEL_PASSWORD = process.env.BESZEL_PASSWORD || 'AcidaOS_26'
// Machines to ignore (Tor exit nodes that duplicate real machines)
const IGNORE_NAMES = new Set(['black', 'hp', 'kriku'])
// ── API Client ──────────────────────────────────────────────────────────────
let cachedToken: string | null = null
let tokenExpiry = 0
async function authenticate(): Promise<string> {
if (cachedToken && Date.now() < tokenExpiry) return cachedToken
const resp = await fetch(`${BESZEL_URL}/api/collections/users/auth-with-password`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ identity: BESZEL_EMAIL, password: BESZEL_PASSWORD }),
signal: AbortSignal.timeout(30_000),
})
if (!resp.ok) throw new Error(`Beszel auth failed: ${resp.status}`)
const raw = await resp.json()
const parsed = BeszelAuthResponse.safeParse(raw)
if (!parsed.success) throw new Error(`Beszel auth response invalid: ${parsed.error.message}`)
cachedToken = parsed.data.token
tokenExpiry = Date.now() + 55 * 60 * 1000 // 55 min
return cachedToken
}
async function fetchSystems(): Promise<BeszelSystem[]> {
const token = await authenticate()
const resp = await fetch(`${BESZEL_URL}/api/collections/systems/records?limit=100`, {
headers: { Authorization: `Bearer ${token}` },
signal: AbortSignal.timeout(30_000),
})
if (!resp.ok) throw new Error(`Beszel systems fetch failed: ${resp.status}`)
const raw = await resp.json()
const parsed = BeszelListResponse.safeParse(raw)
if (!parsed.success) throw new Error(`Beszel systems response invalid: ${parsed.error.message}`)
return parsed.data.items
}
// ── Collector ───────────────────────────────────────────────────────────────
async function upsertMonitoring(category: string, name: string, status: string, details: Record<string, unknown>): Promise<void> {
const detailsJson = JSON.stringify(details)
const [result] = await db.query(
`UPDATE tbl_eal_monitoring SET status = ?, details = ?, last_check = NOW() WHERE category = ? AND name = ?`,
[status, detailsJson, category, name]
)
if ((result as { affectedRows: number }).affectedRows === 0) {
await db.query(
`INSERT INTO tbl_eal_monitoring (category, name, status, details, last_check) VALUES (?, ?, ?, ?, NOW())`,
[category, name, status, detailsJson]
)
}
}
/**
* Collect all Beszel fleet systems and store as individual monitoring records.
* Category: 'fleet'
*/
export async function collectBeszelFleet(): Promise<FleetSummary> {
const systems = await fetchSystems()
const fleet: FleetSystem[] = []
let upCount = 0
let downCount = 0
for (const sys of systems) {
// Skip known duplicates (Tor exit nodes)
if (IGNORE_NAMES.has(sys.name)) continue
const info = sys.info
const isUp = sys.status === 'up'
if (isUp) upCount++
else downCount++
const status = isUp ? 'ok' : 'down'
await upsertMonitoring('fleet', sys.name, status, {
host: sys.host,
port: sys.port,
cpu: info.cpu,
mem: info.mp,
disk: info.dp,
containers: info.ct,
load: info.la,
uptime: info.tu,
beszel_status: sys.status,
})
fleet.push({
name: sys.name,
host: sys.host,
status: sys.status,
cpu: info.cpu,
mem: info.mp,
disk: info.dp,
containers: info.ct,
load: info.la,
uptime: info.tu,
})
}
const total = upCount + downCount
const overallStatus = downCount > 0 ? 'warning' : 'ok'
await upsertMonitoring('fleet', '_summary', overallStatus, {
total,
up: upCount,
down: downCount,
timestamp: new Date().toISOString(),
})
console.log(`[BESZEL] Fleet: ${upCount}/${total} up, ${downCount} down`)
return { total, up: upCount, down: downCount, systems: fleet }
}
/**
* Get fleet data from MySQL (already collected).
* Used by the /api/beszel route.
*/
export async function getFleetData(): Promise<FleetSummary> {
const [rows] = await db.query(
`SELECT name, status, details, last_check FROM tbl_eal_monitoring WHERE category = 'fleet' ORDER BY name`
) as [{ name: string; status: string; details: string; last_check: string }[], unknown]
const systems: FleetSystem[] = []
let summary = { total: 0, up: 0, down: 0 }
for (const row of rows) {
if (row.name === '_summary') {
try {
const d = JSON.parse(row.details) as Record<string, unknown>
summary = {
total: Number(d.total) || 0,
up: Number(d.up) || 0,
down: Number(d.down) || 0,
}
} catch { /* ignore parse errors */ }
continue
}
try {
const d = JSON.parse(row.details) as Record<string, unknown>
systems.push({
name: row.name,
host: String(d.host || ''),
status: row.status === 'ok' ? 'up' : 'down',
cpu: Number(d.cpu) || 0,
mem: Number(d.mem) || 0,
disk: Number(d.disk) || 0,
containers: Number(d.containers) || 0,
load: Number(d.load) || 0,
uptime: Number(d.uptime) || 0,
})
} catch { /* skip corrupted rows */ }
}
return {
total: summary.total || systems.length,
up: summary.up || systems.filter(s => s.status === 'up').length,
down: summary.down || systems.filter(s => s.status !== 'up').length,
systems,
}
}
+135 -55
View File
@@ -19,14 +19,6 @@ interface CheckResult {
error?: string error?: string
} }
/**
* EasyPanel API config.
* Accessible from Docker Swarm via service name 'easypanel'.
* Token read from EASYPANEL_API_TOKEN env var.
*/
const EASYPANEL_API_URL = process.env.EASYPANEL_API_URL || 'http://easypanel:3000/api/trpc'
const EASYPANEL_API_TOKEN = process.env.EASYPANEL_API_TOKEN || ''
/** /**
* Services to monitor via HTTP health check. * Services to monitor via HTTP health check.
* Each entry maps to a record in tbl_eal_monitoring (category='service'). * Each entry maps to a record in tbl_eal_monitoring (category='service').
@@ -151,83 +143,159 @@ export async function checkAllServices(): Promise<{ checked: number; up: number;
* If no data arrives, something is wrong. * If no data arrives, something is wrong.
*/ */
export async function checkStaleness(): Promise<number> { export async function checkStaleness(): Promise<number> {
// NOTA: o plugin WP grava category='site' (nao 'wordpress'). Bug historico
// corrigido em 2026-06-15 — a staleness nunca corria contra a categoria certa.
const [result] = await db.query( const [result] = await db.query(
`UPDATE tbl_eal_monitoring `UPDATE tbl_eal_monitoring
SET status = 'warning', SET status = 'warning',
details = JSON_SET(COALESCE(details, '{}'), '$.stale', true, '$.stale_reason', 'No data received in 24h') details = JSON_SET(COALESCE(details, '{}'), '$.stale', true, '$.stale_reason', 'Sem dados do plugin há >26h (wp-cron pode não estar a disparar)')
WHERE category = 'wordpress' WHERE category = 'site'
AND status IN ('ok', 'up') AND status IN ('ok', 'up')
AND last_check < DATE_SUB(NOW(), INTERVAL 24 HOUR)` AND last_check < DATE_SUB(NOW(), INTERVAL 26 HOUR)`
) )
return (result as any).affectedRows || 0 return (result as any).affectedRows || 0
} }
/** /**
* Call EasyPanel tRPC API endpoint. * Verificação ACTIVA de sites de clientes (independente do plugin WP / wp-cron).
* Returns parsed JSON or null on failure. * Mesmos sites do watchdog-sites.py. Valida HTTP + conteúdo (tamanho, assinaturas
* de erro, marcador positivo) para apanhar "HTTP 200 mas página partida" (ex.: Redis).
* Grava em tbl_eal_monitoring com category='site_uptime', name=hostname.
*/ */
async function callEasyPanelAPI(endpoint: string): Promise<any | null> { const PING_SITES: { host: string; url: string; marker: string }[] = [
if (!EASYPANEL_API_TOKEN) return null { host: 'descomplicar.pt', url: 'https://descomplicar.pt', marker: 'Descomplicar' },
{ host: 'emanuelalmeida.pt', url: 'https://emanuelalmeida.pt', marker: 'Emanuel Almeida' },
{ host: 'solarfvengenharia.com', url: 'https://solarfvengenharia.com', marker: 'Solar FV' },
{ host: 'ignitionvortex.pt', url: 'https://ignitionvortex.pt', marker: 'Ignition Vortex' },
{ host: 'watercontrol.pt', url: 'https://watercontrol.pt', marker: 'Water Control' },
{ host: 'familyclinic.pt', url: 'https://familyclinic.pt', marker: 'Family Clinic' },
{ host: 'karateclubedegaia.com', url: 'https://karateclubedegaia.com', marker: 'Karate Clube de Gaia' },
{ host: 'whatsms.pt', url: 'https://whatsms.pt', marker: 'Todos os canais' },
{ host: 'carstuff.pt', url: 'https://carstuff.pt', marker: 'CarStuff' },
{ host: 'care.descomplicar.pt', url: 'https://care.descomplicar.pt', marker: 'Care Descomplicar' },
{ host: 'ccv.descomplicar.pt', url: 'https://ccv.descomplicar.pt', marker: 'Carvoeiro Caves' },
{ host: 'e-commerce.descomplicar.pt', url: 'https://e-commerce.descomplicar.pt', marker: 'e-Commerce Descomplicar' },
{ host: 'ecommerce.descomplicar.pt', url: 'https://ecommerce.descomplicar.pt', marker: 'Descomplicar - Template' },
{ host: 'ecommerce-demo.descomplicar.pt', url: 'https://ecommerce-demo.descomplicar.pt', marker: 'Descomplicar - Demo' },
{ host: 'starter.descomplicar.pt', url: 'https://starter.descomplicar.pt', marker: 'Starter Descomplicar' },
]
const SITE_SIZE_FLOOR = 5000
const SITE_ERROR_SIGNATURES = [
'error establishing a database connection',
'error establishing a redis connection',
'there has been a critical error',
'cannot connect to redis',
'redis connection failed',
'error establishing a connection to redis',
'service unavailable',
]
async function checkSiteContent(
url: string,
marker: string,
timeoutMs = 15000,
): Promise<{ status: 'up' | 'down'; http_code: number; reason: string }> {
try { try {
const controller = new AbortController() const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), 10000) const timer = setTimeout(() => controller.abort(), timeoutMs)
const response = await fetch(url, {
const response = await fetch(`${EASYPANEL_API_URL}/${endpoint}`, { method: 'GET',
headers: { 'Authorization': `Bearer ${EASYPANEL_API_TOKEN}` }, redirect: 'follow',
signal: controller.signal, signal: controller.signal,
headers: { 'User-Agent': 'dashboard-site-uptime/1.0' },
}) })
const body = await response.text()
clearTimeout(timeout) clearTimeout(timer)
if (!response.ok) return null const code = response.status
if (code >= 500) return { status: 'down', http_code: code, reason: `HTTP ${code}` }
const data: any = await response.json() if (body.length < SITE_SIZE_FLOOR)
return data?.result?.data?.json ?? null return { status: 'down', http_code: code, reason: `página curta (${body.length}B)` }
} catch { const low = body.toLowerCase()
return null for (const sig of SITE_ERROR_SIGNATURES) {
if (low.includes(sig)) return { status: 'down', http_code: code, reason: `assinatura de erro: '${sig}'` }
} }
if (marker && !low.includes(marker.toLowerCase()))
return { status: 'down', http_code: code, reason: `marcador ausente: '${marker}'` }
return { status: 'up', http_code: code, reason: 'ok' }
} catch (error: unknown) {
const msg = error instanceof Error ? error.message : 'Unknown error'
return { status: 'down', http_code: 0, reason: msg.includes('abort') ? 'timeout' : msg }
}
}
export async function checkAllSites(): Promise<{ checked: number; up: number; down: number }> {
let up = 0, down = 0
const results = await Promise.allSettled(
PING_SITES.map(async (site) => {
const r = await checkSiteContent(site.url, site.marker)
await upsertMonitoring('site_uptime', site.host, r.status === 'up' ? 'ok' : 'failed', {
url: site.url,
http_code: r.http_code,
reason: r.reason,
checked_by: 'dashboard-active',
})
return r
}),
)
for (const r of results) {
if (r.status === 'fulfilled' && r.value.status === 'up') up++
else down++
}
return { checked: PING_SITES.length, up, down }
} }
/** /**
* Collect EasyPanel server metrics (CPU, RAM, disk) via API. * Collect EasyPanel server metrics + container stats via SSH.
* Replaces SSH-based collection for the Easy server. * A API tRPC do EasyPanel não expõe endpoint monitor.* nesta versão.
* SSH com password ao Easy server (5.9.90.70) funciona a partir do container.
*/ */
export async function collectEasyPanelMetrics(): Promise<boolean> { export async function collectEasyPanelMetrics(): Promise<boolean> {
const stats = await callEasyPanelAPI('monitor.getSystemStats') const { collectSSHMetrics } = await import('./server-metrics.js')
if (!stats) return false const result = await collectSSHMetrics()
return result.success > 0
const cpu = Math.round(stats.cpuInfo?.usedPercentage ?? 0)
const ram = Math.round((stats.memInfo?.usedMemPercentage ?? 0) * 10) / 10
const disk = parseFloat(stats.diskInfo?.usedPercentage ?? '0')
const load = stats.cpuInfo?.loadavg?.[0] ?? 0
await upsertMonitoring('server', 'EasyPanel', 'up', {
cpu, ram, disk, load,
uptime_hours: Math.round((stats.uptime ?? 0) / 3600),
mem_total_mb: Math.round(stats.memInfo?.totalMemMb ?? 0),
mem_used_mb: Math.round(stats.memInfo?.usedMemMb ?? 0),
disk_total_gb: stats.diskInfo?.totalGb,
disk_free_gb: stats.diskInfo?.freeGb,
})
console.log(`[EASYPANEL] Server: CPU=${cpu}%, RAM=${ram}%, Disk=${disk}%`)
return true
} }
/** /**
* Collect Docker container/task stats via EasyPanel API. * Collect Docker Swarm service status via SSH to EasyPanel server.
* Updates the 'container' category in monitoring DB. * Usa `docker service ls` para obter replicas actual vs desired.
*/ */
export async function collectEasyPanelContainers(): Promise<boolean> { export async function collectEasyPanelContainers(): Promise<boolean> {
const tasks = await callEasyPanelAPI('monitor.getDockerTaskStats') const easyHost = process.env.EASY_HOST || '5.9.90.70'
if (!tasks) return false const easyUser = process.env.EASY_USER || 'root'
const easyPass = process.env.EASY_PASS || ''
if (!easyPass) return false
try {
const { Client } = await import('ssh2')
const output = await new Promise<string>((resolve, reject) => {
const conn = new Client()
let data = ''
const timer = setTimeout(() => { conn.end(); reject(new Error('timeout')) }, 20000)
conn.on('ready', () => {
conn.exec("docker service ls --format '{{.Name}} {{.Replicas}}'", (err, stream) => {
if (err) { clearTimeout(timer); conn.end(); reject(err); return }
stream.on('data', (chunk: Buffer) => { data += chunk.toString() })
stream.on('close', () => { clearTimeout(timer); conn.end(); resolve(data) })
stream.stderr.on('data', () => {})
})
})
conn.on('error', (err) => { clearTimeout(timer); reject(err) })
conn.connect({ host: easyHost, port: 22, username: easyUser, password: easyPass, readyTimeout: 15000 })
})
let total = 0, up = 0, down = 0 let total = 0, up = 0, down = 0
const unhealthy: string[] = [] const unhealthy: string[] = []
for (const [name, info] of Object.entries(tasks) as [string, { actual: number; desired: number }][]) { for (const line of output.trim().split('\n')) {
if (!line.trim()) continue
const parts = line.trim().split(/\s+/)
const name = parts[0] || ''
const replicas = parts[1] || '0/0'
const [actual, desired] = replicas.split('/').map(Number)
total++ total++
if (info.actual >= info.desired) { if (actual >= desired && desired > 0) {
up++ up++
} else { } else {
down++ down++
@@ -243,6 +311,10 @@ export async function collectEasyPanelContainers(): Promise<boolean> {
console.log(`[EASYPANEL] Containers: ${up}/${total} running${down > 0 ? `, ${down} down: ${unhealthy.join(', ')}` : ''}`) console.log(`[EASYPANEL] Containers: ${up}/${total} running${down > 0 ? `, ${down} down: ${unhealthy.join(', ')}` : ''}`)
return true return true
} catch (err: unknown) {
console.error('[EASYPANEL] Container collection failed:', err instanceof Error ? err.message : err)
return false
}
} }
/** /**
@@ -259,12 +331,20 @@ export async function collectMonitoringData(): Promise<void> {
console.error('[COLLECTOR] Service checks failed:', err instanceof Error ? err.message : err) console.error('[COLLECTOR] Service checks failed:', err instanceof Error ? err.message : err)
} }
// Verificação activa de sites de clientes (HTTP+conteúdo, independente do wp-cron)
try {
const sites = await checkAllSites()
console.log(`[COLLECTOR] Sites (active): ${sites.up} up, ${sites.down} down`)
} catch (err: unknown) {
console.error('[COLLECTOR] Active site checks failed:', err instanceof Error ? err.message : err)
}
// EasyPanel API metrics (replaces SSH for Easy server) // EasyPanel API metrics (replaces SSH for Easy server)
try { try {
const gotStats = await collectEasyPanelMetrics() const gotStats = await collectEasyPanelMetrics()
const gotContainers = await collectEasyPanelContainers() const gotContainers = await collectEasyPanelContainers()
if (!gotStats && !gotContainers) { if (!gotStats && !gotContainers) {
console.warn('[COLLECTOR] EasyPanel API unavailable (check EASYPANEL_API_TOKEN)') console.warn('[COLLECTOR] EasyPanel metrics unavailable (check EASY_HOST/EASY_USER/EASY_PASS)')
} }
} catch (err: unknown) { } catch (err: unknown) {
console.error('[COLLECTOR] EasyPanel collection failed:', err instanceof Error ? err.message : err) console.error('[COLLECTOR] EasyPanel collection failed:', err instanceof Error ? err.message : err)
+211
View File
@@ -0,0 +1,211 @@
/**
* Stack Observer Service
* Reads divergences from observer.db (SQLite)
* @author Descomplicar® | @link descomplicar.pt | @copyright 2026
*/
import sqlite3 from 'sqlite3'
import { open } from 'sqlite'
import path from 'path'
import { fileURLToPath } from 'url'
import os from 'os'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
interface Evaluation {
id: number
source: string
entity_id: string
site_sig: string
result_real: string
result_nominal: string
tokens: number
reincidence_count: number
judge_method: string
confidence: number
evaluated_at: number
category: string
}
interface DivergenceStats {
total_divergences: number
by_category: Record<string, number>
by_judge: Record<string, number>
recent_24h: number
high_confidence: number
}
/**
* Open observer.db connection
* DB is located at ~/Dados/Dev/stack-observer/observer.db
*/
async function openObserverDb() {
const dbPath = path.join(
os.homedir(),
'Dados/Dev/stack-observer/observer.db'
)
return open({
filename: dbPath,
driver: sqlite3.Database
})
}
/**
* Get all divergences (evaluations)
* Filters for result_real != result_nominal
*/
export async function getDivergences() {
const db = await openObserverDb()
try {
// Get divergences with divergence flag
const divergences = await db.all<Evaluation[]>(`
SELECT
id, source, entity_id, site_sig,
result_real, result_nominal,
tokens, reincidence_count,
judge_method, confidence, evaluated_at, category
FROM evaluations
WHERE result_real IS NOT NULL
AND category IN ('automation-liveness', 'automation-failure')
ORDER BY evaluated_at DESC
LIMIT 100
`)
// Calculate stats
const now = Math.floor(Date.now() / 1000)
const day_ago = now - 86400
const stats: DivergenceStats = {
total_divergences: 0,
by_category: {},
by_judge: {},
recent_24h: 0,
high_confidence: 0
}
if (divergences) {
stats.total_divergences = divergences.length
for (const div of divergences) {
// Count by category
stats.by_category[div.category] = (stats.by_category[div.category] ?? 0) + 1
// Count by judge method
stats.by_judge[div.judge_method] = (stats.by_judge[div.judge_method] ?? 0) + 1
// Count recent
if (div.evaluated_at >= day_ago) {
stats.recent_24h++
}
// Count high confidence
if (div.confidence >= 0.9) {
stats.high_confidence++
}
}
}
return {
divergences: divergences ?? [],
stats,
generated_at: new Date().toISOString()
}
} finally {
await db.close()
}
}
/**
* Get single divergence by ID
*/
export async function getDivergenceById(id: string) {
const db = await openObserverDb()
try {
const evaluation = await db.get<Evaluation>(`
SELECT * FROM evaluations
WHERE id = ?
`, [id])
return evaluation ?? null
} finally {
await db.close()
}
}
/**
* Get divergence trend (last 7 days)
*/
export async function getDivergenceTrend() {
const db = await openObserverDb()
try {
interface TrendRow {
date: string
count: number
}
const trend = await db.all<TrendRow[]>(`
SELECT
DATE(evaluated_at, 'unixepoch') as date,
COUNT(*) as count
FROM evaluations
WHERE result_real IS NOT NULL
AND category IN ('automation-liveness', 'automation-failure')
AND evaluated_at >= (unixepoch() - 604800)
GROUP BY date
ORDER BY date DESC
`)
return trend ?? []
} finally {
await db.close()
}
}
/**
* Get list of automations (hermes_jobs)
* F1b: Colector lê de ~/.hermes/data/action-log.db
*/
export async function getAutomations() {
const db = await openObserverDb()
try {
const automations = await db.all(`
SELECT
action_id, who, what, where_field, when_ts,
result, result_real, why, how, detail, session_id, created_at,
ingested_at
FROM hermes_jobs
ORDER BY created_at DESC
LIMIT 100
`)
return automations ?? []
} finally {
await db.close()
}
}
/**
* Get list of gate events
* F1a: Colector lê de /tmp/stack-observer/gate-events.queue
*/
export async function getGateEvents() {
const db = await openObserverDb()
try {
const events = await db.all(`
SELECT
id, gate_id, event_type, payload, timestamp, ingested_at
FROM gate_events
ORDER BY timestamp DESC
LIMIT 100
`)
return events ?? []
} finally {
await db.close()
}
}
Executable → Regular
+9 -9
View File
@@ -15,17 +15,17 @@ interface SSHServer {
pass: string pass: string
} }
// EasyPanel metrics: collected via API in monitoring-collector.ts // CWP Server: só aceita autenticação por chave ed25519 (não por password) — não acessível a partir do container
// Gateway metrics: not needed (just Nginx proxy, covered by HTTP health check) // EasyPanel Server: aceita password auth na porta 22 — usado para métricas CPU/RAM/disk
// Only CWP Server remains on SSH (password auth) // Gateway: apenas proxy Nginx, coberto pelo health check HTTP
const SSH_SERVERS: SSHServer[] = [ const SSH_SERVERS: SSHServer[] = [
{ {
name: 'server', name: 'easy',
monitorName: 'CWP Server', monitorName: 'EasyPanel',
host: process.env.SERVER_HOST || '5.9.90.105', host: process.env.EASY_HOST || '5.9.90.70',
port: parseInt(process.env.SERVER_PORT || '9443'), port: 22,
user: process.env.SERVER_USER || 'root', user: process.env.EASY_USER || 'root',
pass: process.env.SERVER_PASS || '' pass: process.env.EASY_PASS || ''
} }
] ]
+270 -28
View File
@@ -5,6 +5,7 @@ import type { SessionMeta } from '../../types/session.js'
export interface ListFilters { export interface ListFilters {
days?: number days?: number
source?: string
project?: string project?: string
tool?: string tool?: string
skill?: string skill?: string
@@ -52,6 +53,30 @@ export interface WorklogFilters {
offset?: number offset?: number
} }
export interface PatternFilters {
week?: string
severity?: 'info' | 'warning' | 'action'
pattern_key?: string
sinceIso?: string
limit?: number
offset?: number
}
export interface SessionsStats {
days: number
sessions: number
by_day: { day: string; sessions: number }[]
by_source: Record<string, number>
by_outcome: Record<string, number>
tool_top: { tool: string; count: number }[]
skill_top: { skill: string; count: number }[]
patterns_by_type: { pattern_type: string; count: number }[]
gate_blocks: number | null
avg_duration_sec: number | null
last_indexed_at: string | null
latest_pattern_week: string | null
}
export interface SessionsDb { export interface SessionsDb {
upsertSession(meta: SessionMeta): void upsertSession(meta: SessionMeta): void
upsertMany(metas: SessionMeta[]): void upsertMany(metas: SessionMeta[]): void
@@ -59,9 +84,14 @@ export interface SessionsDb {
countSessions(filters: ListFilters): number countSessions(filters: ListFilters): number
getSession(id: string): SessionMeta | null getSession(id: string): SessionMeta | null
deleteByJsonlPath(path: string): void deleteByJsonlPath(path: string): void
storeSessionEvents(sessionId: string, events: import('../../types/session.js').SessionEvent[]): void
getSessionEvents(sessionId: string): import('../../types/session.js').SessionEvent[]
upsertPattern(p: PatternRecord): void upsertPattern(p: PatternRecord): void
getPatternsByWeek(week: string): PatternRecord[] getPatternsByWeek(week: string): PatternRecord[]
getConsecutiveWeeks(pattern_key: string, uptoWeek: string): number getConsecutiveWeeks(pattern_key: string, uptoWeek: string): number
listPatterns(filters: PatternFilters): PatternRecord[]
countPatterns(filters: PatternFilters): number
getStats(days: number): SessionsStats
upsertWorklogComment(c: WorklogCommentRecord): { inserted: boolean } upsertWorklogComment(c: WorklogCommentRecord): { inserted: boolean }
hasWorklogComment(id: number): boolean hasWorklogComment(id: number): boolean
listWorklogComments(filters: WorklogFilters): WorklogCommentRecord[] listWorklogComments(filters: WorklogFilters): WorklogCommentRecord[]
@@ -73,9 +103,11 @@ export interface SessionsDb {
const SCHEMA = ` const SCHEMA = `
CREATE TABLE IF NOT EXISTS sessions ( CREATE TABLE IF NOT EXISTS sessions (
session_id TEXT PRIMARY KEY, session_id TEXT PRIMARY KEY,
project_path TEXT NOT NULL, source TEXT NOT NULL DEFAULT 'claude',
project_slug TEXT NOT NULL, project_path TEXT,
jsonl_path TEXT NOT NULL UNIQUE, project_slug TEXT,
jsonl_path TEXT,
model TEXT,
started_at TEXT NOT NULL, started_at TEXT NOT NULL,
ended_at TEXT, ended_at TEXT,
duration_sec INTEGER, duration_sec INTEGER,
@@ -83,16 +115,34 @@ CREATE TABLE IF NOT EXISTS sessions (
user_messages INTEGER NOT NULL, user_messages INTEGER NOT NULL,
assistant_msgs INTEGER NOT NULL, assistant_msgs INTEGER NOT NULL,
tool_calls INTEGER NOT NULL, tool_calls INTEGER NOT NULL,
input_tokens INTEGER,
output_tokens INTEGER,
estimated_cost REAL,
first_prompt TEXT, first_prompt TEXT,
tools_used TEXT NOT NULL, tools_used TEXT NOT NULL DEFAULT '[]',
skills_invoked TEXT NOT NULL, skills_invoked TEXT NOT NULL DEFAULT '[]',
outcome TEXT NOT NULL, outcome TEXT NOT NULL,
permission_mode TEXT, permission_mode TEXT,
file_size INTEGER NOT NULL, file_size INTEGER NOT NULL DEFAULT 0,
indexed_at TEXT NOT NULL indexed_at TEXT NOT NULL
); );
CREATE INDEX IF NOT EXISTS idx_started ON sessions(started_at DESC); CREATE INDEX IF NOT EXISTS idx_started ON sessions(started_at DESC);
CREATE INDEX IF NOT EXISTS idx_project ON sessions(project_slug, started_at DESC); CREATE INDEX IF NOT EXISTS idx_project ON sessions(project_slug, started_at DESC);
CREATE INDEX IF NOT EXISTS idx_source ON sessions(source);
CREATE TABLE IF NOT EXISTS session_events (
session_id TEXT NOT NULL,
event_index INTEGER NOT NULL,
type TEXT NOT NULL,
timestamp TEXT,
text TEXT,
tool_name TEXT,
tool_input TEXT,
tool_result TEXT,
skill_invoked TEXT,
hook_name TEXT,
PRIMARY KEY (session_id, event_index)
) WITHOUT ROWID;
CREATE TABLE IF NOT EXISTS patterns ( CREATE TABLE IF NOT EXISTS patterns (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -134,9 +184,11 @@ CREATE INDEX IF NOT EXISTS idx_wc_task ON worklog_comments(task_ref);
function rowToMeta(row: Record<string, unknown>): SessionMeta { function rowToMeta(row: Record<string, unknown>): SessionMeta {
return { return {
session_id: row.session_id as string, session_id: row.session_id as string,
project_path: row.project_path as string, source: (row.source as SessionMeta['source']) ?? 'claude',
project_slug: row.project_slug as string, project_path: (row.project_path as string | null) ?? null,
jsonl_path: row.jsonl_path as string, project_slug: (row.project_slug as string | null) ?? null,
jsonl_path: (row.jsonl_path as string | null) ?? null,
model: (row.model as string | null) ?? null,
started_at: row.started_at as string, started_at: row.started_at as string,
ended_at: (row.ended_at as string | null) ?? null, ended_at: (row.ended_at as string | null) ?? null,
duration_sec: (row.duration_sec as number | null) ?? null, duration_sec: (row.duration_sec as number | null) ?? null,
@@ -144,9 +196,12 @@ function rowToMeta(row: Record<string, unknown>): SessionMeta {
user_messages: row.user_messages as number, user_messages: row.user_messages as number,
assistant_msgs: row.assistant_msgs as number, assistant_msgs: row.assistant_msgs as number,
tool_calls: row.tool_calls as number, tool_calls: row.tool_calls as number,
input_tokens: (row.input_tokens as number | null) ?? null,
output_tokens: (row.output_tokens as number | null) ?? null,
estimated_cost: (row.estimated_cost as number | null) ?? null,
first_prompt: (row.first_prompt as string | null) ?? null, first_prompt: (row.first_prompt as string | null) ?? null,
tools_used: JSON.parse(row.tools_used as string), tools_used: row.tools_used ? JSON.parse(row.tools_used as string) : [],
skills_invoked: JSON.parse(row.skills_invoked as string), skills_invoked: row.skills_invoked ? JSON.parse(row.skills_invoked as string) : [],
outcome: row.outcome as SessionMeta['outcome'], outcome: row.outcome as SessionMeta['outcome'],
permission_mode: (row.permission_mode as string | null) ?? null, permission_mode: (row.permission_mode as string | null) ?? null,
file_size: row.file_size as number, file_size: row.file_size as number,
@@ -154,6 +209,47 @@ function rowToMeta(row: Record<string, unknown>): SessionMeta {
} }
} }
function rowToPattern(r: Record<string, unknown>): PatternRecord {
return {
id: r.id as number,
detected_at: r.detected_at as string,
week_iso: r.week_iso as string,
pattern_key: r.pattern_key as string,
title: r.title as string,
description: r.description as string,
severity: r.severity as PatternRecord['severity'],
metric_value: (r.metric_value as number | null) ?? null,
sample_session_ids: JSON.parse(r.sample_session_ids as string),
affected_count: r.affected_count as number,
consecutive_weeks: r.consecutive_weeks as number,
}
}
function buildPatternsWhere(f: PatternFilters): { sql: string; params: Record<string, unknown> } {
const parts: string[] = []
const params: Record<string, unknown> = {}
if (f.week) {
parts.push('week_iso = @week')
params.week = f.week
}
if (f.severity) {
parts.push('severity = @severity')
params.severity = f.severity
}
if (f.pattern_key) {
parts.push('pattern_key = @pattern_key')
params.pattern_key = f.pattern_key
}
if (f.sinceIso) {
parts.push('detected_at >= @since')
params.since = f.sinceIso
}
return {
sql: parts.length ? 'WHERE ' + parts.join(' AND ') : '',
params,
}
}
function buildWhere(f: ListFilters): { sql: string; params: Record<string, unknown> } { function buildWhere(f: ListFilters): { sql: string; params: Record<string, unknown> } {
const parts: string[] = [] const parts: string[] = []
const params: Record<string, unknown> = {} const params: Record<string, unknown> = {}
@@ -162,6 +258,10 @@ function buildWhere(f: ListFilters): { sql: string; params: Record<string, unkno
parts.push('started_at >= @cutoff') parts.push('started_at >= @cutoff')
params.cutoff = cutoff params.cutoff = cutoff
} }
if (f.source) {
parts.push('source = @source')
params.source = f.source
}
if (f.project) { if (f.project) {
parts.push('project_slug = @project') parts.push('project_slug = @project')
params.project = f.project params.project = f.project
@@ -191,17 +291,40 @@ export function openSessionsDb(dbPath: string): SessionsDb {
db.pragma('synchronous = NORMAL') db.pragma('synchronous = NORMAL')
db.exec(SCHEMA) db.exec(SCHEMA)
// Migração: adicionar colunas novas se não existirem
const existingCols = db.prepare("PRAGMA table_info(sessions)").all() as { name: string }[]
const colNames = new Set(existingCols.map(c => c.name))
const migrations: [string, string][] = [
['source', "TEXT NOT NULL DEFAULT 'claude'"],
['model', 'TEXT'],
['input_tokens', 'INTEGER'],
['output_tokens', 'INTEGER'],
['estimated_cost', 'REAL'],
]
for (const [col, type] of migrations) {
if (!colNames.has(col)) {
db.exec(`ALTER TABLE sessions ADD COLUMN ${col} ${type}`)
}
}
if (!colNames.has('source')) {
db.exec('CREATE INDEX IF NOT EXISTS idx_source ON sessions(source)')
}
const upsertStmt = db.prepare(` const upsertStmt = db.prepare(`
INSERT INTO sessions (session_id, project_path, project_slug, jsonl_path, started_at, ended_at, INSERT INTO sessions (session_id, source, project_path, project_slug, jsonl_path, model,
duration_sec, event_count, user_messages, assistant_msgs, tool_calls, first_prompt, started_at, ended_at, duration_sec, event_count, user_messages, assistant_msgs, tool_calls,
input_tokens, output_tokens, estimated_cost, first_prompt,
tools_used, skills_invoked, outcome, permission_mode, file_size, indexed_at) tools_used, skills_invoked, outcome, permission_mode, file_size, indexed_at)
VALUES (@session_id, @project_path, @project_slug, @jsonl_path, @started_at, @ended_at, VALUES (@session_id, @source, @project_path, @project_slug, @jsonl_path, @model,
@duration_sec, @event_count, @user_messages, @assistant_msgs, @tool_calls, @first_prompt, @started_at, @ended_at, @duration_sec, @event_count, @user_messages, @assistant_msgs, @tool_calls,
@input_tokens, @output_tokens, @estimated_cost, @first_prompt,
@tools_used, @skills_invoked, @outcome, @permission_mode, @file_size, @indexed_at) @tools_used, @skills_invoked, @outcome, @permission_mode, @file_size, @indexed_at)
ON CONFLICT(session_id) DO UPDATE SET ON CONFLICT(session_id) DO UPDATE SET
source = excluded.source,
project_path = excluded.project_path, project_path = excluded.project_path,
project_slug = excluded.project_slug, project_slug = excluded.project_slug,
jsonl_path = excluded.jsonl_path, jsonl_path = excluded.jsonl_path,
model = excluded.model,
started_at = excluded.started_at, started_at = excluded.started_at,
ended_at = excluded.ended_at, ended_at = excluded.ended_at,
duration_sec = excluded.duration_sec, duration_sec = excluded.duration_sec,
@@ -209,6 +332,9 @@ export function openSessionsDb(dbPath: string): SessionsDb {
user_messages = excluded.user_messages, user_messages = excluded.user_messages,
assistant_msgs = excluded.assistant_msgs, assistant_msgs = excluded.assistant_msgs,
tool_calls = excluded.tool_calls, tool_calls = excluded.tool_calls,
input_tokens = excluded.input_tokens,
output_tokens = excluded.output_tokens,
estimated_cost = excluded.estimated_cost,
first_prompt = excluded.first_prompt, first_prompt = excluded.first_prompt,
tools_used = excluded.tools_used, tools_used = excluded.tools_used,
skills_invoked = excluded.skills_invoked, skills_invoked = excluded.skills_invoked,
@@ -288,21 +414,50 @@ export function openSessionsDb(dbPath: string): SessionsDb {
consecutive_weeks: p.consecutive_weeks, consecutive_weeks: p.consecutive_weeks,
}) })
}, },
storeSessionEvents(sessionId, events) {
const del = db.prepare('DELETE FROM session_events WHERE session_id = ?')
const ins = db.prepare(`
INSERT INTO session_events (session_id, event_index, type, timestamp, text,
tool_name, tool_input, tool_result, skill_invoked, hook_name)
VALUES (@session_id, @index, @type, @timestamp, @text,
@tool_name, @tool_input, @tool_result, @skill_invoked, @hook_name)
`)
db.transaction(() => {
del.run(sessionId)
for (const e of events) {
ins.run({
session_id: sessionId,
index: e.index,
type: e.type,
timestamp: e.timestamp ?? null,
text: e.text,
tool_name: e.tool_name,
tool_input: e.tool_input ? JSON.stringify(e.tool_input) : null,
tool_result: typeof e.tool_result === 'string' ? e.tool_result.slice(0, 50000) : e.tool_result != null ? JSON.stringify(e.tool_result).slice(0, 50000) : null,
skill_invoked: e.skill_invoked,
hook_name: e.hook_name,
})
}
})()
},
getSessionEvents(sessionId) {
const rows = db.prepare('SELECT * FROM session_events WHERE session_id = ? ORDER BY event_index').all(sessionId) as Record<string, unknown>[]
return rows.map((r) => ({
index: r.event_index as number,
type: r.type as import('../../types/session.js').SessionEventType,
timestamp: (r.timestamp as string | null) ?? null,
raw: {}, // not stored
text: (r.text as string | null) ?? null,
tool_name: (r.tool_name as string | null) ?? null,
tool_input: r.tool_input ? JSON.parse(r.tool_input as string) as Record<string, unknown> : null,
tool_result: r.tool_result ?? null,
skill_invoked: (r.skill_invoked as string | null) ?? null,
hook_name: (r.hook_name as string | null) ?? null,
}))
},
getPatternsByWeek(week: string): PatternRecord[] { getPatternsByWeek(week: string): PatternRecord[] {
const rows = db.prepare('SELECT * FROM patterns WHERE week_iso = ? ORDER BY severity DESC, affected_count DESC').all(week) as Record<string, unknown>[] const rows = db.prepare('SELECT * FROM patterns WHERE week_iso = ? ORDER BY severity DESC, affected_count DESC').all(week) as Record<string, unknown>[]
return rows.map((r) => ({ return rows.map(rowToPattern)
id: r.id as number,
detected_at: r.detected_at as string,
week_iso: r.week_iso as string,
pattern_key: r.pattern_key as string,
title: r.title as string,
description: r.description as string,
severity: r.severity as PatternRecord['severity'],
metric_value: (r.metric_value as number | null) ?? null,
sample_session_ids: JSON.parse(r.sample_session_ids as string),
affected_count: r.affected_count as number,
consecutive_weeks: r.consecutive_weeks as number,
}))
}, },
getConsecutiveWeeks(pattern_key: string, uptoWeek: string): number { getConsecutiveWeeks(pattern_key: string, uptoWeek: string): number {
// Conta semanas consecutivas até uptoWeek (inclusive) em que pattern_key apareceu // Conta semanas consecutivas até uptoWeek (inclusive) em que pattern_key apareceu
@@ -320,6 +475,93 @@ export function openSessionsDb(dbPath: string): SessionsDb {
} }
return count return count
}, },
listPatterns(filters: PatternFilters): PatternRecord[] {
const { sql, params } = buildPatternsWhere(filters)
const limit = filters.limit ?? 50
const offset = filters.offset ?? 0
const rows = db
.prepare(`SELECT * FROM patterns ${sql} ORDER BY week_iso DESC, severity DESC, affected_count DESC LIMIT @limit OFFSET @offset`)
.all({ ...params, limit, offset }) as Record<string, unknown>[]
return rows.map(rowToPattern)
},
countPatterns(filters: PatternFilters): number {
const { sql, params } = buildPatternsWhere(filters)
const row = db.prepare(`SELECT COUNT(*) as c FROM patterns ${sql}`).get(params) as { c: number }
return row.c
},
getStats(days: number): SessionsStats {
const cutoff = new Date(Date.now() - days * 86400_000).toISOString()
const totals = db.prepare(`
SELECT COUNT(*) as sessions, AVG(duration_sec) as avg_duration
FROM sessions WHERE started_at >= ?
`).get(cutoff) as { sessions: number; avg_duration: number | null }
const byDay = db.prepare(`
SELECT substr(started_at, 1, 10) as day, COUNT(*) as sessions
FROM sessions WHERE started_at >= ?
GROUP BY day ORDER BY day
`).all(cutoff) as { day: string; sessions: number }[]
const bySource: Record<string, number> = {}
for (const r of db.prepare('SELECT source, COUNT(*) as c FROM sessions WHERE started_at >= ? GROUP BY source').all(cutoff) as { source: string; c: number }[]) {
bySource[r.source] = r.c
}
const byOutcome: Record<string, number> = {}
for (const r of db.prepare('SELECT outcome, COUNT(*) as c FROM sessions WHERE started_at >= ? GROUP BY outcome').all(cutoff) as { outcome: string; c: number }[]) {
byOutcome[r.outcome] = r.c
}
const toolTop = db.prepare(`
SELECT je.value as tool, COUNT(*) as count
FROM sessions s, json_each(s.tools_used) je
WHERE s.started_at >= ?
GROUP BY je.value ORDER BY count DESC LIMIT 10
`).all(cutoff) as { tool: string; count: number }[]
const skillTop = db.prepare(`
SELECT je.value as skill, COUNT(*) as count
FROM sessions s, json_each(s.skills_invoked) je
WHERE s.started_at >= ?
GROUP BY je.value ORDER BY count DESC LIMIT 10
`).all(cutoff) as { skill: string; count: number }[]
// Tipo do padrão = prefixo de pattern_key antes de ':' (ex.: 'skill_error_rate:X' → 'skill_error_rate')
const patternsByType = db.prepare(`
SELECT CASE WHEN instr(pattern_key, ':') > 0
THEN substr(pattern_key, 1, instr(pattern_key, ':') - 1)
ELSE pattern_key END as pattern_type,
COUNT(*) as count
FROM patterns WHERE detected_at >= ?
GROUP BY pattern_type ORDER BY count DESC
`).all(cutoff) as { pattern_type: string; count: number }[]
// gate_events só existe quando F1 (arco C3-gates) entregar — null até lá
let gateBlocks: number | null = null
const hasGateEvents = db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='gate_events'").get()
if (hasGateEvents) {
const r = db.prepare("SELECT COUNT(*) as c FROM gate_events WHERE decision = 'block' AND ts >= ?").get(cutoff) as { c: number }
gateBlocks = r.c
}
const lastIndexed = db.prepare('SELECT MAX(indexed_at) as m FROM sessions').get() as { m: string | null }
const latestWeek = db.prepare('SELECT MAX(week_iso) as m FROM patterns').get() as { m: string | null }
return {
days,
sessions: totals.sessions,
by_day: byDay,
by_source: bySource,
by_outcome: byOutcome,
tool_top: toolTop,
skill_top: skillTop,
patterns_by_type: patternsByType,
gate_blocks: gateBlocks,
avg_duration_sec: totals.avg_duration != null ? Math.round(totals.avg_duration) : null,
last_indexed_at: lastIndexed.m,
latest_pattern_week: latestWeek.m,
}
},
upsertWorklogComment(c: WorklogCommentRecord): { inserted: boolean } { upsertWorklogComment(c: WorklogCommentRecord): { inserted: boolean } {
const existing = db.prepare('SELECT 1 FROM worklog_comments WHERE id = ?').get(c.id) const existing = db.prepare('SELECT 1 FROM worklog_comments WHERE id = ?').get(c.id)
const inserted = !existing const inserted = !existing
+151
View File
@@ -0,0 +1,151 @@
import Database from 'better-sqlite3'
import { homedir } from 'os'
import { join } from 'path'
import type { SessionMeta } from '../../types/session.js'
import { openSessionsDb } from './db.js'
const HERMES_DB = join(homedir(), '.hermes', 'state.db')
const DEFAULT_DB_PATH = join(homedir(), '.claude-work', 'sessions.db')
export interface HermesSessionRow {
id: string
model: string | null
started_at: number | null
ended_at: number | null
end_reason: string | null
message_count: number
tool_call_count: number
input_tokens: number | null
output_tokens: number | null
estimated_cost_usd: number | null
title: string | null
api_call_count: number | null
}
export interface HermesMessageRow {
session_id: string
role: string
content: string | null
tool_name: string | null
timestamp: number | null
}
/**
* Lê sessões do state.db do Hermes Agent
*/
export function readHermesSessions(): HermesSessionRow[] {
const db = new Database(HERMES_DB, { readonly: true })
try {
return db.prepare(`
SELECT id, model, started_at, ended_at, end_reason,
message_count, tool_call_count,
input_tokens, output_tokens, estimated_cost_usd,
title, api_call_count
FROM sessions
ORDER BY started_at DESC
`).all() as HermesSessionRow[]
} finally {
db.close()
}
}
/**
* Lê mensagens de uma sessão Hermes
*/
export function readHermesMessages(sessionId: string): HermesMessageRow[] {
const db = new Database(HERMES_DB, { readonly: true })
try {
return db.prepare(`
SELECT session_id, role, content, tool_name, timestamp
FROM messages
WHERE session_id = ?
ORDER BY timestamp ASC
`).all(sessionId) as HermesMessageRow[]
} finally {
db.close()
}
}
function toHermesMeta(row: HermesSessionRow, messages: HermesMessageRow[]): SessionMeta {
const userMsgs = messages.filter(m => m.role === 'user')
const assistantMsgs = messages.filter(m => m.role === 'assistant')
const toolMsgs = messages.filter(m => m.role === 'tool')
const firstUser = userMsgs[0]
const toolsUsed = [...new Set(messages.filter(m => m.tool_name).map(m => m.tool_name!))]
return {
session_id: row.id,
source: 'hermes',
project_path: null,
project_slug: null,
jsonl_path: null,
model: row.model ?? null,
started_at: row.started_at ? new Date(row.started_at * 1000).toISOString() : new Date(0).toISOString(),
ended_at: row.ended_at ? new Date(row.ended_at * 1000).toISOString() : null,
duration_sec: row.started_at && row.ended_at ? Math.round(row.ended_at - row.started_at) : null,
event_count: messages.length,
user_messages: userMsgs.length,
assistant_msgs: assistantMsgs.length,
tool_calls: toolMsgs.length,
input_tokens: row.input_tokens ?? null,
output_tokens: row.output_tokens ?? null,
estimated_cost: row.estimated_cost_usd ?? null,
first_prompt: firstUser?.content?.slice(0, 500) ?? null,
tools_used: toolsUsed,
skills_invoked: [],
outcome: row.end_reason === 'completed' ? 'completed'
: row.end_reason === 'interrupted' || row.end_reason === 'stopped' ? 'interrupted'
: row.end_reason === 'error' ? 'error'
: 'unknown',
permission_mode: null,
file_size: 0,
indexed_at: new Date().toISOString(),
}
}
export interface IndexResult {
indexed: number
failed: number
}
/**
* Indexa todas as sessões Hermes no state.db para o sessions.db unificado
*/
export async function indexHermesSessions(
options: { dbPath?: string } = {},
): Promise<IndexResult> {
let indexed = 0
let failed = 0
const db = openSessionsDb(options.dbPath ?? DEFAULT_DB_PATH)
try {
const sessions = readHermesSessions()
let batch: SessionMeta[] = []
const BATCH = 50
for (const session of sessions) {
try {
const messages = readHermesMessages(session.id)
const meta = toHermesMeta(session, messages)
batch.push(meta)
if (batch.length >= BATCH) {
db.upsertMany(batch)
indexed += batch.length
batch = []
}
} catch (err) {
failed++
console.error(`[hermes-indexer] erro em sessão ${session.id}:`, err)
}
}
if (batch.length > 0) {
db.upsertMany(batch)
indexed += batch.length
}
} finally {
db.close()
}
return { indexed, failed }
}
+53 -6
View File
@@ -3,6 +3,8 @@ import { homedir } from 'os'
import { join } from 'path' import { join } from 'path'
import { parseSessionFile } from './parser.js' import { parseSessionFile } from './parser.js'
import { openSessionsDb, type SessionsDb } from './db.js' import { openSessionsDb, type SessionsDb } from './db.js'
import { indexHermesSessions } from './hermes-indexer.js'
import { indexOCSessions } from './opencode-indexer.js'
import type { SessionMeta } from '../../types/session.js' import type { SessionMeta } from '../../types/session.js'
export const PROJECTS_ROOT = join(homedir(), '.claude', 'projects') export const PROJECTS_ROOT = join(homedir(), '.claude', 'projects')
@@ -46,32 +48,52 @@ export function findAllJsonl(root: string = PROJECTS_ROOT): string[] {
* Indexa um único ficheiro (parse + upsert). Uso individual — útil para o watcher (Task 8). * Indexa um único ficheiro (parse + upsert). Uso individual — útil para o watcher (Task 8).
*/ */
export async function indexFile(db: SessionsDb, path: string): Promise<void> { export async function indexFile(db: SessionsDb, path: string): Promise<void> {
const { meta } = await parseSessionFile(path) const { meta, events } = await parseSessionFile(path)
db.upsertSession(meta) db.upsertSession(meta)
db.storeSessionEvents(meta.session_id, events)
} }
export interface IndexAllOptions { export interface IndexAllOptions {
dbPath?: string dbPath?: string
sources?: ('claude' | 'hermes' | 'opencode')[]
onProgress?: (done: number, total: number) => void onProgress?: (done: number, total: number) => void
} }
/** /**
* Full scan: percorre todos os JSONL e faz upsert em lote (batch 50 via transacção). * Full scan: indexa Claude Code (JSONL) + Hermes (state.db) + OpenCode (opencode.db).
*/ */
export async function indexAll( export async function indexAll(
options: IndexAllOptions = {}, options: IndexAllOptions = {},
): Promise<{ indexed: number; failed: number }> { ): Promise<{ indexed: number; failed: number }> {
const db = openSessionsDb(options.dbPath ?? DEFAULT_DB_PATH) const sources = options.sources ?? ['claude', 'hermes', 'opencode']
const files = findAllJsonl()
const BATCH = 50
let indexed = 0 let indexed = 0
let failed = 0 let failed = 0
const dbPath = options.dbPath ?? DEFAULT_DB_PATH
if (sources.includes('claude')) {
const db = openSessionsDb(dbPath)
const files = findAllJsonl()
const BATCH = 50
let batch: SessionMeta[] = [] let batch: SessionMeta[] = []
try { try {
for (let i = 0; i < files.length; i++) { for (let i = 0; i < files.length; i++) {
try { try {
const { meta } = await parseSessionFile(files[i]) const { meta, events } = await parseSessionFile(files[i])
meta.source = 'claude'
meta.model = meta.model ?? null
meta.ended_at = meta.ended_at ?? null
meta.duration_sec = meta.duration_sec ?? null
meta.input_tokens = meta.input_tokens ?? null
meta.output_tokens = meta.output_tokens ?? null
meta.estimated_cost = meta.estimated_cost ?? null
meta.first_prompt = meta.first_prompt ?? null
meta.permission_mode = meta.permission_mode ?? null
meta.project_path = meta.project_path ?? null
meta.project_slug = meta.project_slug ?? null
meta.jsonl_path = meta.jsonl_path ?? null
// Store events per-session (delete+insert)
db.storeSessionEvents(meta.session_id, events)
batch.push(meta) batch.push(meta)
if (batch.length >= BATCH) { if (batch.length >= BATCH) {
db.upsertMany(batch) db.upsertMany(batch)
@@ -93,6 +115,31 @@ export async function indexAll(
} finally { } finally {
db.close() db.close()
} }
}
if (sources.includes('hermes')) {
try {
const result = await indexHermesSessions({ dbPath })
indexed += result.indexed
failed += result.failed
console.log(`[indexer] Hermes: ${result.indexed} indexadas, ${result.failed} falhas`)
} catch (err) {
failed++
console.error('[indexer] Erro ao indexar Hermes:', err)
}
}
if (sources.includes('opencode')) {
try {
const result = await indexOCSessions({ dbPath })
indexed += result.indexed
failed += result.failed
console.log(`[indexer] OpenCode: ${result.indexed} indexadas, ${result.failed} falhas`)
} catch (err) {
failed++
console.error('[indexer] Erro ao indexar OpenCode:', err)
}
}
return { indexed, failed } return { indexed, failed }
} }
+208
View File
@@ -0,0 +1,208 @@
import Database from 'better-sqlite3'
import { homedir } from 'os'
import { join } from 'path'
import type { SessionMeta } from '../../types/session.js'
import { openSessionsDb } from './db.js'
const OPENCODE_DB = join(homedir(), '.local', 'share', 'opencode', 'opencode.db')
const DEFAULT_DB_PATH = join(homedir(), '.claude-work', 'sessions.db')
export interface OCSessionRow {
id: string
project_id: string | null
title: string | null
summary_additions: number | null
summary_deletions: number | null
summary_files: number | null
time_created: number | null
time_updated: number | null
path: string | null
}
export interface OCMessageRow {
id: string
session_id: string
time_created: number | null
data: string | null
}
export interface OCPartRow {
id: string
message_id: string
session_id: string
time_created: number | null
data: string | null
}
export interface OCProjectRow {
id: string
name: string | null
worktree: string | null
}
/**
* Lê projectos OpenCode
*/
export function readOCProjects(): OCProjectRow[] {
const db = new Database(OPENCODE_DB, { readonly: true })
try {
return db.prepare('SELECT id, name, worktree FROM project').all() as OCProjectRow[]
} finally {
db.close()
}
}
/**
* Lê sessões OpenCode
*/
export function readOCSessions(): OCSessionRow[] {
const db = new Database(OPENCODE_DB, { readonly: true })
try {
return db.prepare(`
SELECT s.id, s.project_id, s.title, s.summary_additions, s.summary_deletions,
s.summary_files, s.time_created, s.time_updated, s.path
FROM session s
ORDER BY s.time_created DESC
`).all() as OCSessionRow[]
} finally {
db.close()
}
}
/**
* Lê todas as messages + parts de uma sessão OpenCode
*/
export function readOCSessionData(sessionId: string): { messages: OCMessageRow[]; parts: OCPartRow[] } {
const db = new Database(OPENCODE_DB, { readonly: true })
try {
const messages = db.prepare(
'SELECT id, session_id, time_created, data FROM message WHERE session_id = ? ORDER BY time_created ASC'
).all(sessionId) as OCMessageRow[]
const messageIds = messages.map(m => m.id)
let parts: OCPartRow[] = []
if (messageIds.length > 0) {
const placeholders = messageIds.map(() => '?').join(',')
parts = db.prepare(
`SELECT id, message_id, session_id, time_created, data FROM part WHERE message_id IN (${placeholders}) ORDER BY time_created ASC`
).all(...messageIds) as OCPartRow[]
}
return { messages, parts }
} finally {
db.close()
}
}
function parsePartData(data: string | null): { role?: string; content?: string } {
if (!data) return {}
try {
return JSON.parse(data) as { role?: string; content?: string }
} catch {
return {}
}
}
function toOCMeta(
session: OCSessionRow,
projects: Map<string, OCProjectRow>,
_messages: OCMessageRow[],
parts: OCPartRow[],
): SessionMeta {
const userMsgs = parts.filter(p => parsePartData(p.data).role === 'user')
const assistantMsgs = parts.filter(p => parsePartData(p.data).role === 'assistant')
const project = session.project_id ? projects.get(session.project_id) : undefined
const firstUser = userMsgs[0]
const firstUserContent = firstUser ? parsePartData(firstUser.data).content : null
let toolsUsed: string[] = []
for (const p of parts) {
const d = parsePartData(p.data)
if (d.content && (d.content.includes('tool_use') || d.content.includes('tool_result'))) {
try {
const parsed = JSON.parse(d.content)
if (parsed.name) toolsUsed.push(parsed.name)
} catch { /* não é JSON */ }
}
}
toolsUsed = [...new Set(toolsUsed)]
const startTs = session.time_created ? session.time_created / 1000 : Date.now() / 1000
const endTs = session.time_updated ? session.time_updated / 1000 : startTs
return {
session_id: session.id,
source: 'opencode',
project_path: project?.worktree ?? session.path ?? null,
project_slug: project?.name ?? session.path?.split('/').pop() ?? null,
jsonl_path: null,
model: null,
started_at: new Date(startTs * 1000).toISOString(),
ended_at: new Date(endTs * 1000).toISOString(),
duration_sec: Math.round(endTs - startTs),
event_count: parts.length,
user_messages: userMsgs.length,
assistant_msgs: assistantMsgs.length,
tool_calls: 0,
input_tokens: null,
output_tokens: null,
estimated_cost: null,
first_prompt: firstUserContent?.slice(0, 500) ?? null,
tools_used: toolsUsed,
skills_invoked: [],
outcome: 'completed',
permission_mode: null,
file_size: session.summary_additions ?? 0,
indexed_at: new Date().toISOString(),
}
}
export interface IndexResult {
indexed: number
failed: number
}
/**
* Indexa todas as sessões OpenCode para o sessions.db unificado
*/
export async function indexOCSessions(
options: { dbPath?: string } = {},
): Promise<IndexResult> {
let indexed = 0
let failed = 0
const db = openSessionsDb(options.dbPath ?? DEFAULT_DB_PATH)
try {
const sessions = readOCSessions()
const projects = readOCProjects()
const projectMap = new Map(projects.map(p => [p.id, p]))
let batch: SessionMeta[] = []
const BATCH = 50
for (const session of sessions) {
try {
const { messages, parts } = readOCSessionData(session.id)
const meta = toOCMeta(session, projectMap, messages, parts)
batch.push(meta)
if (batch.length >= BATCH) {
db.upsertMany(batch)
indexed += batch.length
batch = []
}
} catch (err) {
failed++
console.error(`[opencode-indexer] erro em sessão ${session.id}:`, err)
}
}
if (batch.length > 0) {
db.upsertMany(batch)
indexed += batch.length
}
} finally {
db.close()
}
return { indexed, failed }
}
+5
View File
@@ -186,9 +186,11 @@ export async function parseSessionFile(jsonlPath: string): Promise<ParseResult>
const meta: SessionMeta = { const meta: SessionMeta = {
session_id: sessionId, session_id: sessionId,
source: 'claude',
project_path: dirname(jsonlPath), project_path: dirname(jsonlPath),
project_slug: slugFromProjectPath(jsonlPath), project_slug: slugFromProjectPath(jsonlPath),
jsonl_path: jsonlPath, jsonl_path: jsonlPath,
model: null,
started_at: firstTs ?? new Date(stats.birthtimeMs).toISOString(), started_at: firstTs ?? new Date(stats.birthtimeMs).toISOString(),
ended_at: lastTs, ended_at: lastTs,
duration_sec: durationSec, duration_sec: durationSec,
@@ -196,6 +198,9 @@ export async function parseSessionFile(jsonlPath: string): Promise<ParseResult>
user_messages: userMessages, user_messages: userMessages,
assistant_msgs: assistantMsgs, assistant_msgs: assistantMsgs,
tool_calls: toolCalls, tool_calls: toolCalls,
input_tokens: null,
output_tokens: null,
estimated_cost: null,
first_prompt: firstPrompt, first_prompt: firstPrompt,
tools_used: [...toolsUsed], tools_used: [...toolsUsed],
skills_invoked: [...skillsInvoked], skills_invoked: [...skillsInvoked],
+5 -2
View File
@@ -188,7 +188,9 @@ export function detectSkillToolPairs(ctx: DetectCtx): Pattern[] {
})) }))
} }
/** 4. Duration outliers: sessões > p95 por projecto com outcome != completed. */ /** 4. Duration outliers: sessões > p95 por projecto com outcome de falha medido (error/interrupted).
* `unknown` não conta: significa outcome não medido (ex.: sessões cron do Hermes, project_slug NULL,
* 100% `unknown`), e contá-lo gerava ~5% de "outliers" todas as semanas por construção (#14404). */
export function detectDurationOutliers(ctx: DetectCtx): Pattern[] { export function detectDurationOutliers(ctx: DetectCtx): Pattern[] {
const rows = baseRows(ctx).filter((r) => r.duration_sec != null && r.duration_sec > 0) const rows = baseRows(ctx).filter((r) => r.duration_sec != null && r.duration_sec > 0)
const byProject = new Map<string, Array<typeof rows[number]>>() const byProject = new Map<string, Array<typeof rows[number]>>()
@@ -203,7 +205,8 @@ export function detectDurationOutliers(ctx: DetectCtx): Pattern[] {
const durations = arr.map((r) => r.duration_sec as number).sort((a, b) => a - b) const durations = arr.map((r) => r.duration_sec as number).sort((a, b) => a - b)
const p95Idx = Math.max(0, Math.floor(durations.length * 0.95) - 1) const p95Idx = Math.max(0, Math.floor(durations.length * 0.95) - 1)
const p95 = durations[p95Idx] const p95 = durations[p95Idx]
const outliers = arr.filter((r) => (r.duration_sec as number) > p95 && r.outcome !== 'completed') const outliers = arr.filter((r) => (r.duration_sec as number) > p95 &&
r.outcome !== 'completed' && r.outcome !== 'unknown')
if (outliers.length < 3) continue if (outliers.length < 3) continue
out.push({ out.push({
pattern_key: `duration_outliers:${proj}`, pattern_key: `duration_outliers:${proj}`,
+1 -1
View File
@@ -154,7 +154,7 @@ function parseProblemItem(raw: string): { problema: string; solucao: string } {
function extractAllLiItems(root: HTMLElement): string[] { function extractAllLiItems(root: HTMLElement): string[] {
return root return root
.querySelectorAll('li') .querySelectorAll('li')
.map((li) => textOf(li)) .map((li: HTMLElement) => textOf(li))
.filter(Boolean) .filter(Boolean)
} }
+228
View File
@@ -0,0 +1,228 @@
/**
* Testes das rotas /patterns e /stats (contrato C4-análise, Pipeline QT F4.1).
* Cobre: filtros da lista de padrões, agregados de stats, validação Zod,
* gate_blocks condicional (tabela gate_events pode não existir) e a
* não-colisão de /patterns com o param route /:id do router de sessões.
* @author Descomplicar® | Projecto Observabilidade (Espelho)
*/
import { describe, it, expect, beforeAll } from 'vitest'
import express from 'express'
import request from 'supertest'
import { mkdtempSync } from 'fs'
import { tmpdir } from 'os'
import { join } from 'path'
import { openSessionsDb, type SessionsDb, type PatternRecord } from '../services/sessions/db.js'
import { createSessionsRouter, createSessionsAnalysisRouter } from '../routes/sessions.js'
import type { SessionMeta } from '../types/session.js'
function meta(overrides: Partial<SessionMeta> = {}): SessionMeta {
return {
session_id: 's-' + Math.random().toString(36).slice(2, 10),
source: 'claude',
project_path: '/tmp/p',
project_slug: 'p',
jsonl_path: '/tmp/p/' + Math.random().toString(36).slice(2) + '.jsonl',
model: null,
started_at: new Date().toISOString(),
ended_at: null,
duration_sec: 60,
event_count: 10,
user_messages: 2,
assistant_msgs: 5,
tool_calls: 3,
input_tokens: null,
output_tokens: null,
estimated_cost: null,
first_prompt: 'teste',
tools_used: ['Bash'],
skills_invoked: [],
outcome: 'completed',
permission_mode: 'default',
file_size: 1000,
indexed_at: new Date().toISOString(),
...overrides,
}
}
function pattern(overrides: Partial<PatternRecord> = {}): PatternRecord {
return {
detected_at: new Date().toISOString(),
week_iso: '2026-W27',
pattern_key: 'abandoned_sessions',
title: 'Sessões abandonadas',
description: 'd',
severity: 'warning',
metric_value: 6,
sample_session_ids: ['a', 'b'],
affected_count: 6,
consecutive_weeks: 1,
...overrides,
}
}
describe('rotas de análise C4 (/patterns, /stats)', () => {
let app: express.Express
let db: SessionsDb
beforeAll(() => {
const dbPath = join(mkdtempSync(join(tmpdir(), 'obs-c4-')), 'sessions.db')
db = openSessionsDb(dbPath)
// Sessões: 2 claude completed hoje, 1 opencode error há 3 dias, 1 antiga (400 dias)
const now = Date.now()
db.upsertSession(meta({ session_id: 's1', tools_used: ['Bash', 'Read'], skills_invoked: ['wp-dev'] }))
db.upsertSession(meta({ session_id: 's2', tools_used: ['Bash'] }))
db.upsertSession(meta({
session_id: 's3',
source: 'opencode',
outcome: 'error',
started_at: new Date(now - 3 * 86400_000).toISOString(),
}))
db.upsertSession(meta({
session_id: 's-antiga',
started_at: new Date(now - 400 * 86400_000).toISOString(),
}))
// Padrões: 2 semanas, 2 tipos, severities distintas
db.upsertPattern(pattern({ week_iso: '2026-W26', pattern_key: 'skill_error_rate:wp-dev', severity: 'action' }))
db.upsertPattern(pattern({ week_iso: '2026-W27', pattern_key: 'skill_error_rate:wp-dev', severity: 'action', consecutive_weeks: 2 }))
db.upsertPattern(pattern({ week_iso: '2026-W27', pattern_key: 'abandoned_sessions', severity: 'warning' }))
app = express()
app.use('/api/sessions', createSessionsRouter(db))
app.use('/api', createSessionsAnalysisRouter(db))
})
describe('GET /api/patterns', () => {
it('lista todos os padrões por omissão, mais recentes primeiro', async () => {
const res = await request(app).get('/api/patterns')
expect(res.status).toBe(200)
expect(res.body.total).toBe(3)
expect(res.body.items).toHaveLength(3)
expect(res.body.items[0].week_iso).toBe('2026-W27')
})
it('filtra por semana ISO', async () => {
const res = await request(app).get('/api/patterns').query({ week: '2026-W27' })
expect(res.status).toBe(200)
expect(res.body.total).toBe(2)
expect(res.body.week).toBe('2026-W27')
expect(res.body.items.every((p: PatternRecord) => p.week_iso === '2026-W27')).toBe(true)
})
it('filtra por severity e por key', async () => {
const bySev = await request(app).get('/api/patterns').query({ severity: 'warning' })
expect(bySev.body.total).toBe(1)
expect(bySev.body.items[0].pattern_key).toBe('abandoned_sessions')
const byKey = await request(app).get('/api/patterns').query({ key: 'skill_error_rate:wp-dev' })
expect(byKey.body.total).toBe(2)
})
it('pagina com limit/offset', async () => {
const res = await request(app).get('/api/patterns').query({ limit: 2, offset: 2 })
expect(res.status).toBe(200)
expect(res.body.total).toBe(3)
expect(res.body.items).toHaveLength(1)
})
it('devolve sample_session_ids e consecutive_weeks desserializados', async () => {
const res = await request(app).get('/api/patterns').query({ week: '2026-W27', key: 'skill_error_rate:wp-dev' })
expect(res.body.items[0].sample_session_ids).toEqual(['a', 'b'])
expect(res.body.items[0].consecutive_weeks).toBe(2)
})
it('rejeita week com formato inválido', async () => {
const res = await request(app).get('/api/patterns').query({ week: '2026-27' })
expect(res.status).toBe(400)
})
it('responde também em /api/sessions/patterns sem colidir com /:id', async () => {
const res = await request(app).get('/api/sessions/patterns')
expect(res.status).toBe(200)
expect(res.body.total).toBe(3)
})
})
describe('GET /api/stats', () => {
it('agrega sessões dos últimos 30 dias por omissão', async () => {
const res = await request(app).get('/api/stats')
expect(res.status).toBe(200)
expect(res.body.days).toBe(30)
expect(res.body.sessions).toBe(3) // s-antiga (400 dias) fica fora
expect(res.body.by_source).toEqual({ claude: 2, opencode: 1 })
expect(res.body.by_outcome).toEqual({ completed: 2, error: 1 })
})
it('devolve by_day ordenado com contagens por dia', async () => {
const res = await request(app).get('/api/stats')
expect(res.body.by_day.length).toBeGreaterThanOrEqual(1)
const total = res.body.by_day.reduce((s: number, d: { sessions: number }) => s + d.sessions, 0)
expect(total).toBe(3)
for (const d of res.body.by_day) {
expect(d.day).toMatch(/^\d{4}-\d{2}-\d{2}$/)
}
})
it('devolve tool_top e skill_top a partir dos arrays JSON', async () => {
const res = await request(app).get('/api/stats')
const bash = res.body.tool_top.find((t: { tool: string }) => t.tool === 'Bash')
expect(bash).toBeDefined()
expect(bash.count).toBe(3)
expect(res.body.skill_top[0]).toEqual({ skill: 'wp-dev', count: 1 })
})
it('agrupa padrões por tipo (prefixo antes de ":")', async () => {
const res = await request(app).get('/api/stats')
const types = Object.fromEntries(
res.body.patterns_by_type.map((p: { pattern_type: string; count: number }) => [p.pattern_type, p.count]),
)
expect(types.skill_error_rate).toBe(2)
expect(types.abandoned_sessions).toBe(1)
})
it('devolve última indexação e última semana de padrões', async () => {
const res = await request(app).get('/api/stats')
expect(res.body.last_indexed_at).toBeTruthy()
expect(res.body.latest_pattern_week).toBe('2026-W27')
expect(res.body.avg_duration_sec).toBe(60)
})
it('gate_blocks é null sem tabela gate_events e conta blocks quando existe', async () => {
const before = await request(app).get('/api/stats')
expect(before.body.gate_blocks).toBeNull()
db.rawDb().exec(`
CREATE TABLE gate_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ts TEXT NOT NULL, hook TEXT NOT NULL, gate TEXT,
session_id TEXT, project_slug TEXT,
decision TEXT NOT NULL, reason TEXT, target TEXT, raw TEXT
);
`)
db.rawDb().prepare("INSERT INTO gate_events (ts, hook, decision) VALUES (?, 'stale-identifier-gate', 'block')")
.run(new Date().toISOString())
db.rawDb().prepare("INSERT INTO gate_events (ts, hook, decision) VALUES (?, 'socrates-truth-gate', 'warn')")
.run(new Date().toISOString())
const after = await request(app).get('/api/stats')
expect(after.body.gate_blocks).toBe(1)
})
it('aceita days custom e rejeita days inválido', async () => {
const ok = await request(app).get('/api/stats').query({ days: 2 })
expect(ok.status).toBe(200)
expect(ok.body.days).toBe(2)
expect(ok.body.sessions).toBe(2) // s3 (3 dias) fica fora
const bad = await request(app).get('/api/stats').query({ days: 'abc' })
expect(bad.status).toBe(400)
})
it('responde também em /api/sessions/stats', async () => {
const res = await request(app).get('/api/sessions/stats')
expect(res.status).toBe(200)
expect(res.body.sessions).toBe(3)
})
})
})
+5
View File
@@ -8,6 +8,11 @@ import type { SessionMeta } from '../types/session.js'
function sampleMeta(overrides: Partial<SessionMeta> = {}): SessionMeta { function sampleMeta(overrides: Partial<SessionMeta> = {}): SessionMeta {
return { return {
session_id: 's1', session_id: 's1',
source: 'claude',
model: null,
input_tokens: null,
output_tokens: null,
estimated_cost: null,
project_path: '/tmp/project', project_path: '/tmp/project',
project_slug: 'project', project_slug: 'project',
jsonl_path: '/tmp/project/s1.jsonl', jsonl_path: '/tmp/project/s1.jsonl',
+5
View File
@@ -9,6 +9,11 @@ import type { SessionMeta } from '../types/session.js'
function meta(overrides: Partial<SessionMeta>): SessionMeta { function meta(overrides: Partial<SessionMeta>): SessionMeta {
return { return {
session_id: 's-' + Math.random().toString(36).slice(2, 10), session_id: 's-' + Math.random().toString(36).slice(2, 10),
source: 'claude',
model: null,
input_tokens: null,
output_tokens: null,
estimated_cost: null,
project_path: '/tmp/project', project_path: '/tmp/project',
project_slug: 'project', project_slug: 'project',
jsonl_path: '/tmp/' + Math.random().toString(36).slice(2) + '.jsonl', jsonl_path: '/tmp/' + Math.random().toString(36).slice(2) + '.jsonl',
+5
View File
@@ -15,9 +15,11 @@ import type { SessionMeta } from '../types/session.js'
function meta(overrides: Partial<SessionMeta> = {}): SessionMeta { function meta(overrides: Partial<SessionMeta> = {}): SessionMeta {
return { return {
session_id: 's1', session_id: 's1',
source: 'claude',
project_path: '/tmp/p', project_path: '/tmp/p',
project_slug: 'p', project_slug: 'p',
jsonl_path: '/tmp/p/s1.jsonl', jsonl_path: '/tmp/p/s1.jsonl',
model: null,
started_at: new Date().toISOString(), started_at: new Date().toISOString(),
ended_at: null, ended_at: null,
duration_sec: 60, duration_sec: 60,
@@ -25,6 +27,9 @@ function meta(overrides: Partial<SessionMeta> = {}): SessionMeta {
user_messages: 2, user_messages: 2,
assistant_msgs: 5, assistant_msgs: 5,
tool_calls: 3, tool_calls: 3,
input_tokens: null,
output_tokens: null,
estimated_cost: null,
first_prompt: 'teste', first_prompt: 'teste',
tools_used: ['Bash'], tools_used: ['Bash'],
skills_invoked: [], skills_invoked: [],
+8 -3
View File
@@ -2,9 +2,11 @@ export type SessionOutcome = 'completed' | 'interrupted' | 'error' | 'unknown'
export interface SessionMeta { export interface SessionMeta {
session_id: string session_id: string
project_path: string source: 'claude' | 'hermes' | 'opencode'
project_slug: string project_path: string | null
jsonl_path: string project_slug: string | null
jsonl_path: string | null
model: string | null
started_at: string started_at: string
ended_at: string | null ended_at: string | null
duration_sec: number | null duration_sec: number | null
@@ -12,6 +14,9 @@ export interface SessionMeta {
user_messages: number user_messages: number
assistant_msgs: number assistant_msgs: number
tool_calls: number tool_calls: number
input_tokens: number | null
output_tokens: number | null
estimated_cost: number | null
first_prompt: string | null first_prompt: string | null
tools_used: string[] tools_used: string[]
skills_invoked: string[] skills_invoked: string[]
+8
View File
@@ -0,0 +1,8 @@
{
"task_id": 2323,
"project_id": 65,
"project_name": "DashDescomplicar",
"rel_type": "project_documentation",
"comment": "Documentação operacional fase C completa: AGENTS.md (When/How), STATUS.md (PDCA), okf/ (10 relacionamentos), coerência validada",
"updated_at": "2026-06-30"
}
View File
+10
View File
@@ -0,0 +1,10 @@
---
type: Brainstorm
title: 'Brainstorm — DashDescomplicar'
timestamp: 2026-06-30
layer: wiki
---
# BRAINSTORM — DashDescomplicar
Estrutura padrão aplicada.
+262
View File
@@ -0,0 +1,262 @@
---
layer: wiki
type: Document
title: "PLAN — DashDescomplicar"
description: >-
Plano operacional do DashDescomplicar v2.7.3. Roadmap de manutenção, deploys e expansões.
Ciclo PDCA: versão atual em produção, próximas iterações planejadas.
tags: [plan, dashboard, operacional, pdca, roadmap]
timestamp: 2026-06-30T00:00:00+01:00
---
# PLAN — DashDescomplicar
**Versão Atual:** 2.7.3 · **Data:** 2026-06-30 · **Status:** 🟢 Operacional
---
## Visão Geral
Dashboard interno de monitorização operacional (React 19 + Express + TypeScript) com 8 painéis ativos. Corre em EasyPanel Swarm (`easy.descomplicar.pt`) com MySQL no CWP. Ciclo de releases: manutenção mensal + features trimestrais.
---
## Roadmap Operacional (Q3-Q4 2026)
### Q3 2026 — Consolidação
| Sprint | Objetivo | Proprietário | Estado |
|--------|----------|-------------|--------|
| Q3-S1 (Jul) | Audit segurança completo + certificado de revisão | Claude Code | 📋 Planeado |
| Q3-S2 (Ago) | Painel Cron Dev (visibilidade de agentes cron) | Claude Code | 📋 Planeado |
| Q3-S3 (Set) | Reintrodução progressiva Paperclip (renegociação) | Claude Code | 📋 Planeado |
| Q3-S4 (Out) | Sincronização multi-máquina (HP + desktop) | Claude Code | 📋 Planeado |
### Q4 2026 — Expansão Estratégica
| Sprint | Objetivo | Proprietário | Estado |
|--------|----------|-------------|--------|
| Q4-S1 (Nov) | Integração Wiki Descomplicar (leitura de artigos) | Claude Code | 📋 Backlog |
| Q4-S2 (Dez) | Painel Inteligência Stack — melhorias | Claude Code | 📋 Backlog |
---
## Ciclos de Manutenção
### Semanal (2ªf)
- ✅ Verificar saúde do serviço: `curl -sf https://dash.descomplicar.pt/api/dashboard`
- ✅ Revisar logs de erro no EasyPanel
- ✅ Sincronização manual de sessões se necessário: `rsync -av ~/IJFW/sessions/ easy:/var/dash-sessions/`
### Mensal (último Fri)
- ✅ Audit de dependências: `npm audit`
- ✅ Atualizar ficheiro STATUS.md com métricas de uptime
- ✅ Revisar tickets de sugestões e bugs abertos
### Trimestral (fim de trimestre)
- ✅ Revisão de segurança (OWASP top 10)
- ✅ Testes de carga/stress (Load testing)
- ✅ Atualização de documentação (AGENTS.md, SPEC.md, CHANGELOG.md)
- ✅ Release notes e bump de versão
---
## Processo de Deploy
### Pré-requisitos
1. Branch `main` deve estar com todos os testes a passar: `npm test`
2. Lint sem erros: `npm run lint`
3. Build local validado: `npm run build`
4. STATUS.md actualizado com versão nova
### Passos do Deploy (SSH directo no EasyPanel)
```bash
# 1. SSH para o servidor
ssh root@easy.descomplicar.pt
# 2. Navegar para a pasta do projeto
cd /etc/easypanel/projects/descomplicar/dashboard_descomplicar/code
# 3. Pull da branch main
git pull origin main
# 4. Build da imagem Docker
docker build -t easypanel/descomplicar/dashboard_descomplicar:latest .
# 5. Forçar reinicialização do serviço
docker service update --force descomplicar_dashboard_descomplicar
# 6. Verificação pós-deploy
curl -sf https://dash.descomplicar.pt/api/dashboard && echo "✅ Deploy OK" || echo "❌ Deploy FALHOU"
```
**Tempo estimado:** 3-5 minutos (pull + build + restart)
### Rollback (se necessário)
```bash
# Voltar para versão anterior
git revert HEAD
git push origin main
# Repetir deploy
```
---
## Estrutura de Features
### Painéis Ativos (v2.7.3)
| ID | Painel | Rota | Ficheiros | Status |
|----|----|------|-----------|--------|
| 1 | Dashboard | `/` | `pages/Monitor.tsx` | 🟢 Activo |
| 2 | Monitor | `/monitor` | `pages/Monitor.tsx` | 🟢 Activo |
| 3 | Financeiro | `/financial` | `pages/Financial.tsx` | 🟢 Activo |
| 4 | MCPs | `/mcps` | `pages/McpMonitor.tsx` | 🟢 Activo |
| 5 | n8n | `/n8n` | `pages/N8nMonitor.tsx` | 🟢 Activo |
| 6 | Paperclip | `/paperclip` | `pages/Paperclip.tsx` | 🟡 Suspenso |
| 7 | IA/Claude | `/ai` | `pages/AiOverview.tsx` | 🟢 Activo |
| 8 | Operações | `/operations` | `pages/Operations.tsx` | 🟢 Activo |
| 9 | Sessions (Observabilidade) | `/sessions` | `pages/Sessions.tsx` + `pages/SessionDetail.tsx` | 🟢 Activo |
### Painéis Planeados
- **Cron Dev** (Q3 2026) — visibilidade de agentes systemd-user cron
- **Wiki Integration** (Q4 2026) — busca e leitura de artigos do Hub
- **Inteligência Stack** (Q4 2026) — melhorias e recomendações de stack
---
## Dependências Críticas
### Infraestrutura
- **EasyPanel:** `easy.descomplicar.pt` (5.9.90.70) — serviço `descomplicar_dashboard_descomplicar`
- **MySQL:** `176.9.3.158` (CWP) — DB `desscomplicar`, tabelas `tbl_eal_*`
- **MCP Gateway:** `gateway.descomplicar.pt` — estado de 39 MCPs
- **Desk CRM:** MySQL interno — tarefas, tickets, projecto
### Serviços Externos
- **n8n:** workflows automação
- **Moloni API:** dados financeiros
- **Hetzner Cloud API:** métricas servidores VPS
- **IJFW:** sessões Claude Code (SQLite local)
- **SSH (CWP + EasyPanel):** coleta de métricas
---
## Processo de Contribuição
### 1. Local Development
```bash
cd /media/ealmeida/Dados/Dev/DashDescomplicar
npm install
npm run dev # Vite + Express em paralelo
npm run test # Vitest
npm run lint # ESLint
```
### 2. Branch Workflow
```bash
git checkout -b feature/meu-painel
# ... fazer changes ...
npm run build # Validar build
npm run lint # Lint
npm test # Testes
git add .
git commit -m "feat: novo painel..."
git push origin feature/meu-painel
# Criar PR no Gitea
```
### 3. Code Review
- [ ] Build passa
- [ ] Lint passa
- [ ] Testes adicionados/passam
- [ ] Documentação actualizada
- [ ] Sem hardcoded secrets
- [ ] Validação Zod em todas as rotas novas
### 4. Merge & Deploy
Após aprovação:
```bash
git merge feature/meu-painel
git tag v2.7.4 # Nova versão
npm run build
npm start # Local test
# Seguir "Processo de Deploy" acima
```
---
## Troubleshooting Rápido
### Serviço não responde
```bash
# 1. SSH no EasyPanel
ssh root@easy.descomplicar.pt
# 2. Verificar logs
docker logs descomplicar_dashboard_descomplicar
# 3. Reiniciar serviço
docker service update --force descomplicar_dashboard_descomplicar
```
### Build local falha
```bash
# Limpar cache e reinstalar
rm -rf node_modules package-lock.json
npm install
npm run build
```
### DB não responde
```bash
# Verificar conectividade MySQL
mysql -h 176.9.3.158 -u [user] -p [pass] desscomplicar
# Se falhar, contactar administrador de infraestrutura
```
---
## Métricas de Saúde
Monitorar semanalmente:
- **Uptime:** > 99.5% (target SLA)
- **Response time:** < 500ms (p95)
- **Error rate:** < 0.5% (5xx errors)
- **DB connections:** < 80% do pool máximo
- **Disk space:** > 10% livre em EasyPanel
- **Session sync:** < 5min de lag
Alertas automáticos configurados em `monitor-alerts` (Desk #2323 com regra DO monitor).
---
## Contactos & Escalação
| Cenário | Contacto | Nível |
|---------|----------|-------|
| Bug/Feature Request | Comentário em Desk #65 | Prioridade normal |
| Incidente crítico (site down) | Usar CodeRED: `curl -X POST https://codered.descomplicar.pt/v1/trigger/RED-ZMT3FCU8` | P0 |
| Questão técnica | IRC com Claude Code (`DocFaseA-DashDescomplicar`) | On-demand |
| Deploy urgente | SSH directo + pós-deploy verification | On-demand |
---
**Próxima revisão de plano:** 2026-09-30 (fim Q3)
@@ -1,15 +1,18 @@
--- ---
title: PLAN Conductor — Expansão DashDescomplicar title: PLAN Conductor — Expansão DashDescomplicar
description: >-
Spec: `docs/SPECS/SPEC-dashboard-expansion-q2-2026.md`
date: 2026-04-06 date: 2026-04-06
type: plan type: plan
status: active status: active
spec: SPEC-dashboard-expansion-q2-2026.md spec: SPEC-dashboard-expansion-q2-2026.md
tags: [conductor, plan, dashboard, parallel] tags: [conductor, plan, dashboard, parallel]
layer: wiki
--- ---
# Plano Conductor — Expansão DashDescomplicar # Plano Conductor — Expansão DashDescomplicar
**Spec:** `docs/SPEC-dashboard-expansion-q2-2026.md` **Spec:** `docs/SPECS/SPEC-dashboard-expansion-q2-2026.md`
**Projecto:** `/media/ealmeida/Dados/Dev/DashDescomplicar/` **Projecto:** `/media/ealmeida/Dados/Dev/DashDescomplicar/`
**Método:** Conductor parallel sprints — agentes independentes por domínio **Método:** Conductor parallel sprints — agentes independentes por domínio
@@ -62,7 +65,7 @@ Cada painel (backend + frontend) é **independente** — ficheiros distintos, se
Estás a trabalhar no projecto DashDescomplicar (/media/ealmeida/Dados/Dev/DashDescomplicar/). Estás a trabalhar no projecto DashDescomplicar (/media/ealmeida/Dados/Dev/DashDescomplicar/).
Stack: React 19 + Vite + Tailwind CSS 4 + Express 4 + TypeScript. Stack: React 19 + Vite + Tailwind CSS 4 + Express 4 + TypeScript.
Lê a spec completa em docs/SPEC-dashboard-expansion-q2-2026.md, secção 4.1 (Painel MCPs). Lê a spec completa em docs/SPECS/SPEC-dashboard-expansion-q2-2026.md, secção 4.1 (Painel MCPs).
Cria 3 ficheiros: Cria 3 ficheiros:
@@ -117,7 +120,7 @@ Não alteres nenhum ficheiro existente. Apenas cria os 3 novos.
Estás a trabalhar no projecto DashDescomplicar (/media/ealmeida/Dados/Dev/DashDescomplicar/). Estás a trabalhar no projecto DashDescomplicar (/media/ealmeida/Dados/Dev/DashDescomplicar/).
Stack: React 19 + Vite + Tailwind CSS 4 + Express 4 + TypeScript. Stack: React 19 + Vite + Tailwind CSS 4 + Express 4 + TypeScript.
Lê a spec completa em docs/SPEC-dashboard-expansion-q2-2026.md, secção 4.2 (Painel n8n). Lê a spec completa em docs/SPECS/SPEC-dashboard-expansion-q2-2026.md, secção 4.2 (Painel n8n).
Cria 3 ficheiros: Cria 3 ficheiros:
@@ -159,7 +162,7 @@ Não alteres nenhum ficheiro existente. Apenas cria os 3 novos.
Estás a trabalhar no projecto DashDescomplicar (/media/ealmeida/Dados/Dev/DashDescomplicar/). Estás a trabalhar no projecto DashDescomplicar (/media/ealmeida/Dados/Dev/DashDescomplicar/).
Stack: React 19 + Vite + Tailwind CSS 4 + Express 4 + TypeScript. Stack: React 19 + Vite + Tailwind CSS 4 + Express 4 + TypeScript.
Lê a spec completa em docs/SPEC-dashboard-expansion-q2-2026.md, secção 4.3 (Painel Paperclip). Lê a spec completa em docs/SPECS/SPEC-dashboard-expansion-q2-2026.md, secção 4.3 (Painel Paperclip).
O pacote `pg` já foi instalado (npm install pg @types/pg). O pacote `pg` já foi instalado (npm install pg @types/pg).
@@ -211,7 +214,7 @@ Não alteres nenhum ficheiro existente. Apenas cria os 4 novos.
Estás a trabalhar no projecto DashDescomplicar (/media/ealmeida/Dados/Dev/DashDescomplicar/). Estás a trabalhar no projecto DashDescomplicar (/media/ealmeida/Dados/Dev/DashDescomplicar/).
Stack: React 19 + Vite + Tailwind CSS 4 + Express 4 + TypeScript. Stack: React 19 + Vite + Tailwind CSS 4 + Express 4 + TypeScript.
Lê a spec completa em docs/SPEC-dashboard-expansion-q2-2026.md, secção 4.4 (Painel Claude Code / IA). Lê a spec completa em docs/SPECS/SPEC-dashboard-expansion-q2-2026.md, secção 4.4 (Painel Claude Code / IA).
Cria 3 ficheiros: Cria 3 ficheiros:
@@ -261,7 +264,7 @@ Não alteres nenhum ficheiro existente. Apenas cria os 3 novos.
Estás a trabalhar no projecto DashDescomplicar (/media/ealmeida/Dados/Dev/DashDescomplicar/). Estás a trabalhar no projecto DashDescomplicar (/media/ealmeida/Dados/Dev/DashDescomplicar/).
Stack: React 19 + Vite + Tailwind CSS 4 + Express 4 + TypeScript. Stack: React 19 + Vite + Tailwind CSS 4 + Express 4 + TypeScript.
Lê a spec em docs/SPEC-dashboard-expansion-q2-2026.md, secção 4.5 (Painel Operações). Lê a spec em docs/SPECS/SPEC-dashboard-expansion-q2-2026.md, secção 4.5 (Painel Operações).
A BD MySQL (Desk CRM) já tem conexão configurada em api/db.ts (export default pool mysql2/promise). A BD MySQL (Desk CRM) já tem conexão configurada em api/db.ts (export default pool mysql2/promise).
+10
View File
@@ -0,0 +1,10 @@
---
type: Plan
title: 'PLAN — DashDescomplicar'
timestamp: 2026-06-30
layer: wiki
---
# PLAN — DashDescomplicar
1. Estruturação\n2. Validação\n3. Manutenção
View File
+10
View File
@@ -0,0 +1,10 @@
---
type: Brief
title: 'Briefing — DashDescomplicar'
timestamp: 2026-06-30
layer: wiki
---
# BRIEF — DashDescomplicar
Projecto DashDescomplicar do ecossistema Descomplicar.
@@ -1,3 +1,11 @@
---
type: Specification
title: Spec
description: >-
Especificação do projecto DashDescomplicar, agregando os projectos activos — nomeadamente o painel Observabilidade (replay de sessões Claude Code com indexer, SQLite, API e UI timeline).
timestamp: 2026-04-27T10:15:35.733200+00:00
layer: wiki
---
<!-- APPROVED: 2026-04-23 by ealmeida --> <!-- APPROVED: 2026-04-23 by ealmeida -->
# SPEC — DashDescomplicar # SPEC — DashDescomplicar
@@ -1,10 +1,13 @@
--- ---
title: SPEC — Expansão DashDescomplicar Q2 2026 title: SPEC — Expansão DashDescomplicar Q2 2026
description: >-
O DashDescomplicar é o painel de gestão interno da Descomplicar®. Stack: React 19 + Vite + Tailwind CSS 4 + Express 4 + …
date: 2026-04-06 date: 2026-04-06
type: spec type: spec
status: active status: active
desk_project: 65 desk_project: 65
tags: [dashboard, expansion, mcps, paperclip, n8n, claude-code] tags: [dashboard, expansion, mcps, paperclip, n8n, claude-code]
layer: wiki
--- ---
# SPEC — Expansão DashDescomplicar Q2 2026 # SPEC — Expansão DashDescomplicar Q2 2026
+11
View File
@@ -0,0 +1,11 @@
---
type: Spec
title: 'SPEC — DashDescomplicar'
status: draft
timestamp: 2026-06-30
layer: wiki
---
# SPEC — DashDescomplicar
Projecto em estruturação.
+10
View File
@@ -0,0 +1,10 @@
---
type: Note
title: 'Pesquisa — DashDescomplicar'
timestamp: 2026-06-30
layer: wiki
---
# Pesquisa
Documentar pesquisa aqui.
+39
View File
@@ -0,0 +1,39 @@
---
layer: wiki
type: Index
title: "DashDescomplicar — Índice"
description: >-
Índice de navegação do DashDescomplicar. Dashboard operacional interno da Descomplicar® em dash.descomplicar.pt.
tags: [index, dashboard, aplicacao]
timestamp: 2026-06-29T23:00:00+01:00
---
# DashDescomplicar
## Documentação
- [[AGENTS]] — contexto 5W2H para agentes IA
- [[CLAUDE]] — instruções operacionais para Claude Code
- [[STATUS]] — estado PDCA e bloqueios
- [[README]] — referência técnica completa
- [[CHANGELOG]] — histórico de versões
- [[MEMORY]] — memórias operacionais acumuladas
- [[SYS-DashDescomplicar]] — descrição do sistema e painéis
## Especificações e Planos
- [[docs/SPECS/SPEC-dashboard-expansion-q2-2026]] — spec expansão Q2 2026
- [[docs/PLANS/PLAN-conductor-dashboard-expansion]] — plano de expansão (Conductor)
- [[AUDIT-REPORT]] — relatório de auditoria de segurança
- [[SPEC]] — especificação activa (Observabilidade)
## Recursos Externos
- [dash.descomplicar.pt](https://dash.descomplicar.pt) — produção
- [git.descomplicar.pt/ealmeida/DashDescomplicar](https://git.descomplicar.pt/ealmeida/DashDescomplicar) — repositório
- [easy.descomplicar.pt](https://easy.descomplicar.pt) — EasyPanel (deploy)
- [Desk Projecto #65](https://desk.descomplicar.pt) — gestão de tarefas
## Dev Local
- `/media/ealmeida/Dados/Dev/DashDescomplicar` — código fonte
- `/media/ealmeida/Dados/Hub/04-Stack/02.05-Aplicações/DashDescomplicar/` — esta pasta Hub
+23
View File
@@ -0,0 +1,23 @@
---
layer: wiki
type: Changelog
title: "Log — DashDescomplicar"
description: >-
Histórico append-only de operações e eventos em 02.05-Aplicações/DashDescomplicar.
timestamp: 2026-06-30T00:00:00+01:00
tags: [log, okf, dashdescomplicar]
status: active
---
# Log — DashDescomplicar
**Newest first. Append-only. Formato: `## YYYY-MM-DD tipo | Descrição`**
---
## 2026-06-30 okf | log.md criado
- Ficheiro criado conforme modelo OKF Karpathy (XDP-PRJ-001)
---
*Última alteração: 2026-06-30*
+27
View File
@@ -0,0 +1,27 @@
#!/bin/bash
# Gemini CLI Health Check - Integração com Observabilidade
# Este script verifica o estado dos servidores MCP e regista métricas.
LOG_FILE="/media/ealmeida/Dados/Hub/05-Projectos/Observabilidade/logs/gemini/health.log"
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
echo "[$TIMESTAMP] A iniciar verificação de saúde do Gemini CLI..." >> "$LOG_FILE"
# Verifica se o gemini está no PATH e responde
if ! command -v gemini &> /dev/null; then
echo "[$TIMESTAMP] ERRO: Gemini CLI não encontrado no PATH." >> "$LOG_FILE"
exit 1
fi
# Lista servidores MCP e extrai estado
MCP_STATUS=$(gemini mcp list)
# Procura por servidores desconectados (marcados com ✗ no gemini mcp list)
if echo "$MCP_STATUS" | grep -q "✗"; then
echo "[$TIMESTAMP] ALERTA: Um ou mais servidores MCP estão desconectados!" >> "$LOG_FILE"
echo "$MCP_STATUS" | grep "✗" >> "$LOG_FILE"
else
echo "[$TIMESTAMP] SUCESSO: Todos os servidores MCP estão conectados." >> "$LOG_FILE"
fi
echo "------------------------------------------------" >> "$LOG_FILE"
+30
View File
@@ -0,0 +1,30 @@
#!/usr/bin/env python3
import sys
import json
import os
import time
def pre_tool_hook():
try:
# Lê o input do Gemini
hook_input = json.load(sys.stdin)
session_id = hook_input.get("session_id", "unknown")
# Cria um ficheiro temporário para guardar o tempo de início desta sessão
# Nota: O Gemini CLI executa ferramentas de forma sequencial ou paralela,
# mas os hooks AfterTool recebem o contexto necessário.
temp_dir = os.path.expanduser("~/.gemini/tmp/telemetry")
os.makedirs(temp_dir, exist_ok=True)
# Guardamos o tempo de início num ficheiro nomeado pela sessão
# Para maior precisão em chamadas paralelas, o ideal seria um tool_call_id,
# mas o session_id é a base comum.
with open(os.path.join(temp_dir, f"{session_id}.start"), "w") as f:
f.write(str(time.time()))
print(json.dumps({}))
except Exception:
print(json.dumps({}))
if __name__ == "__main__":
pre_tool_hook()
+63
View File
@@ -0,0 +1,63 @@
#!/usr/bin/env python3
import sys
import json
import os
from datetime import datetime
def log_telemetry():
try:
# Lê o JSON do stdin enviado pelo Gemini CLI
hook_input = json.load(sys.stdin)
# Extrai dados básicos
session_id = hook_input.get("session_id", "unknown")
tool_name = hook_input.get("tool_name", "unknown")
timestamp = hook_input.get("timestamp", datetime.now().isoformat())
# Determina o sucesso/erro a partir da resposta da ferramenta
tool_response = hook_input.get("tool_response", {})
error = tool_response.get("error", "")
# No formato do Gemini, o sucesso pode ser inferido pela ausência de erro
# ou por flags específicas em certas ferramentas.
success = "true" if not error else "false"
# Prepara o log no formato compatível com o Claude (mas com engine: gemini)
# O ficheiro de destino é o mesmo usado pelo Claude para métricas unificadas
log_path = os.path.expanduser("~/.claude-work/tool-invocations.jsonl")
# Tenta calcular a duração
duration = 0
start_file = os.path.expanduser(f"~/.gemini/tmp/telemetry/{session_id}.start")
if os.path.exists(start_file):
with open(start_file, "r") as f:
start_time = float(f.read())
duration = round(time.time() - start_time, 2)
log_entry = {
"ts": timestamp,
"session": session_id,
"tool_name": tool_name,
"success": success == "true",
"error": error,
"duration": duration, # Nova métrica de tempo
"engine": "gemini"
}
# Garante que a pasta existe
os.makedirs(os.path.dirname(log_path), exist_ok=True)
# Escreve no log central
with open(log_path, "a") as f:
f.write(json.dumps(log_entry) + "\n")
# O Gemini exige um JSON válido no stdout para hooks de AfterTool
print(json.dumps({}))
except Exception as e:
# Erros vão para o stderr para não quebrar o CLI
sys.stderr.write(f"Erro no Gemini Telemetry Hook: {str(e)}\n")
print(json.dumps({}))
if __name__ == "__main__":
log_telemetry()
+182
View File
@@ -0,0 +1,182 @@
---
type: Validation
layer: wiki
tags: [stack, dashdescomplicar, fase-c, 5w2h, pdca, validacao]
title: "Validação Fase C — DashDescomplicar"
---
# Validação Fase C — DashDescomplicar (2026-06-30)
## Checklist de Conclusão Fase C
### 1. ✅ AGENTS.md — When/How Completo
**Ficheiro:** `/media/ealmeida/Dados/Hub/04-Stack/02.05-Aplicações/DashDescomplicar/AGENTS.md`
- ✅ **Claude Code (cc)** — 5W2H completo
- **What:** Desenvolvimento, DevOps, features DashDescomplicar
- **Why:** Fonte central de código, deploy, PDCA operacional
- **Where:** Dev/DashDescomplicar + EasyPanel
- **Who:** Emanuel + Claude Code
- **When:** Semanal (Seg), Mensal (último Sex), Trimestral, Contínuo
- **How:** Referências a docs/PLANS/DashDescomplicar-PLAN.md + PROC-Deploy-EasyPanel + QR-Desk-Subtarefas
- ✅ **Observabilidade (Sistema)** — 5W2H completo
- **What:** Coleta e agregação de métricas operacionais
- **Why:** Observabilidade de infraestrutura e aplicação
- **Where:** okf/entidades/Metrics.md + logs/performance-metrics.json
- **Who:** Observabilidade (Sistema #2059)
- **When:** Contínuo (eventos em tempo real)
- **How:** Referências a Observabilidade SPEC/PLAN + okf/entidades/Metrics.md
### 2. ✅ STATUS.md — PDCA Ligado a Recursos
**Ficheiro:** `/media/ealmeida/Dados/Hub/04-Stack/02.05-Aplicações/DashDescomplicar/STATUS.md`
#### 🟦 PLAN
- ✅ Referência: `docs/PLANS/DashDescomplicar-PLAN.md` (autoritário)
- ✅ Roadmap Q3-Q4 mapeado
- ✅ Ciclos de manutenção ligados (Semanal/Mensal/Trimestral)
#### 🟩 DO
- ✅ Localização código: Gitea + Dev local + Deploy EasyPanel
- ✅ Dependências críticas listadas (MCP Gateway, Desk CRM, n8n, Moloni, SSH, IJFW)
- ✅ Processo de deploy documentado (npm test → build → deploy)
- ✅ 9 features activas mapeadas
#### 🟨 CHECK
- ✅ Observabilidade OKF ligada: `okf/entidades/Metrics.md`
- ✅ Integração: `okf/relacionamentos/CSRIntegration-integra-Metrics.md`
- ✅ Métricas de saúde definidas (uptime, response time, error rate, DB connections, Disk, Session sync)
- ✅ Health endpoints listados (`/api/dashboard`, `/api/metrics`)
#### 🟧 ACT
- ✅ SelfImprovement ligado (Desk #2066)
- ✅ Processo PDCA definido (Recolher → Análise → Decisão → Implementar → Registar)
- ✅ Escalação de incidentes (P0 CodeRED, P1, P2)
- ✅ Links a SPEC/PLAN autoritários de SelfImprovement
### 3. ✅ okf/relacionamentos/ — 10 Arestas (Fase C +3)
**Localização:** `/media/ealmeida/Dados/Hub/04-Stack/02.05-Aplicações/DashDescomplicar/okf/relacionamentos/`
#### Fase B (Original — 7 arestas)
1. ✅ `DashboardEngine-exibe-Paineis.md` — Motor renderiza painéis
2. ✅ `DashboardEngine-integra-CSRIntegration.md` — Integração CSR
3. ✅ `CSRIntegration-integra-Metrics.md` — Telemetria
4. ✅ `Paineis-consomem-DeskCRM.md` — Consumo Desk
5. ✅ `Paineis-consomem-Moloni.md` — Consumo Moloni
6. ✅ `Paineis-consomem-SSH.md` — Consumo SSH
7. ✅ `Paineis-consomem-MCPGateway.md` — Consumo MCP Gateway
#### Fase C (Novo — 3 arestas cruzadas)
8. ✅ `DashboardEngine-integra-Observabilidade.md` — Sistema Observabilidade (#2059)
9. ✅ `DashboardEngine-integra-SelfImprovement.md` — Sistema SelfImprovement (#2066)
10. ✅ `Metrics-alimenta-PainelMonitor.md` — Métricas → Monitor (observabilidade)
**Padrão:** Cada relação é um ficheiro atómico com frontmatter `type: Relationship`
### 4. ✅ okf/index.md — Navegação Atualizada
**Ficheiro:** `/media/ealmeida/Dados/Hub/04-Stack/02.05-Aplicações/DashDescomplicar/okf/index.md`
- ✅ Entidades Principais (12 totais) — referenciadas com `[[` wiki links `]]`
- ✅ Relacionamentos — 4 secções:
1. Estrutura de Painéis (1 aresta)
2. Integrações Funcionais (4 arestas)
3. Consumo de Dados & Observabilidade (5 arestas)
- ✅ Taxonomia — exibe, integra-com, alimenta, usa, consome
- ✅ Regras Karpathy — 1 conceito = 1 ficheiro, atómico, navegável, bidirecional
- ✅ Estrutura de ficheiros atualizada (diagrama ASCII)
- ✅ Critérios de conclusão Fase C
### 5. ✅ Coerência do Grafo — Sem Órfãos
**Validação de arestas:** Cada entidade tem ≥1 entrada + ≥1 saída
| Entidade | Entrada | Saída | Status |
|----------|---------|-------|--------|
| Dashboard-Engine | exibe (painéis), integra (CSR, Obs, SelfImp) | exibe painéis, integra | ✅ |
| Painel-Monitor | exibe (Dashboard-Engine), alimenta (Metrics) | consome (SSH, MCP Gateway) | ✅ |
| Painel-Operacoes | exibe (Dashboard-Engine) | consome (Desk CRM) | ✅ |
| Painel-Financeiro | exibe (Dashboard-Engine) | consome (Moloni, Desk) | ✅ |
| Painel-MCPs | exibe (Dashboard-Engine) | consome (MCP Gateway) | ✅ |
| Painel-N8n | exibe (Dashboard-Engine) | — (interno) | ✅ |
| Painel-Paperclip | exibe (Dashboard-Engine) | — (suspenso) | ✅ |
| Painel-IA | exibe (Dashboard-Engine) | — (interno) | ✅ |
| Painel-Dashboard | exibe (Dashboard-Engine) | — (agregador) | ✅ |
| Painel-CronDev | exibe (Dashboard-Engine) | consome (SSH, IJFW) | ✅ |
| CSR-Integration | integra (Dashboard-Engine) | integra (Metrics) | ✅ |
| Metrics | integra (CSR), alimenta (Monitor) | coleta eventos, expõe `/api/metrics` | ✅ |
**Conclusão:** Zero entidades órfãs. Grafo coerente e navegável.
### 6. ✅ Cross-Sistema — Links a SPEC/PLAN Autoritários
| Sistema | SPEC | PLAN | Desk | Integração |
|---------|------|------|------|-----------|
| **Observabilidade** | `/media/ealmeida/Dados/Hub/05-Projectos/Observabilidade/SPEC.md` | `/media/ealmeida/Dados/Hub/05-Projectos/Observabilidade/PLAN.md` | #2059 | AGENTS.md + STATUS.md (CHECK) + okf/DashboardEngine-integra-Observabilidade |
| **SelfImprovement** | `/media/ealmeida/Dados/Hub/05-Projectos/SelfImprovement/SPEC.md` | `/media/ealmeida/Dados/Hub/05-Projectos/SelfImprovement/PLAN.md` | #2066 | AGENTS.md + STATUS.md (ACT) + okf/DashboardEngine-integra-SelfImprovement |
### 7. ✅ desk.project — Mapeamento CRM
**Ficheiro:** `/media/ealmeida/Dados/Hub/04-Stack/02.05-Aplicações/DashDescomplicar/desk.project`
```json
{
"task_id": 2323,
"project_id": 65,
"project_name": "DashDescomplicar",
"rel_type": "project_documentation",
"comment": "Documentação operacional fase C completa: AGENTS.md (When/How), STATUS.md (PDCA), okf/ (10 relacionamentos), coerência validada",
"updated_at": "2026-06-30"
}
```
---
## Resumo Executivo Fase C
### Artefatos Completados
1. **AGENTS.md** — Governance de agentes com 5W2H completo
- Claude Code: semanal health checks, mensal audits, trimestral review
- Observabilidade: telemetria contínua, coleta de métricas
2. **STATUS.md** — PDCA operacional ligado a recursos
- **PLAN:** docs/PLANS/ (roadmap Q3-Q4, ciclos de manutenção)
- **DO:** Código + Deploy (Gitea, EasyPanel, MySQL)
- **CHECK:** Observabilidade (Metrics, health endpoints, SLA targets)
- **ACT:** SelfImprovement (feedback → análise → priorização → implementação)
3. **okf/relacionamentos/** — 10 arestas documentadas
- 7 originais (Fase B)
- 3 novas Fase C (Observabilidade, SelfImprovement, Metrics-Monitor)
- Cada ficheiro atómico com frontmatter correcto
4. **okf/index.md** — Navegação completa e validação
- 12 entidades, 10 relacionamentos, bidirecionalidade
- Diagrama ASCII estrutura
- Critérios Fase C
### Conformidade com Regras Fase C
✅ **When/How em AGENTS.md** — Preenchidos com PLAN/PROC/QR refs
✅ **PDCA em STATUS.md** — Cada secção (Plan/Do/Check/Act) com links para recursos
✅ **Relacionamentos cruzados** — A integra-com B, B serve A (documentado em index)
✅ **Sem órfãos** — Todas 12 entidades têm ≥1 entrada + ≥1 saída
✅ **Cross-sistema** — Links a SPEC/PLAN autoritários de Observabilidade + SelfImprovement
---
## Próximos Passos (Sugestões)
1. **M2 Milstone (Memória — cloud sync):** Aguardando Desk #2364 (bloqueado)
2. **M3 Milstone (Wiki bridge):** Integração com Hub wiki.descomplicar.pt
3. **M4 Milstone (Validação DoD):** Testes de aceitação dos critérios
---
**Validação Assinada:** 2026-06-30
**Agente:** DocFaseC-DashDescomplicar
**Fase:** C — 5W2H+PDCA Completo
+182
View File
@@ -0,0 +1,182 @@
---
type: Schema
layer: okf
title: "OKF Schema — DashDescomplicar"
description: >-
Schema canónico do Organizational Knowledge Framework (OKF) para o projecto
DashDescomplicar. Formaliza o padrão já em uso — entidades curadas +
relacionamentos narrativos em Markdown — e declara o Nível de Maturidade
actual (N1).
tags: [dashdescomplicar, okf, schema, governanca, convencoes]
timestamp: 2026-07-07
status: active
---
# OKF Schema — DashDescomplicar
**Schema canónico do projecto. Formaliza, sem alterar, o padrão já em produção em `okf/entidades/` e `okf/relacionamentos/`.**
Alinhado com o OKF global Descomplicar® (`Hub/04-Stack/02.04-Sistemas/90.Conhecimento/OKF/SCHEMA.md`) e com `QR-OKF-Project-Level.md` (Hub, v2.0) — padrão genérico de OKF de projecto. Este ficheiro documenta explicitamente uma decisão já tomada em `SPEC-OKF-Projeto-Sistema-Excelencia.md` (§3.6, §5.4): DashDescomplicar mantém-se em **N1**, com `okf/relacionamentos/*.md` como Tier 3 narrativo válido (não `relations.jsonl`).
---
## 1. Modelo em Uso
DashDescomplicar usa **2 das 3 camadas** do modelo OKF de projecto — não implementa Tier 2 (inventário automático do código) nem Tier 3 máquina-legível (`relations.jsonl`):
| Tier | Nome | Estado neste projecto | Origem |
|------|------|------------------------|--------|
| **1** | Entidades curadas | ✅ Implementado — `okf/entidades/*.md` (25 fichas) | Escrita humana |
| **2** | Inventários derivados do código | ❌ Não implementado — sem `okf/inventory/`, sem `scripts/okf-gen-inventory.mjs` | N/A |
| **3** | Grafo derivado (`relations.jsonl`) | ❌ Não implementado — substituído pelo Tier 3 narrativo (§5) | N/A |
**Tier 3 narrativo (em uso):** `okf/relacionamentos/*.md` — um ficheiro Markdown por aresta, com frontmatter + descrição prosa (e, quando aplicável, fluxo de dados/tabelas). Ver §5 para a decisão formal de não migrar para `relations.jsonl`.
---
## 2. Estrutura de Pastas (real)
```
okf/
├── SCHEMA.md ← este ficheiro
├── index.md ← catálogo navegável das 25 entidades + 21 relacionamentos
├── FASE-C-VALIDACAO.md ← registo histórico da validação Fase C
├── entidades/ ← Tier 1 — 25 fichas curadas
│ ├── Dashboard-Engine.md
│ ├── Express-API.md
│ ├── Painel-Monitor.md
│ ├── Integracao-Hetzner.md
│ └── ...
└── relacionamentos/ ← Tier 3 narrativo — 21 fichas de aresta
├── DashboardEngine-exibe-Paineis.md
├── MySQL-Pool-alimenta-Servicos.md
└── ...
```
Sem `inventory/`, sem `relations.jsonl`, sem `graph-metrics.json`, sem `scripts/okf-*.mjs` — nenhum destes é exigido a N1 (ver §6).
---
## 3. Nomenclatura
### Fichas de entidade (`okf/entidades/`)
- **PascalCase-com-hífens**, descritivo, **sem prefixo `okf-*`** — diferente do padrão WhatSMS/Pleno (`okf-svc-*`, `okf-dom-*`). Esta convenção já está consolidada nas 25 fichas existentes; não se migra retroactivamente.
- Exemplos reais: `Dashboard-Engine.md`, `Painel-Monitor.md`, `Integracao-Hetzner.md`, `MySQL-Pool.md`.
### Fichas de relacionamento (`okf/relacionamentos/`)
- Nome descreve a aresta: `<Origem>-<verbo>-<Destino>.md` (ex.: `Integracao-Hetzner-alimenta-Monitor.md`).
- Quando há múltiplos destinos ou uma relação agregada, o nome reflecte o padrão dominante (ex.: `Paineis-consomem-DeskCRM.md`).
### Wikilinks
- Referência cruzada: `[[Dashboard-Engine]]` (nome do ficheiro sem extensão), resolvido contra `okf/entidades/*.md`.
---
## 4. Frontmatter (real, validado por inspecção — não por script)
### Entidade (Tier 1)
```yaml
---
type: Entity
layer: wiki
tags: [stack, dashdescomplicar, tag2, tag3]
title: "Nome-Da-Entidade"
---
```
### Relacionamento (Tier 3 narrativo)
```yaml
---
type: Relationship
layer: wiki
tags: [stack, dashdescomplicar, relacionamento, tag3, tag4]
title: "Origem-verbo-Destino"
---
```
**Nota de convenção:** `type`/`layer` usam a casing e os valores do padrão de wiki do Hub (`type: Entity`, `layer: wiki`), não o par `type: entity`/`layer: okf` do schema WhatSMS/Pleno. Não é um erro a corrigir por este ficheiro — é a convenção já estabelecida e consistente nas 46 fichas existentes (25 entidades + 21 relacionamentos); mantém-se como está.
Frontmatter **não inclui** `category`, `status`, `desk_project` ou `timestamp` por ficha — mais leve que o schema WhatSMS/Pleno, adequado à escala N1. Estado/categoria são comunicados em prosa dentro do corpo (ex.: secção `## Estado` em `Painel-Monitor.md`).
---
## 5. Relacionamentos Narrativos vs `relations.jsonl` — Decisão
**Decisão (já tomada, `SPEC-OKF-Projeto-Sistema-Excelencia.md` §3.6/§5.4, não reaberta aqui):** DashDescomplicar **não migra** `okf/relacionamentos/*.md` para `relations.jsonl`.
**Justificação:**
- `relations.jsonl` só é **obrigatório a partir de N3** (`QR-OKF-Project-Level.md` §Níveis de Maturidade) — DashDescomplicar está em N1 (§6).
- O custo de construir e manter `scripts/okf-gen-graph.mjs` (parser + geração de arestas `derived`/`reciprocal`) não se justifica para 21 arestas geridas manualmente sem atrito.
- `okf/relacionamentos/*.md` (um ficheiro por aresta, com frontmatter + descrição narrativa, por vezes com fluxo de dados/tabelas de detalhe — ex. `MySQL-Pool-alimenta-Servicos.md`) é reconhecido formalmente pelo `QR-OKF-Project-Level.md` (§Relacionamentos narrativos vs `relations.jsonl`) como **fonte Tier 3 adicional válida**, própria para relações que beneficiam de explicação processual em vez de uma aresta plana.
- Se o projecto crescer para N3 (≥50 ficheiros de código E ≥3 serviços/domínios — ver §6), `relations.jsonl` pode ser gerado a partir destas mesmas fichas (`okf-gen-graph.mjs` pode parsear `relacionamentos/*.md` como origem `origin: narrative`, per QR) sem duplicar trabalho manual — mas essa geração **não é criada por este ficheiro**.
---
## 6. Nível de Maturidade: N1
| Nível | Critério de entrada | Requisitos | Estado DashDescomplicar |
|-------|---------------------|-------------|--------------------------|
| **N1** | Qualquer projecto com `okf/` | `entidades/` + `index.md` | ✅ Cumprido — 25 entidades + `index.md` |
| N2 | ≥50 ficheiros de código | + `inventory/` automático | ❌ Não implementado |
| N3 | ≥50 ficheiros E ≥3 serviços/domínios | + `relations.jsonl` + `SCHEMA.md` formal | ❌ Não implementado |
| N4 | Produção activa | + overlays risk/churn + `okf-feat-*` + `specs-by-entity.json` + drift-check | ❌ Não implementado |
**Justificação (N1, escala do projecto):**
- 25 entidades Tier 1 bem documentadas, 21 relacionamentos narrativos — cobertura curada completa para a superfície do projecto.
- Volume de código (`src/` + `api/`, TypeScript): **75 ficheiros `.ts`/`.tsx`** — acima do limiar literal de 50 ficheiros que define a entrada em N2 no `QR-OKF-Project-Level.md`. Isto por si só sugeriria N2.
- **Mas** o requisito real de N2 não é a contagem de ficheiros — é a existência de `okf/inventory/` **automático**, gerado por script (`scripts/okf-gen-inventory.mjs`). Nenhum dos dois existe neste repositório (confirmado: sem pasta `scripts/`, sem `okf/inventory/`, sem ficheiros `okf-gen-*`). Sem essa infra-estrutura de geração, o projecto não cumpre o requisito formal de N2, independentemente do volume de código.
- Esta avaliação está alinhada com `SPEC-OKF-Projeto-Sistema-Excelencia.md` §3.6 (mapeamento explícito: "CodeRED/DashDescomplicar = N1, mantêm-se aí por agora — escala pequena não justifica N2+ ainda") e §5.4 (Fase 4: "CodeRED/DashDescomplicar: criar SCHEMA.md mínimo documentando o padrão `relacionamentos/*.md` já em uso como válido").
- **Nota de avaliação (reportada, não decidida aqui):** o volume de código (75 ficheiros) é tecnicamente suficiente para justificar a criação de `scripts/okf-gen-inventory.mjs` e subir a N2 num ciclo futuro — mas essa subida implica trabalho de automação novo (fora do âmbito desta Fase 4, que é apenas formalização documental) e não foi implementada aqui.
---
## 7. Estrutura de Conteúdo das Fichas de Entidade (real)
As fichas em `okf/entidades/` **não seguem o formato rígido 5W2H+PDCA** (usado em WhatSMS v2/Pleno) — usam secções livres, adaptadas ao tipo de componente, tipicamente um subconjunto de:
- `## Responsabilidades` — o que o componente faz
- `## Stack` — tecnologias usadas
- `## Localizações` — paths no repo, rotas, deploy
- `## Tipos` — interfaces/tipos TypeScript relevantes (quando aplicável)
- `## APIs Consumidas` / `## Dependências` — o que consome / de que depende
- `## Fornece a` — quem consome este componente
- `## Estado` — activo/suspenso/proposto, marcos relevantes
Este formato é adequado à escala N1 (fichas de 60-100 linhas, foco técnico denso) e mantém-se sem alteração.
---
## 8. Regras de Qualidade
1. Frontmatter mínimo presente em toda a ficha (`type`, `layer`, `tags`, `title`).
2. Toda a entidade referenciada em `okf/index.md` com wikilink bidireccional (entidade ↔ relacionamentos que a tocam).
3. Toda a relação com ficha própria em `okf/relacionamentos/`, mínimo `Origem`/`Destino`/`Tipo`/`Descrição`.
4. PT-PT estrito, sem brasileirismos.
5. Primeira tag de qualquer ficha: `stack`; segunda: `dashdescomplicar`.
6. Sem Tier 2/3 automáticos a este nível — nada a regenerar; toda a manutenção é manual.
---
## 9. Manutenção
| O quê | Quem | Quando |
|-------|------|--------|
| Entidades Tier 1 (novos componentes) | Developer | Quando um novo painel/integração é criado |
| Relacionamentos narrativos | Developer | Quando uma nova aresta de dados/dependência surge |
| `okf/index.md` | Developer | Após qualquer alteração a entidades/relacionamentos |
| `SCHEMA.md` | Developer | Quando o padrão evolui ou o Nível de Maturidade muda |
---
## Referências
- OKF Global (`Hub/04-Stack/02.04-Sistemas/90.Conhecimento/OKF/SCHEMA.md`) — schema canónico, taxonomia, 5W2H+PDCA
- `QR-OKF-Project-Level.md` (Hub) — padrão genérico de OKF de projecto, v2.0
- `SPEC-OKF-Projeto-Sistema-Excelencia.md` (`Hub/04-Stack/docs/SPECS/`) — §3.6 (Níveis de Maturidade), §5.4 (Fase 4 — decisão de manter CodeRED/DashDescomplicar em N1)
- WhatSMS v2 (`okf/SCHEMA.md`) — referência viva do padrão N4 completo (3 tiers automáticos)
- CodeRED (`okf/SCHEMA.md`) — mesmo padrão de relacionamentos narrativos, formalizado em paralelo a este ficheiro
**OKF Schema DashDescomplicar | v1.0 | 2026-07-07**
**Alinhado com `Hub/04-Stack/02.04-Sistemas/90.Conhecimento/OKF/SCHEMA.md` (OKF global) e `QR-OKF-Project-Level.md` (padrão de projecto)**
+65
View File
@@ -0,0 +1,65 @@
---
type: Entity
layer: wiki
tags: [stack, dashdescomplicar, integracao, csr, cache, fetch]
title: "CSR-Integration"
---
# CSR-Integration
Camada de integração Cliente-Servidor do DashDescomplicar. Gere fetch de dados da API, cache local, reconnection logic e tratamento de erros para todos os painéis.
## Responsabilidades
- **Fetch API**: Chamadas HTTP à Express-API com tipagem TypeScript
- **Cache Local**: Estado React (`useState` + `useCallback`) para evitar refetchs desnecessários
- **Reconnection**: Lógica de retry em falhas de rede (sem exponential backoff — simplicidade)
- **Error Handling**: Erros capturados com `console.error`, estados de loading/error nos painéis
- **Refresh Manual**: Botão "Actualizar" (RefreshCw) em cada painel com animação de spinner
## Padrão de Fetch
```typescript
const [data, setData] = useState<T | null>(null)
const [loading, setLoading] = useState(true)
const [refreshing, setRefreshing] = useState(false)
const fetchData = useCallback(async () => {
setRefreshing(true)
try {
const response = await fetch('/api/<endpoint>')
if (!response.ok) throw new Error('Failed')
setData(await response.json())
} catch {
console.error('Failed to fetch <endpoint>')
} finally {
setLoading(false)
setRefreshing(false)
}
}, [])
useEffect(() => { fetchData() }, [fetchData])
```
## Estados de UI
- **Loading**: Spinner animado com ícone específico do painel
- **Error**: Console.error (sem toast — minimalista)
- **Empty**: `if (!data) return null` (protecção de render)
- **Refreshing**: Ícone RefreshCw com animação `animate-spin`
## Localizações
- **Código**: Padrão replicado em cada `src/pages/*.tsx` (não extraído para hook — simplicidade)
- **Sessions API**: `src/lib/api/sessions.ts` (único módulo de API extraído)
## Dependências
- **Express-API**: Todos os endpoints REST
- **Dashboard-Engine**: Ciclo de vida React (mount/unmount)
- **Metrics**: Eventos de fetch (latência, erros)
## Fornece a
- **Todos os 9 painéis**: Dados actualizados da API
- **Metrics**: Telemetria de fetch (tempo de resposta, taxa de erro)
+75
View File
@@ -0,0 +1,75 @@
---
type: Entity
layer: wiki
tags: [stack, dashdescomplicar, engine, frontend, core]
title: "Dashboard-Engine"
---
# Dashboard-Engine
Motor central da aplicação DashDescomplicar. Orquestra layout, navegação, autenticação e ciclo de dados entre API e painéis.
## Responsabilidades
- **Layout**: Sidebar (9 itens) + Main Content area com glass-morphism design
- **Roteamento**: React Router DOM 7 — `/` (Dashboard), `/monitor`, `/financial`, `/mcps`, `/n8n`, `/paperclip`, `/ai`, `/operations`, `/sessions`
- **Autenticação**: OIDC opcional (`react-oidc-context`) + rate-limit (100 req/15min)
- **Data Fetching**: `useEffect` + `useCallback` com reconnection logic
- **Componentes Compartilhados**: `HeroStat`, `GlassCard`, `Sparkline`, `StatusPill`, `ProgressRing`, `ListItem`, `SummaryWidget`
- **Animações**: Framer Motion (`containerVariants`, `itemVariants` com stagger)
- **Dashboard Principal**: 16 queries paralelas ao Desk CRM (tarefas, tickets, leads, projectos, calendário, pipeline, timesheet, billing 360)
## Stack
- **React 19** + TypeScript 5.9
- **Vite 7** (build, proxy `/api` → `localhost:3001`)
- **Tailwind CSS 4** (glass-card, dark theme)
- **Framer Motion 12** (stagger animations, spring physics)
- **Lucide React** (ícones — 25+ importados)
- **Recharts 3** (gráficos: Bar, Pie, Line, Sparkline)
- **React Router DOM 7** (BrowserRouter, Routes, Link)
## Localizações
- **Código**: `Dev/DashDescomplicar/src/App.tsx` (894 linhas)
- **Layout**: `src/components/Layout.tsx` (sidebar, navegação)
- **Deploy**: `dash.descomplicar.pt` (EasyPanel Swarm)
- **Repo**: `git.descomplicar.pt/ealmeida/DashDescomplicar`
- **Proxy Dev**: Vite proxy `/api` → `http://localhost:3001`
## Arquitectura de Componentes
```
App.tsx
├── Layout.tsx (sidebar + Outlet)
│ ├── Logo + Nome
│ ├── NavItem ×9 (Dashboard, Monitor, Financial, MCPs, n8n, Paperclip, AI, Operations, Sessions)
│ └── Footer (versão, status)
├── Páginas (lazy-loaded via React Router)
│ ├── Dashboard (/) — App.tsx interno
│ ├── Monitor (/monitor)
│ ├── Financial (/financial)
│ ├── McpMonitor (/mcps)
│ ├── N8nMonitor (/n8n)
│ ├── Paperclip (/paperclip)
│ ├── AiOverview (/ai)
│ ├── Operations (/operations)
│ ├── Sessions (/sessions)
│ └── SessionDetail (/sessions/:id)
└── Componentes compartilhados
├── GlassCard, HeroStat, Sparkline
├── StatusPill, ProgressRing
└── SummaryWidget, ListItem
```
## Dependências
- **Express-API**: 15 endpoints REST para dados
- **CSR-Integration**: Camada de fetch, cache, reconnection
- **Metrics**: Telemetria de renderização e interacção
- **Todos os 9 painéis**: Renderizados via React Router
## Fornece a
- **Todos os painéis**: Layout, navegação, autenticação
- **Utilizador**: Interface unificada `dash.descomplicar.pt`
+81
View File
@@ -0,0 +1,81 @@
---
type: Entity
layer: wiki
tags: [stack, dashdescomplicar, infra, docker, easypanel, deploy]
title: "Docker-Deploy"
---
# Docker-Deploy
Pipeline de containerização e deploy do DashDescomplicar via Docker + EasyPanel Swarm no servidor `easy` (VM 101, Proxmox).
## Responsabilidades
- **Build**: Imagem Docker multi-stage (`node:22-alpine`) com `npm ci` + build TypeScript
- **Deploy**: `docker service update --force` no EasyPanel Swarm
- **Runtime**: Node.js 22 Alpine, porta 3001, `NODE_ENV=production`
- **Static Serving**: Express serve o bundle Vite (`dist/`) em produção
- **Health**: `curl -sf https://dash.descomplicar.pt/api/health`
## Stack
- **Dockerfile**: 26 linhas, `node:22-alpine`
- **Build**: `tsc -b && vite build && tsc -p api/tsconfig.json`
- **Start**: `node api/dist/server.js` (compilado TS → JS)
- **Procfile**: `web: node api/dist/server.js` (EasyPanel)
- **Repo**: Gitea (`git.descomplicar.pt/ealmeida/DashDescomplicar`)
## Pipeline de Deploy
1. **Validar build local**: `npx tsc -b && npx vite build && npx tsc -p api/tsconfig.json`
2. **Push Gitea**: `git push origin main` (HTTPS, owner `ealmeida`)
3. **SSH easy**: `docker build -t easypanel/descomplicar/dashboard_descomplicar:latest .`
4. **Redeploy**: `docker service update --force descomplicar_dashboard_descomplicar`
5. **Verificar**: `curl -sf https://dash.descomplicar.pt/api/dashboard`
## Variáveis de Ambiente (Container)
```
DB_HOST=176.9.3.158
DB_USER=...
DB_PASS=...
DB_NAME=...
WP_MONITOR_API_KEY=...
HETZNER_TOKEN=...
N8N_API_KEY=...
BESZEL_URL=...
BESZEL_EMAIL=...
BESZEL_PASSWORD=...
EASY_HOST=5.9.90.70
EASY_USER=root
EASY_PASS=...
MCP_GATEWAY_URL=https://gateway.descomplicar.pt
OBSERVABILIDADE_DB=/data/sessions.db
NODE_ENV=production
API_PORT=3001
FRONTEND_URL=https://dash.descomplicar.pt
```
## Volumes
- `/root/data/dashboard-descomplicar/sessions.db:/data/sessions.db` (SQLite sessions)
## Localizações
- **Dockerfile**: `Dev/DashDescomplicar/Dockerfile`
- **Procfile**: `Dev/DashDescomplicar/Procfile`
- **Código no servidor**: `/etc/easypanel/projects/descomplicar/dashboard_descomplicar/code/`
- **Deploy**: `dash.descomplicar.pt` (EasyPanel Swarm, Traefik reverse proxy)
## Dependências
- **Gitea**: Push de código (`git.descomplicar.pt/ealmeida/DashDescomplicar`)
- **EasyPanel Server**: SSH (`5.9.90.70:22`, user `root`, password auth)
- **Express-API**: Servidor Node.js dentro do container
- **MySQL-Pool**: Conexão ao CWP (`176.9.3.158:3306`)
## Notas Operacionais
- EasyPanel MCP (`redeploy_service`) está quebrado — usar SEMPRE SSH directo
- Pre-commit hook (`Regra #47`): `npm audit --audit-level=moderate`
- `.env` no `.gitignore` — nunca commitar credenciais
+82
View File
@@ -0,0 +1,82 @@
---
type: Entity
layer: wiki
tags: [stack, dashdescomplicar, backend, express, api, core]
title: "Express-API"
---
# Express-API
Servidor backend Express 4 que expõe a REST API do DashDescomplicar. Orquestra 15 rotas, middleware de segurança e serve o frontend estático em produção.
## Responsabilidades
- **Roteamento**: 15 rotas REST (`/api/dashboard`, `/api/monitor`, `/api/hetzner`, `/api/wp-monitor`, `/api/server-metrics`, `/api/financial`, `/api/mcps`, `/api/n8n`, `/api/paperclip`, `/api/operations`, `/api/ai`, `/api/beszel`, `/api/sessions`, `/api/diagnostic`, `/api/health`)
- **Segurança**: Rate limiting (100 req/15min produção, 1000/15min dev), CORS restrito, validação Zod em todas as rotas, OIDC opcional
- **Error Handling**: UUIDs para tracking de erros, sem stack traces em produção, logs estruturados
- **Static Serving**: Serve o bundle Vite (React SPA) em produção na porta 3001
- **Health Check**: Endpoint `/health` (público) e `/api/health` (autenticado)
## Stack
- **Runtime**: Node.js 22+ (Alpine no Docker)
- **Framework**: Express 4.19+ com TypeScript 5.9
- **Validação**: Zod 4.3 (`z.object()`, `.passthrough()`, `z.coerce`)
- **Rate Limit**: `express-rate-limit` 8.2
- **CORS**: `cors` 2.8
- **Auth**: `express-openid-connect` (opcional, via `OIDC_ENABLED`)
- **DB Drivers**: `mysql2` 3.11 (promise), `ssh2` 1.17, `better-sqlite3` 12.9
- **Observabilidade**: SQLite local (`~/.claude-work/sessions.db`) para sessões Claude Code
## Localizações
- **Código**: `Dev/DashDescomplicar/api/server.ts` (238 linhas)
- **Rotas**: `api/routes/` (14 ficheiros)
- **Serviços**: `api/services/` (16 ficheiros)
- **Middleware**: `api/middleware/validation.ts`
- **DB Pool**: `api/db.ts`
- **Deploy**: `dash.descomplicar.pt:3001` (EasyPanel Swarm)
- **Dev**: `http://localhost:3001` (tsx watch)
## Rotas Expostas
| Rota | Ficheiro | Fonte Dados |
|---|---|---|
| `GET /api/dashboard` | `routes/dashboard.ts` | Desk CRM MySQL (16 queries paralelas) |
| `GET /api/monitor` | `routes/monitor.ts` | `tbl_eal_monitoring` (MySQL) |
| `GET /api/diagnostic` | `routes/diagnostic.ts` | Testes de conectividade (17 checks) |
| `GET/POST /api/hetzner` | `routes/hetzner.ts` | Hetzner Cloud API + MySQL |
| `GET/POST /api/wp-monitor` | `routes/wp-monitor.ts` | Plugin WP → MySQL (API key auth) |
| `POST /api/server-metrics` | `routes/server-metrics.ts` | SSH2 (EasyPanel) + MySQL |
| `GET /api/financial` | `routes/financial.ts` | Desk CRM MySQL (7 queries) |
| `GET /api/mcps` | `routes/mcps.ts` | MCP Gateway (HTTP ping, cache 60s) |
| `GET /api/n8n` | `routes/n8n.ts` | n8n REST API (cache 300s) |
| `GET /api/paperclip` | `routes/paperclip.ts` | PostgreSQL Paperclip (com fallback) |
| `GET /api/operations` | `routes/operations.ts` | Desk CRM MySQL (4 queries) |
| `GET /api/ai` | `routes/ai.ts` | Dados estáticos (snapshot STK) |
| `GET /api/beszel` | `routes/beszel.ts` | Beszel PocketBase API + MySQL |
| `GET /api/sessions` | `routes/sessions.ts` | SQLite local (better-sqlite3) |
## Dependências
- **MySQL-Pool**: Conexão ao Desk CRM (`176.9.3.158`)
- **Variáveis de Ambiente**: `DB_*`, `HETZNER_TOKEN`, `WP_MONITOR_API_KEY`, `N8N_API_KEY`, `BESZEL_*`, `EASY_*`, `MCP_GATEWAY_*`, `OIDC_*`, `OBSERVABILIDADE_DB`
- **Docker-Deploy**: Empacotado via `Dockerfile` → EasyPanel Swarm
- **Observabilidade-Sessions**: Abre ligação SQLite para sessões Claude Code
## Consome de
- Desk CRM (MySQL — `176.9.3.158:3306`)
- Hetzner Cloud API (`api.hetzner.cloud/v1`)
- MCP Gateway (`gateway.descomplicar.pt`)
- n8n API (`automator.descomplicar.pt/api/v1`)
- Paperclip PostgreSQL (container Docker)
- Beszel PocketBase API
- EasyPanel Server (SSH2 porta 22)
- Sistema de ficheiros local (SQLite sessions.db)
## Fornece a
- Todos os 8 painéis frontend (via fetch à API)
- Dashboard-Engine (dados agregados para `App.tsx`)
- Sistema de diagnóstico (`/api/diagnostic`)
+55
View File
@@ -0,0 +1,55 @@
---
type: Entity
layer: wiki
tags: [stack, dashdescomplicar, backend, integracao, beszel, monitorizacao]
title: "Integracao-Beszel"
---
# Integracao-Beszel
Integração com Beszel Hub (PocketBase API) para monitorização de frota de sistemas. Recolhe métricas de CPU, RAM, disco, containers e uptime de múltiplos hosts.
## Responsabilidades
- **Auth**: Autenticação PocketBase (token JWT com cache)
- **Fleet Collection**: Recolher métricas de todos os sistemas Beszel
- **Storage**: Guardar métricas em `tbl_eal_monitoring` (MySQL)
- **Dashboard**: Agregar total/up/down por sistema
## Stack
- **API**: PocketBase (`descomplicar-beszel.i7zsqo.easypanel.host`)
- **Auth**: Email + password → JWT token (cache em memória)
- **Validação**: Zod 4.3 (`BeszelSystemInfo`, `BeszelSystem`, `BeszelListResponse`)
- **DB**: MySQL via `db.ts`
## Rotas
| Método | Rota | Função |
|---|---|---|
| `GET` | `/api/beszel` | Fleet data do MySQL |
| `POST` | `/api/beszel/collect` | Trigger fresh collection |
## Sistemas Ignorados
- `black`, `hp`, `kriku` — Tor exit nodes que duplicam máquinas reais
## Tipos de Dados
- `FleetSummary`: total, up, down, systems[]
- `FleetSystem`: name, host, status, cpu, mem, disk, containers, load, uptime
## Localizações
- **Código**: `api/services/beszel.ts` (249 linhas), `api/routes/beszel.ts` (37 linhas)
- **DB**: `tbl_eal_monitoring` (category=beszel)
## Dependências
- **Beszel Hub**: PocketBase API (`BESZEL_URL`, `BESZEL_EMAIL`, `BESZEL_PASSWORD`)
- **MySQL-Pool**: Armazenamento de métricas
## Fornece a
- **Painel-Monitor**: Dados de fleet exibidos na secção "Frota Beszel"
- **Express-API**: Rota `/api/beszel`
+57
View File
@@ -0,0 +1,57 @@
---
type: Entity
layer: wiki
tags: [stack, dashdescomplicar, backend, integracao, hetzner, api]
title: "Integracao-Hetzner"
---
# Integracao-Hetzner
Integração com Hetzner Cloud API para monitorização de servidores VPS. Sincroniza lista de servidores, recolhe métricas (CPU, disco, rede) e armazena histórico na MySQL do Desk CRM.
## Responsabilidades
- **Sync**: Sincronizar lista de servidores da API Hetzner → `tbl_eal_hetzner_servers`
- **Métricas**: Recolher CPU%, disk IOPS, network BPS via `/servers/{id}/metrics`
- **Dashboard**: Agregar servidores + últimas métricas para o painel Monitor
- **Histórico**: Armazenar métricas em `tbl_eal_hetzner_metrics` para gráficos
- **Cleanup**: Limpeza de métricas antigas (>30 dias)
## Stack
- **API**: `https://api.hetzner.cloud/v1` (Bearer token `HETZNER_TOKEN`)
- **DB**: MySQL via `db.ts` (pool)
- **Validação**: Tipos TypeScript (sem Zod nesta rota)
## Rotas
| Método | Rota | Função |
|---|---|---|
| `GET` | `/api/hetzner` | Dashboard (servidores + últimas métricas) |
| `POST` | `/api/hetzner/sync` | Sincronizar lista de servidores |
| `POST` | `/api/hetzner/collect` | Recolher métricas de todos os servidores |
| `POST` | `/api/hetzner/collect/:hetzner_id` | Recolher métricas de servidor específico |
| `GET` | `/api/hetzner/history/:server_id` | Histórico de métricas para gráficos |
| `POST` | `/api/hetzner/cleanup` | Limpar métricas antigas |
## Servidores Monitorizados
- **CWP Server** (VM 100): IP `5.9.90.105` — hosting clientes
- **EasyPanel** (VM 101): IP `5.9.90.70` — Docker Swarm (46 serviços)
- **Gateway** (VM 103): proxy Nginx
- **Proxmox Host**: hypervisor físico
## Localizações
- **Código**: `api/services/hetzner.ts` (266 linhas), `api/routes/hetzner.ts` (141 linhas)
- **DB**: `tbl_eal_hetzner_servers`, `tbl_eal_hetzner_metrics`
## Dependências
- **Hetzner Cloud API**: Token `HETZNER_TOKEN` em `.env`
- **MySQL-Pool**: Armazenamento de servidores e métricas
## Fornece a
- **Painel-Monitor**: Dados de servidores exibidos na secção "Servidores"
- **Express-API**: Rota `/api/hetzner`
+58
View File
@@ -0,0 +1,58 @@
---
type: Entity
layer: wiki
tags: [stack, dashdescomplicar, backend, integracao, mcp, gateway]
title: "Integracao-MCP-Gateway"
---
# Integracao-MCP-Gateway
Monitorização do MCP Gateway (`gateway.descomplicar.pt`). Faz ping paralelo a 32 MCPs registados e expõe estado (online/offline/disabled) com cache de 60 segundos.
## Responsabilidades
- **Ping Paralelo**: Verifica health de cada MCP via HTTP (timeout 5s)
- **Gateway Status**: Verifica se o gateway está online
- **Categorização**: CRM, Infra, AI, Tools, External, Gateway, Project
- **Cache**: 60 segundos TTL para evitar sobrecarga
- **Auth Info**: Expõe método de autenticação e expiração do token
## Stack
- **Gateway**: `https://gateway.descomplicar.pt`
- **Auth**: `MCP_GATEWAY_TOKEN` (Bearer)
- **Ping**: HTTP GET com timeout 5s por MCP
- **Cache**: Em memória, 60s TTL
## MCPs Monitorizados (32)
| Categoria | MCPs | Exemplos |
|---|---|---|
| CRM | 2 | desk-crm-v3 (3150), desk-project-minimal (3153, disabled) |
| Infra | 3 | ssh-unified (3192), filesystem, chrome-devtools |
| AI | 5 | lightrag (3160), memory-supabase (3151), notebooklm (3190) |
| Tools | 5 | mcp-time (3155), deepl (3188), pexels, vimeo, drawio |
| External | 9 | google-workspace (3156), gitea (3162), moloni (3158), n8n |
| Gateway | 4 | authentik, spaceship, puppeteer, lighthouse |
| Project | 2 | carl-mcp, reonic |
## Rotas
| Método | Rota | Função |
|---|---|---|
| `GET` | `/api/mcps` | Dashboard com estado de todos os MCPs |
## Localizações
- **Código**: `api/services/mcps.ts` (219 linhas), `api/routes/mcps.ts` (24 linhas)
## Dependências
- **MCP Gateway**: `MCP_GATEWAY_URL` e `MCP_GATEWAY_TOKEN`
- **Express-API**: Rota `/api/mcps`
## Fornece a
- **Painel-MCPs**: Estado de todos os MCPs (online/offline/disabled)
- **Painel-IA**: Camada 1 (Claude Code) — contagem de MCPs
- **Express-API**: Rota `/api/mcps`
+50
View File
@@ -0,0 +1,50 @@
---
type: Entity
layer: wiki
tags: [stack, dashdescomplicar, backend, integracao, n8n, automacoes]
title: "Integracao-N8n-API"
---
# Integracao-N8n-API
Integração com a API REST do n8n para monitorização de workflows determinísticos. Consulta workflows e execuções e expõe dashboard agregado com cache de 5 minutos.
## Responsabilidades
- **Workflows**: Listar todos os workflows (activos/inactivos)
- **Execuções**: Última execução por workflow (status, duração)
- **Falhas 24h**: Contagem de execuções com erro nas últimas 24h
- **Cache**: 300 segundos TTL em memória para evitar sobrecarga da API n8n
## Stack
- **API**: `automator.descomplicar.pt/api/v1` (REST)
- **Auth**: `X-N8N-API-KEY` header
- **Cache**: Em memória (objecto, não Redis), 300s TTL
- **Validação**: Tipos TypeScript (sem Zod)
## Tipos
- `N8nWorkflow`: id, name, active, last_execution, tags
- `N8nDashboard`: total, active, inactive, failed_24h, workflows[], last_updated
## Rotas
| Método | Rota | Função |
|---|---|---|
| `GET` | `/api/n8n` | Dashboard agregado |
## Localizações
- **Código**: `api/services/n8n.ts` (183 linhas), `api/routes/n8n.ts` (24 linhas)
## Dependências
- **n8n API**: `N8N_API_KEY` e `N8N_API_URL` em `.env`
- **Express-API**: Rota `/api/n8n`
## Fornece a
- **Painel-N8n**: Workflows, execuções, falhas
- **Painel-IA**: Camada 2 (Determinístico) — contagem de workflows
- **Express-API**: Rota `/api/n8n`
+53
View File
@@ -0,0 +1,53 @@
---
type: Entity
layer: wiki
tags: [stack, dashdescomplicar, backend, integracao, ssh, metricas]
title: "Integracao-SSH-Metrics"
---
# Integracao-SSH-Metrics
Colector de métricas de servidores via SSH (biblioteca `ssh2`). Recolhe CPU, RAM, disco, load e containers do EasyPanel Server.
## Responsabilidades
- **SSH Connection**: Ligação ao EasyPanel (`5.9.90.70:22`) com password auth
- **Métricas**: Executa comandos remotos para recolher CPU%, RAM%, disco%, load avg, nº containers
- **Parsing**: Extrai valores de output formatado (`CPU:`, `MEM:`, `DISK:`, `LOAD:`, `CONTAINERS:`)
- **Sync**: Sincroniza servidores Hetzner para `tbl_eal_monitoring`
## Stack
- **Library**: `ssh2` 1.17 (Client, exec)
- **Timeout**: 20 segundos por ligação SSH
- **Auth**: Password (não suporta chave privada no container)
## Servidores
| Nome | Host | Porta | Auth | Notas |
|---|---|---|---|---|
| EasyPanel | `5.9.90.70` | 22 | Password | Docker Swarm — 46 serviços |
| CWP Server | `5.9.90.105` | 22 | ❌ Chave ed25519 | NÃO acessível do container |
## Rotas
| Método | Rota | Função |
|---|---|---|
| `POST` | `/api/server-metrics/collect` | Recolher todas as métricas |
| `POST` | `/api/server-metrics/ssh` | Apenas métricas SSH |
| `POST` | `/api/server-metrics/hetzner` | Sync Hetzner → monitoring |
## Localizações
- **Código**: `api/services/server-metrics.ts` (188 linhas), `api/routes/server-metrics.ts` (68 linhas)
- **DB**: `tbl_eal_monitoring`
## Dependências
- **EasyPanel Server**: SSH acesso (`.env`: `EASY_HOST`, `EASY_USER`, `EASY_PASS`)
- **MySQL-Pool**: Armazenamento de métricas
## Fornece a
- **Painel-Monitor**: Métricas SSH exibidas na secção "Infraestrutura"
- **Express-API**: Rota `/api/server-metrics`
+56
View File
@@ -0,0 +1,56 @@
---
type: Entity
layer: wiki
tags: [stack, dashdescomplicar, backend, integracao, wordpress, monitorizacao]
title: "Integracao-WordPress"
---
# Integracao-WordPress
Ingestão de dados de monitorização de sites WordPress. Recebe dados do plugin "Descomplicar Monitor" instalado em cada site cliente e armazena na base de dados para o painel Monitor.
## Responsabilidades
- **Ingestão**: Endpoint POST para receber dados dos plugins WP (versão WP, plugins, temas, health status)
- **API Key Auth**: Validação obrigatória via header `x-api-key` ou query param `key`
- **Determinação de Status**: Algoritmo que classifica o site como `ok`, `warning` ou `critical` baseado em:
- Core/plugins/themes desactualizados
- Erros de saúde (health check)
- Versão PHP
- **Listagem**: GET para listar todos os sites monitorizados
- **Connection Test**: `GET ?test` para verificar conectividade
## Stack
- **Validação**: Zod (`wpMonitorSchema`) com `.passthrough()` para campos extra de plugins
- **Auth**: `WP_MONITOR_API_KEY` obrigatória (crash ao iniciar se ausente)
- **DB**: MySQL via `db.ts`
## Schema de Dados (Plugin WP → API)
Recebe: `site_url`, `wp_version`, `php_version`, `plugins[]`, `themes[]`, `health_status`, `server_info`
## Rotas
| Método | Rota | Auth | Função |
|---|---|---|---|
| `GET` | `/api/wp-monitor` | API Key | Listar sites monitorizados |
| `GET` | `/api/wp-monitor?test` | API Key | Teste de conectividade |
| `POST` | `/api/wp-monitor` | API Key + Zod | Receber dados do plugin WP |
## Localizações
- **Código**: `api/routes/wp-monitor.ts` (148 linhas)
- **DB**: `tbl_eal_monitoring` (category=wordpress)
- **Plugin**: "Descomplicar Monitor" (instalado nos sites cliente)
## Dependências
- **MySQL-Pool**: Armazenamento de dados
- **WP_MONITOR_API_KEY**: Chave de API em `.env`
- **Plugins WP nos sites cliente**: Enviam dados periodicamente
## Fornece a
- **Painel-Monitor**: Secção "Sites WordPress" e "WP Updates"
- **Express-API**: Rota `/api/wp-monitor`
+91
View File
@@ -0,0 +1,91 @@
---
type: Entity
layer: wiki
tags: [stack, dashdescomplicar, telemetria, observabilidade, metricas]
title: "Metrics"
---
# Metrics
Sistema de telemetria e observabilidade do DashDescomplicar. Recolhe métricas de performance, disponibilidade e negócio em todas as camadas (frontend, API, infraestrutura).
## Responsabilidades
- **API Performance**: Tempo de resposta por endpoint, latência, taxa de erro
- **Frontend**: Tempo de renderização, tamanho de bundle, LCP, FID, CLS
- **Infraestrutura**: CPU, RAM, Disk dos servidores monitorados (via SSH + Hetzner + Beszel)
- **Disponibilidade**: Uptime dos sistemas integrados (MCP Gateway, n8n, Paperclip)
- **Negócio**: Vendas, despesas, lucro, tickets, leads (via Desk CRM)
- **Observabilidade**: Sessões Claude Code, tool calls, skills, padrões de uso
## Dimensões Recolhidas
### API
- Latência por endpoint (ms)
- Taxa de erro (4xx/5xx)
- Rate limit hits
### Frontend
- Tempo de renderização inicial (mount → data)
- Tamanho de bundle (Vite build)
- Core Web Vitals (LCP, FID, CLS)
### Infraestrutura
- CPU% por servidor (Hetzner, Beszel, SSH)
- RAM% por servidor
- Disk% por servidor
- Load average
- Containers Docker activos
### Disponibilidade
- MCP Gateway (32 MCPs — online/offline)
- n8n workflows (activos/falhas)
- Paperclip agentes (activos/idle/erro)
- Sites WordPress (ok/warning/critical)
### Negócio
- Vendas mês/ano (EUR)
- Despesas mês/ano (EUR)
- Tickets abertos/prioridade
- Leads em pipeline
### Observabilidade (Sessões)
- Sessões Claude Code / dia
- Tool calls / sessão
- Skills invocadas
- Projectos activos
- Padrões de erro
- Duração média de sessão
## Stack
- **Colectores**: Scripts Python (Gemini), Node.js (sessions-indexer), MySQL queries
- **Armazenamento**: SQLite (`sessions.db`), MySQL (`tbl_eal_monitoring`, `tbl_eal_hetzner_metrics`)
- **Exposição**: Endpoints REST (não Prometheus — planeado)
- **Logs**: `logs/performance-metrics.json` (180B — inicial)
## Localizações
- **Logs**: `logs/performance-metrics.json`
- **Sessions DB**: `~/.claude-work/sessions.db`
- **Monitoring DB**: `tbl_eal_monitoring` (MySQL)
- **Hetzner Metrics**: `tbl_eal_hetzner_metrics` (MySQL)
- **Gemini Logs**: `~/.claude-work/tool-invocations.jsonl`
## Consome de
- **Dashboard-Engine**: Eventos de renderização
- **Express-API**: Latência, erros (middleware)
- **Painéis**: Eventos de interacção, fetch timing
- **Systemd-Timers**: Indexação de sessões
- **Monitorizacao-Gemini**: Telemetria Gemini
- **Integracao-Hetzner**: Métricas de servidores
- **Integracao-SSH-Metrics**: Métricas SSH
- **Integracao-Beszel**: Fleet metrics
## Fornece a
- **Painel-Monitor**: Métricas de infra agregadas
- **Painel-Financeiro**: Métricas de negócio
- **Painel-Sessoes**: Dados de observabilidade
- **Sistema IA**: Padrões de uso, anomalias
+61
View File
@@ -0,0 +1,61 @@
---
type: Entity
layer: wiki
tags: [stack, dashdescomplicar, infra, monitorizacao, gemini, telemetria]
title: "Monitorizacao-Gemini"
---
# Monitorizacao-Gemini
Sistema de telemetria para hooks do Gemini CLI. Recolhe métricas de tool calls do Gemini e escreve no mesmo ficheiro JSONL usado pelo Claude Code para métricas unificadas.
## Responsabilidades
- **Hook AfterTool**: Captura tool_name, session_id, success/error, duration
- **Formato Unificado**: Escreve em `~/.claude-work/tool-invocations.jsonl` com campo `engine: gemini`
- **Pré-Telemetria**: Script `gemini-pre-telemetry.py` regista timestamp de início para cálculo de duração
- **Health Check**: `gemini-health.sh` verifica conectividade do Gemini CLI
## Stack
- **Python 3**: Scripts de hook (piped via stdin/stdout do Gemini CLI)
- **Formato**: JSONL (compatível com Claude Code)
- **Ficheiro**: `~/.claude-work/tool-invocations.jsonl`
## Scripts
| Script | Função | Trigger |
|---|---|---|
| `gemini-telemetry.py` (2.3KB) | Hook AfterTool — captura e regista métricas | Gemini CLI after tool call |
| `gemini-pre-telemetry.py` (1.0KB) | Hook BeforeTool — regista timestamp início | Gemini CLI before tool call |
| `gemini-health.sh` (1.0KB) | Health check conectividade Gemini | Manual/agendado |
## Formato de Log
```json
{
"ts": "2026-06-30T12:00:00Z",
"session": "<session_id>",
"tool_name": "read_file",
"success": true,
"error": "",
"duration": 1.23,
"engine": "gemini"
}
```
## Localizações
- **Scripts**: `Dev/DashDescomplicar/monitorizacao/` (3 ficheiros)
- **Output**: `~/.claude-work/tool-invocations.jsonl`
- **Temp**: `~/.gemini/tmp/telemetry/<session_id>.start`
## Dependências
- **Gemini CLI**: Pipe stdin/stdout para hooks Python
- **Sistema de Ficheiros**: `~/.claude-work/`, `~/.gemini/tmp/`
## Fornece a
- **Observabilidade-Sessions**: Dados de telemetria Gemini unificados com Claude
- **Sistema IA**: Métricas cross-engine (Claude + Gemini + Hermes + OpenCode)
+57
View File
@@ -0,0 +1,57 @@
---
type: Entity
layer: wiki
tags: [stack, dashdescomplicar, backend, mysql, database, core]
title: "MySQL-Pool"
---
# MySQL-Pool
Pool de ligações MySQL2 (promise) ao Desk CRM Perfex. Fonte de verdade para dados de tarefas, projectos, tickets, facturação, despesas, leads e monitorização.
## Responsabilidades
- **Connection Pool**: 10 ligações máx, timeout 10s connect / 15s acquire / 30s query
- **Validação**: Credenciais obrigatórias (`DB_USER`, `DB_PASS`, `DB_NAME`) — crash ao iniciar se ausentes
- **Charset**: `utf8mb4` para suporte completo a caracteres PT-PT
- **Health Check**: Teste automático ao iniciar (`pool.getConnection()`)
## Stack
- **Driver**: `mysql2` 3.11 (modo promise)
- **Host**: `176.9.3.158` (servidor CWP)
- **Base de Dados**: `ealmeida_desk24` (Perfex CRM)
- **Prefixo**: `DB_*` (NÃO `MYSQL_*`)
## Tabelas Consumidas
| Tabela | Consumidor | Uso |
|---|---|---|
| `tbltasks` | dashboard.ts, operations.ts | Tarefas (urgentes, alta, vencidas) |
| `tblprojects` | dashboard.ts | Projectos (JOIN com tasks) |
| `tbltickets` | dashboard.ts, operations.ts | Tickets suporte (abertos, prioridade, departamento) |
| `tblinvoices` | financial.ts | Facturas (vendas mês/ano, evolução) |
| `tblexpenses` | financial.ts | Despesas (mês/ano, categorias) |
| `tblexpenses_categories` | financial.ts | Categorias despesas (JOIN) |
| `tblleads` | dashboard.ts | Leads (contactar, followup, proposta) |
| `tbltaskstimers` | dashboard.ts | Timesheet (horas registadas) |
| `tbldepartments` | operations.ts | Departamentos tickets |
| `tbl_eal_monitoring` | monitoring.ts | Monitorização (items, categorias, status) |
| `tbl_eal_hetzner_servers` | hetzner.ts | Servidores Hetzner (sync, métricas) |
| `tbl_eal_hetzner_metrics` | hetzner.ts | Histórico métricas Hetzner |
## Localizações
- **Código**: `Dev/DashDescomplicar/api/db.ts` (42 linhas)
- **Config**: `.env` (`DB_HOST`, `DB_USER`, `DB_PASS`, `DB_NAME`)
- **Host**: CWP Server (VM 100, IP `5.9.90.105`)
## Dependências
- `.env` com `DB_HOST`, `DB_USER`, `DB_PASS`, `DB_NAME`
- Servidor MySQL no CWP (porta 3306, reachable do EasyPanel)
## Fornece a
- **Express-API**: Todas as rotas que consomem Desk CRM
- **Serviços**: dashboard, financial, operations, monitoring, hetzner, server-metrics
+80
View File
@@ -0,0 +1,80 @@
---
type: Entity
layer: wiki
tags: [stack, dashdescomplicar, backend, observabilidade, sessoes, sqlite, systemd]
title: "Observabilidade-Sessions"
---
# Observabilidade-Sessions
Sistema de observabilidade de sessões Claude Code (Espelho). Indexa, armazena e expõe dados de sessões de múltiplos motores IA (Claude Code, Hermes, OpenCode) via SQLite local.
## Responsabilidades
- **Indexação**: Watcher incremental de ficheiros JSONL (Claude Code) + import de BD (Hermes SQLite, OpenCode SQLite)
- **Armazenamento**: SQLite local (`~/.claude-work/sessions.db`) com schema optimizado
- **Worklog Import**: Import de worklogs Desk CRM (discussions #31/#32/#33)
- **Pattern Detection**: Detecção semanal de padrões de uso (tools, skills, erros)
- **API**: Rota `/api/sessions` (lista + detalhe com eventos)
- **Sincronização**: Volume bind EasyPanel (`/root/data/dashboard-descomplicar/sessions.db` → `/data/sessions.db`)
## Stack
- **DB**: `better-sqlite3` 12.9 (síncrono, zero-deps)
- **Watcher**: `chokidar` 5.0 (file system events)
- **Parser**: `node-html-parser` 7.1 (extração HTML de worklogs)
- **Runtime**: Node.js 22 via `tsx` (systemd services)
## Componentes do Sistema
| Componente | Ficheiro | Função |
|---|---|---|
| Indexer | `api/scripts/sessions-indexer.ts` | Watcher incremental de JSONL |
| Patterns | `api/scripts/sessions-patterns.ts` | Detector semanal de padrões |
| Worklog Import | `api/scripts/sessions-worklog-import.ts` | Import de worklogs Desk |
| DB Layer | `api/services/sessions/db.ts` | Schema, queries, CRUD |
| Parser | `api/services/sessions/parser.ts` | Parse de ficheiros JSONL |
| Claude Indexer | `api/services/sessions/indexer.ts` | Indexação específica Claude |
| Hermes Indexer | `api/services/sessions/hermes-indexer.ts` | Indexação Hermes (`~/.hermes/state.db`) |
| OpenCode Indexer | `api/services/sessions/opencode-indexer.ts` | Indexação OpenCode (`~/.local/share/opencode/opencode.db`) |
| MCP Client | `api/services/sessions/mcp-client.ts` | Cliente MCP para Desk CRM |
| Watcher | `api/services/sessions/watcher.ts` | File watcher eventos |
| Patterns Engine | `api/services/sessions/patterns.ts` | Análise de padrões (19KB) |
## Fontes de Dados
- **Claude Code**: `~/.claude/projects/*/sessions/*.jsonl` (JSONL)
- **Hermes**: `~/.hermes/state.db` (SQLite)
- **OpenCode**: `~/.local/share/opencode/opencode.db` (SQLite)
- **Gemini**: `~/.claude-work/tool-invocations.jsonl` (via Gemini-Telemetry)
- **Desk CRM**: Discussions #31, #32, #33 (worklogs)
## Systemd Timers
| Timer | Schedule | Função |
|---|---|---|
| `observabilidade-indexer.service` | Sempre ligado (simple) | Watcher incremental |
| `observabilidade-worklog-import.timer` | Diário 03:00 | Import worklogs Desk |
| `observabilidade-patterns.timer` | Dom 23:00 | Detecção de padrões |
## Localizações
- **SQLite DB**: `~/.claude-work/sessions.db` (local) / `/data/sessions.db` (container)
- **Código**: `api/services/sessions/` (10 ficheiros, ~56KB)
- **Scripts**: `api/scripts/sessions-*.ts` (3 scripts)
- **Systemd**: `systemd/observabilidade-*.{service,timer}` (5 ficheiros)
- **Volume**: `/root/data/dashboard-descomplicar/sessions.db` → container
## Dependências
- **Systemd-Timers**: Execução agendada dos scripts
- **Gemini-Telemetry**: Dados de telemetria Gemini
- **MySQL-Pool**: Worklogs Desk CRM (discussions)
- **Express-API**: Rota `/api/sessions` expõe dados
- **Sistema de Ficheiros**: `~/.claude/`, `~/.hermes/`, `~/.local/share/opencode/`
## Fornece a
- **Painel-Sessoes**: Dados para listagem e detalhe
- **Express-API**: Rota `/api/sessions`
- **Sistema IA**: Métricas de uso, padrões, anomalias
+35
View File
@@ -0,0 +1,35 @@
---
type: Entity
layer: wiki
tags: [stack, dashdescomplicar, painel, cron, proposto]
title: "Painel-CronDev"
---
# Painel-CronDev
Painel para todas as acções cron do servidor `dev` (CT 102 / systemd user timers).
## Status
- **Proposto** — não implementado ainda
- Requisição: visibilidade agregada dos 9 agentes cron
## Conteúdo planeado
- Lista de jobs cron activos (schedule, último run, próximo run, duração, exit code)
- Estado: OK / Warning / Erro / Stale (>24h sem correr)
- Logs recentes (tail) por job, filtro e pesquisa
- Alertas para falhas consecutivas (>2)
- Acção manual "Run now" (opcional, autenticada)
- Métricas: total jobs, sucessos 24h, falhas 24h, tempo médio
## Fonte planeada
- SSH ao `dev`
- Leitura de `/var/log/` e logs de agentes
- `systemctl --user list-timers`
## Componente
- **Localização**: `src/pages/CronDev.tsx` (proposto)
- **Route**: `/cron-dev` (proposto)
+66
View File
@@ -0,0 +1,66 @@
---
type: Entity
layer: wiki
tags: [stack, dashdescomplicar, painel, dashboard, home]
title: "Painel-Dashboard"
---
# Painel-Dashboard
Página principal (home) do DashDescomplicar. Hero stats agregados do Desk CRM com resumo do dia, tarefas, tickets, leads, projectos, pipeline e métricas de tempo.
## Responsabilidades
- **Hero Stats**: 5 métricas principais com sparklines, gradientes e tendências
- **Resumo do Dia**: Data formatada PT-PT, eventos hoje, tarefas urgentes/alta, tickets abertos
- **Tarefas**: Urgentes (priority 4), Alta (priority 3), Vencidas, Em Testes, Esta Semana
- **Leads**: Contactar, Follow-up, Proposta — com pipeline stage
- **Projectos**: Lista de projectos activos com progresso
- **Calendário**: Eventos de hoje e da semana (Google Calendar via Desk)
- **Timesheet**: Horas registadas hoje/semana
- **Billing 360**: Facturação do mês com progress ring
- **Pipeline**: Leads em pipeline por stage
- **Monday Mood**: Frase motivacional (apenas às segundas)
## Stack
- **React 19** + TypeScript 5.6
- **Framer Motion**: Stagger + spring + hover animations
- **Lucide React**: 25+ ícones (Calendar, AlertTriangle, Clock, RefreshCw, etc.)
- **Tailwind CSS**: Glass cards, gradientes, status pills
- **Recharts**: Sparkline (mini gráfico de tendência)
## Componentes
- `HeroStat`: Métrica grande com ícone, gradiente, sparkline, tendência
- `GlassCard`: Card com accent color e contador
- `Sparkline`: Mini gráfico SVG de tendência
- `StatusPill`: Badge de status (emerald, amber, red)
- `ProgressRing`: Anel de progresso SVG circular
- `SummaryWidget`: Widget bento-style com resumo do dia
- `ListItem`: Item de lista com hover e badges
## Tipos
- `DashboardData`: date, day_of_week, is_monday, monday_mood, tarefas (urgente, alta, vencidas, em_testes, esta_semana), tickets (abertos, alta_prioridade), leads (contactar, proposta, followup), projectos[], pipeline[], eventos (hoje[], semana[]), timesheet (hoje, semana), billing_360, resumo
## API Consumida
| API | Rota | Dados |
|---|---|---|
| Dashboard | `GET /api/dashboard` | 16 queries paralelas ao Desk CRM |
## Localizações
- **Código**: `src/App.tsx` (função App(), linhas 416-818 + mock data)
- **Rota**: `/`
## Dependências
- **Express-API**: Rota `/api/dashboard` (16 queries)
- **MySQL-Pool**: Dados do Desk CRM (tarefas, tickets, leads, projectos, calendário, timesheet, facturação)
- **Dashboard-Engine**: Layout (sidebar + header)
## Estado
- Activo
+63
View File
@@ -0,0 +1,63 @@
---
type: Entity
layer: wiki
tags: [stack, dashdescomplicar, painel, financeiro, metricas]
title: "Painel-Financeiro"
---
# Painel-Financeiro
Painel de métricas financeiras do negócio Descomplicar®. Exibe receitas, despesas e lucro com gráficos de evolução e distribuição por categoria.
## Responsabilidades
- **Métricas Principais**: Vendas Mês, Vendas Ano, Despesas Mês, Despesas Ano, Lucro/Prejuízo
- **Gráfico de Evolução**: Receita vs Despesa (últimos 12 meses) — Recharts BarChart
- **Distribuição**: Despesas por categoria (pie chart visual)
- **Formatação**: EUR com locale `pt-PT` (`formatEUR`)
- **Estados**: Loading (pulse animation), Empty (null check), Error (console)
## Stack
- **React 19** + TypeScript 5.6
- **Recharts 3**: BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend
- **Framer Motion**: Stagger animations
- **Lucide React**: DollarSign, TrendingUp, TrendingDown, Receipt, PiggyBank
- **Tailwind CSS**: Glass cards, status colors (emerald para lucro, red para prejuízo)
## Tipos
- `FinancialData`: vendas_mes, vendas_ano, despesas_mes, despesas_ano, lucro_mes, lucro_ano, categorias[] (name, value), evolucao[] (mes, receita, despesa)
## Componentes
- `StatCard`: Card de métrica com label, valor formatado, ícone, cor
- `CustomTooltip`: Tooltip do Recharts com formatação EUR
- `formatEUR`: Conversor para locale PT-PT
## API Consumida
| API | Rota | Dados |
|---|---|---|
| Financial | `GET /api/financial` | Agregados de vendas/despesas MySQL |
## Fontes de Dados (via API)
- **`tblinvoices`**: Facturas (não-draft, não-canceladas) — vendas mês/ano, evolução 12 meses
- **`tblexpenses`**: Despesas — despesas mês/ano, categorias, evolução 12 meses
- **`tblexpenses_categories`**: JOIN para nomes de categorias
## Localizações
- **Código**: `src/pages/Financial.tsx` (251 linhas)
- **Rota**: `/financial`
## Dependências
- **Express-API**: Rota `/api/financial`
- **MySQL-Pool**: Dados de facturação do Desk CRM
- **Dashboard-Engine**: Layout, navegação
## Estado
- Activo
+62
View File
@@ -0,0 +1,62 @@
---
type: Entity
layer: wiki
tags: [stack, dashdescomplicar, painel, ia, claude-code, stack]
title: "Painel-IA"
---
# Painel-IA
Painel de overview do ecossistema de IA Descomplicar®. Visualiza as 3 camadas de execução (Claude Code, n8n, Paperclip), sistemas transversais e configuração CARL.
## Responsabilidades
- **3 Camadas**: Claude Code (interactivo), n8n (determinístico), Paperclip (autónomo)
- **Métricas por Camada**: Skills (189), Agents (72), MCPs (39), Hooks (9), Plugins (23), Workflows (14), Agentes Paperclip (16), Routines (5)
- **Sistemas Transversais**: RAG/CARL, Anti-alucinação, Knowledge Graph (LightRAG), Auto-melhoria, Design
- **CARL Config**: 8 domínios, 45 regras
- **Notebooks**: 58 notebooks NotebookLM
- **Design Visual**: Gradientes únicos por camada (violet=Claude, amber=n8n, cyan=Paperclip), glow effects
## Stack
- **React 19** + TypeScript 5.6
- **Framer Motion**: Stagger + spring animations
- **Lucide React**: Brain, Cpu, Bot, Zap, Shield, Database, BookOpen, Layers
- **Tailwind CSS**: Gradient cards, badges coloridos
## Tipos
- `AiLayer`: name, label, items[] (metric, value, detail)
- `TransversalSystem`: name, status (active|warning|inactive), detail
- `CarlConfig`: domains[], total_rules
- `AiDashboard`: layers[], transversal[], carl, notebooks, last_updated
## Configuração Visual por Camada
| Camada | Gradiente | Cor | Ícone |
|---|---|---|---|
| Claude Code | violet-500 → purple-500 | `text-violet-300` | Brain |
| n8n | amber-500 → orange-500 | `text-amber-300` | Zap |
| Paperclip | cyan-500 → teal-500 | `text-cyan-300` | Bot |
## API Consumida
| API | Rota | Dados |
|---|---|---|
| AI | `GET /api/ai` | Dados estáticos do stack (snapshot STK-Estado-Actual.md) |
## Localizações
- **Código**: `src/pages/AiOverview.tsx` (452 linhas)
- **Rota**: `/ai`
## Dependências
- **Express-API**: Rota `/api/ai`
- **STK-Estado-Actual.md**: Fonte estática de dados do stack
- **Dashboard-Engine**: Layout, navegação
## Estado
- Activo — dados estáticos (snapshot 06-04-2026), a evoluir para dados dinâmicos
+57
View File
@@ -0,0 +1,57 @@
---
type: Entity
layer: wiki
tags: [stack, dashdescomplicar, painel, mcps, gateway, monitorizacao]
title: "Painel-MCPs"
---
# Painel-MCPs
Painel de estado dos 39 MCPs do ecossistema Descomplicar®. Visualização em tempo real do gateway com categorização, status indicators e refresh manual.
## Responsabilidades
- **Visão Agregada**: Contadores Online / Offline / Disabled / Total
- **Gateway Status**: Indicador de estado do gateway (online/offline)
- **Categorização**: 7 categorias (CRM, Infra, AI, Tools, External, Gateway, Project) com ordenação fixa
- **Detalhe por MCP**: Nome, porta, status, response time, tools count, última verificação
- **Refresh Manual**: Botão "Actualizar" para forçar re-ping
- **Auth Info**: Método de autenticação + expiração do token
## Stack
- **React 19** + TypeScript 5.6
- **Framer Motion**: Stagger + spring animations
- **Lucide React**: Network, Wifi, WifiOff, Server, CheckCircle2, XCircle, Clock
- **Tailwind CSS**: Status colors (emerald=online, red=offline, zinc=disabled), glow effects
## Tipos
- `McpStatus`: name, port, category, enabled, status (online|offline|disabled|unknown), response_time_ms, last_check, tools_count
- `McpDashboard`: gateway_status, total, online, offline, disabled, mcps[], auth
## Componentes
- `StatusIndicator`: Bolinha pulsante (online=emerald glow, offline=red pulse, disabled=zinc)
- `McpCard`: Card individual com status, porta, response time
## API Consumida
| API | Rota | Dados |
|---|---|---|
| MCPs | `GET /api/mcps` | Gateway health + 32 MCPs com cache 60s |
## Localizações
- **Código**: `src/pages/McpMonitor.tsx` (362 linhas)
- **Rota**: `/mcps`
## Dependências
- **Integracao-MCP-Gateway**: Ping aos 32 MCPs via gateway
- **Express-API**: Rota `/api/mcps`
- **Dashboard-Engine**: Layout, navegação
## Estado
- Activo
+62
View File
@@ -0,0 +1,62 @@
---
type: Entity
layer: wiki
tags: [stack, dashdescomplicar, painel, monitor, infraestrutura]
title: "Painel-Monitor"
---
# Painel-Monitor
Painel de monitorização de infraestrutura e WordPress. Visualiza estado de servidores Proxmox, containers Docker, sites WordPress, backups e frota Beszel em tempo real.
## Responsabilidades
- **Infraestrutura**: Load, storage (NVMe, RAID1), containers Docker por servidor
- **Servidores**: CWP Server (VM 100, QEMU), EasyPanel (VM 101, QEMU), Gateway (VM 103), Proxmox Host
- **WordPress**: Sites monitorizados (status, actualizações pendentes)
- **Frota Beszel**: Sistemas agregados via Beszel Hub (CPU, RAM, disco, containers, uptime)
- **Backups**: Estado dos backups (último, tamanho, status)
- **Alertas**: Itens com status warning/critical destacados
- **Categorização**: Itens agrupados por categoria (infra, wordpress, backups, beszel, hetzner)
## Stack
- **React 19** + TypeScript 5.6
- **Framer Motion** (stagger animations)
- **Lucide React** (15+ ícones: Server, Container, HardDrive, Globe, Wifi, etc.)
- **Tailwind CSS** (glass-card, status colors)
## Tipos
- `MonitorItem`: id, name, category, status, details (JSON), last_check
- `CategorySummary`: category, total, ok, warning, critical
- `MonitorData`: items (Record<category, MonitorItem[]>), summary, overall, stats
- `VMConfig`: name, id, type (QEMU/LXC), ip, role, icon, accent colors
- `FleetSystem`: name, host, status, cpu, mem, disk, containers, load, uptime
- `FleetData`: total, up, down, systems[]
## APIs Consumidas
| API | Rota | Dados |
|---|---|---|
| Monitor | `GET /api/monitor` | Items de monitorização (MySQL `tbl_eal_monitoring`) |
| Beszel | `GET /api/beszel` | Frota de sistemas Beszel |
| Hetzner | `GET /api/hetzner` | Servidores Hetzner + métricas |
## Localizações
- **Código**: `src/pages/Monitor.tsx` (788 linhas — maior página)
- **Rota**: `/monitor`
## Dependências
- **Express-API**: Rotas `/api/monitor`, `/api/beszel`, `/api/hetzner`
- **Dashboard-Engine**: Layout, navegação
- **Integracao-Hetzner**: Dados de servidores
- **Integracao-Beszel**: Fleet data
- **Integracao-WordPress**: Dados de sites WP
- **Integracao-SSH-Metrics**: Métricas SSH
## Estado
- Activo (Fase C — com frota Beszel integrada 2026-06)
+57
View File
@@ -0,0 +1,57 @@
---
type: Entity
layer: wiki
tags: [stack, dashdescomplicar, painel, n8n, automacoes, workflows]
title: "Painel-N8n"
---
# Painel-N8n
Painel de monitorização de workflows n8n (Camada 2 — Determinístico). Exibe estado de todos os workflows, execuções recentes e falhas.
## Responsabilidades
- **Métricas Principais**: Total Workflows, Activos, Inactivos, Falhas (24h)
- **Lista de Workflows**: Nome, estado (activo/inactivo), última execução, duração, tags
- **Status Badges**: Sucesso (emerald), Erro (red), Running, Pendente
- **Formatação**: Duração em ms/s/min, datas em locale PT-PT
- **Cache**: Dados com cache de 300s no backend
## Stack
- **React 19** + TypeScript 5.6
- **Framer Motion**: Stagger animations
- **Lucide React**: GitBranch, Play, Pause, CheckCircle2, XCircle, Clock, AlertTriangle
- **Tailwind CSS**: Glass cards, highlight para falhas (borda red)
## Tipos
- `N8nWorkflow`: id, name, active, last_execution (status, started_at, finished_at, duration_ms), tags
- `N8nDashboard`: total, active, inactive, failed_24h, workflows[], last_updated
## Componentes
- `StatCard`: Métrica agregada com highlight condicional (red se falhas > 0)
- `StatusBadge`: Pill com ícone e cor por status
- `formatDuration`, `formatDate`, `formatLastUpdated`: Utilitários PT-PT
## API Consumida
| API | Rota | Dados |
|---|---|---|
| n8n | `GET /api/n8n` | Workflows + execuções (cache 300s) |
## Localizações
- **Código**: `src/pages/N8nMonitor.tsx` (382 linhas)
- **Rota**: `/n8n`
## Dependências
- **Integracao-N8n-API**: n8n REST API (`automator.descomplicar.pt`)
- **Express-API**: Rota `/api/n8n`
- **Dashboard-Engine**: Layout, navegação
## Estado
- Activo
+71
View File
@@ -0,0 +1,71 @@
---
type: Entity
layer: wiki
tags: [stack, dashdescomplicar, painel, operacoes, tickets, procs]
title: "Painel-Operacoes"
---
# Painel-Operacoes
Painel de gestão operacional via Desk CRM. Exibe tickets abertos, prioridades, distribuição por departamento e cobertura de PROCs.
## Responsabilidades
- **Tickets**: Total abertos (Open, In Progress, Answered), Alta Prioridade, Tempo Médio Resposta (90 dias)
- **Distribuição**: Tickets por departamento (gráfico de barras)
- **Cobertura PROCs**: Barra de progresso por departamento com percentagem
- **Thresholds**: ≥80% verde, ≥50% amarelo, <50% vermelho
## Stack
- **React 19** + TypeScript 5.6
- **Recharts 3**: BarChart para tickets por departamento
- **Framer Motion**: Stagger animations
- **Lucide React**: Ticket, AlertTriangle, Clock, FileText, Building2
- **Tailwind CSS**: Glass cards, progress bars coloridas
## Tipos
- `TicketsByDept`: dept, count
- `CoverageItem`: dept, procs, total_expected, pct
- `OperationsData`: tickets (open, high_priority, avg_response_hours, by_department), procedures (total, departments, coverage)
## Componentes
- `StatCard`: Métrica com label, valor, ícone, cor
- `ProgressBar`: Barra de progresso com cor condicional (emerald ≥80%, amber ≥50%, red <50%)
- `CustomTooltip`: Tooltip do Recharts
## Cobertura de PROCs (Dados Estáticos)
| Departamento | PROCs | Esperado | % |
|---|---|---|---|
| D7 — Tecnologia | 18 | 20 | 90% |
| D6 — Gestão | 8 | 10 | 80% |
| D1 — Comercial | 5 | 8 | 62% |
| D5 — Marketing | 5 | 8 | 62% |
| Cross-Departamental | 5 | 6 | 83% |
| D3 — Contabilidade | 3 | 5 | 60% |
| D2 — Suporte | 3 | 6 | 50% |
| D4 — RH | 1 | 4 | 25% |
## API Consumida
| API | Rota | Dados |
|---|---|---|
| Operations | `GET /api/operations` | Tickets Desk CRM + cobertura PROCs |
## Localizações
- **Código**: `src/pages/Operations.tsx` (323 linhas)
- **Rota**: `/operations`
## Dependências
- **Express-API**: Rota `/api/operations`
- **MySQL-Pool**: Dados de tickets do Desk CRM
- **Dashboard-Engine**: Layout, navegação
## Estado
- Activo
+53
View File
@@ -0,0 +1,53 @@
---
type: Entity
layer: wiki
tags: [stack, dashdescomplicar, painel, paperclip, agentes, automacao]
title: "Painel-Paperclip"
---
# Painel-Paperclip
Painel do orquestrador autónomo Paperclip (Camada 3 — Autónomo). Monitoriza agentes (64), routines e issues. Suspenso desde 08-04-2026, mantido para reintrodução progressiva.
## Responsabilidades
- **Agentes**: Total, Activos, Idle, Erro — agrupados por role hierárquico (Board → CEO → C-Level → Director → Specialist)
- **Routines**: Total, Activas, com cron expression e última execução
- **Issues**: Abertas, Em Progresso, Fechadas (7d)
- **Fallback**: Dados zeros quando BD Paperclip inacessível (sem erro 500)
- **Ordenação Hierárquica**: Roles ordenadas por senioridade (ROLE_ORDER map)
## Stack
- **React 19** + TypeScript 5.6
- **Framer Motion**: Stagger + spring animations
- **Lucide React**: Bot, Users, Calendar, CheckCircle2, AlertTriangle, Activity, Zap
- **Tailwind CSS**: Glass cards, status colors
## Tipos
- `PaperclipAgent`: id, name, role, status (active|idle|error|archived), last_heartbeat, last_run, total_runs
- `PaperclipRoutine`: id, name, cron, active, last_run, last_status, next_run
- `PaperclipDashboard`: agents (total, active, idle, error, list), routines (total, active, list), issues (open, in_progress, closed_7d)
## API Consumida
| API | Rota | Dados |
|---|---|---|
| Paperclip | `GET /api/paperclip` | PostgreSQL Paperclip (com fallback) |
## Localizações
- **Código**: `src/pages/Paperclip.tsx` (546 linhas)
- **Rota**: `/paperclip`
## Dependências
- **Express-API**: Rota `/api/paperclip`
- **Paperclip PostgreSQL**: Base de dados de agentes/routines
- **Dashboard-Engine**: Layout, navegação
## Estado
- **Suspenso** (08-04-2026) — reintrodução progressiva planeada
- Painel mantido para monitorização durante reintrodução
+62
View File
@@ -0,0 +1,62 @@
---
type: Entity
layer: wiki
tags: [stack, dashdescomplicar, frontend, painel, sessoes, claude-code]
title: "Painel-Sessoes"
---
# Painel-Sessoes
Painel de observabilidade de sessões Claude Code. Lista sessões indexadas com filtros (projecto, tool, skill, pesquisa) e navega para detalhe com visualização de eventos.
## Responsabilidades
- **Listagem**: 50 sessões por página com paginação (offset)
- **Filtros**: Dias (7 default), projecto, tool, skill, pesquisa textual
- **Navegação**: Link para `/sessions/:id` (detalhe da sessão)
- **Detalhe**: Visualização de eventos com filtros (all, no-system, tools-only, prompts-only)
- **Estado**: Loading, error, e empty states com fallback
## Stack
- **React 19** + TypeScript 5.6
- **Framer Motion** (animações)
- **React Router DOM 7** (`useParams`, `Link`)
- **Tailwind CSS** (glass-card, bordas, estados)
## Componentes
| Componente | Ficheiro | Função |
|---|---|---|
| `Sessions` | `src/pages/Sessions.tsx` | Página listagem (130 linhas) |
| `SessionDetail` | `src/pages/SessionDetail.tsx` | Página detalhe (83 linhas) |
| `FilterBar` | `src/components/sessions/FilterBar.tsx` | Barra de filtros (projecto, tool, skill, search) |
| `SessionRow` | `src/components/sessions/SessionRow.tsx` | Linha de sessão na tabela |
| `EventBlock` | `src/components/sessions/EventBlock.tsx` | Bloco de evento no detalhe |
## Tipos
- `SessionMeta`: session_id, project_slug, started_at, duration_sec, event_count, tool_calls, skills_invoked, outcome, jsonl_path, first_prompt
- `SessionEvent`: index, type (user|system|assistant), tool_name, tool_result, content
- `ListResponse`: total + items[]
- `Filters`: days, project, tool, skill, q
## API Consumida
- `GET /api/sessions` — lista paginada com filtros (Zod validation)
- `GET /api/sessions/:id` — detalhe com meta + eventos
## Rota
- `/sessions` — Lista de sessões
- `/sessions/:id` — Detalhe da sessão
## Dependências
- **Observabilidade-Sessions**: SQLite local (`sessions.db`) + indexer
- **Express-API**: Rota `/api/sessions`
- **Dashboard-Engine**: Layout + navegação sidebar
## Estado
- Activo (Fase C — implementado 2026-06)
+69
View File
@@ -0,0 +1,69 @@
---
type: Entity
layer: wiki
tags: [stack, dashdescomplicar, infra, systemd, timers, observabilidade]
title: "Systemd-Timers"
---
# Systemd-Timers
Timers systemd (user) que executam scripts de observabilidade do DashDescomplicar no desktop local (`dev`). Gerem indexação de sessões, import de worklogs e detecção de padrões.
## Responsabilidades
- **Indexação contínua**: Watcher incremental de ficheiros JSONL (Claude Code)
- **Import worklogs**: Import diário de worklogs Desk CRM (discussions #31/#32/#33)
- **Detecção padrões**: Análise semanal de padrões de uso (tools, skills, erros)
- **Publicação**: Resultados publicados para consulta pela API
## Serviços e Timers
| Ficheiro | Tipo | Schedule | Função |
|---|---|---|---|
| `observabilidade-indexer.service` | simple (always-on) | — | Watcher incremental ficheiros JSONL |
| `observabilidade-worklog-import.service` | oneshot | — | Import worklogs Desk |
| `observabilidade-worklog-import.timer` | timer | Diário 03:00 | Dispara import worklogs |
| `observabilidade-patterns.service` | oneshot | — | Detecção de padrões |
| `observabilidade-patterns.timer` | timer | Dom 23:00 | Dispara detecção semanal |
| `observabilidade-patterns.env.example` | env | — | Template variáveis ambiente |
## Stack
- **Runtime**: Node.js 22.22.2 via `npx tsx`
- **Working Directory**: `/media/ealmeida/Dados/Dev/DashDescomplicar`
- **DB Path**: `~/.claude-work/sessions.db`
- **Logs**: `~/.claude-work/observabilidade-*.log`
## Comandos Executados
```
# Indexer (always-on)
npx tsx api/scripts/sessions-indexer.ts --watch
# Worklog Import (daily 03:00)
npx tsx api/scripts/sessions-worklog-import.ts --discussion all --since-days 7
# Patterns (weekly Sun 23:00)
npx tsx api/scripts/sessions-patterns.ts --publish
```
## Localizações
- **Systemd files**: `Dev/DashDescomplicar/systemd/` (5 ficheiros)
- **Scripts**: `Dev/DashDescomplicar/api/scripts/sessions-*.ts`
- **Logs**: `~/.claude-work/observabilidade-*.log`
- **DB**: `~/.claude-work/sessions.db`
- **Env file**: `~/.claude-work/observabilidade-patterns.env`
## Dependências
- **Node.js 22**: `/home/ealmeida/.nvm/versions/node/v22.22.2/bin/`
- **Observabilidade-Sessions**: Scripts e lógica de indexação
- **Gemini-Telemetry**: Dados de telemetria Gemini no mesmo DB
- **Sistema de Ficheiros**: `~/.claude/projects/`, `~/.hermes/`, `~/.local/share/opencode/`
## Fornece a
- **Observabilidade-Sessions**: Execução agendada dos scripts
- **Painel-Sessoes**: Dados actualizados para consulta
- **Express-API**: Sessões indexadas disponíveis via `/api/sessions`
+202
View File
@@ -0,0 +1,202 @@
---
type: Index
layer: wiki
tags: [stack, dashdescomplicar, okf, grafo]
title: "DashDescomplicar — OKF / Grafo de Conhecimento"
---
# DashDescomplicar — OKF / Grafo de Conhecimento
Representação do DashDescomplicar como um grafo de entidades e relacionamentos seguindo os princípios Karpathy:
- 1 conceito = 1 ficheiro atómico
- Navegável e bidireccional
- Classificado por layer
---
## Entidades Principais (25)
### Motor Central (3)
- [[Dashboard-Engine]] — Orquestrador principal, router React, layout
- [[Express-API]] — Servidor backend Express 4 com 15 rotas REST
- [[MySQL-Pool]] — Pool de ligações MySQL2 ao Desk CRM Perfex
### Painéis Frontend (9)
- [[Painel-Dashboard]] — Página principal, hero stats Desk CRM
- [[Painel-Monitor]] — Monitorização de infraestrutura, WP, Beszel, Hetzner
- [[Painel-Financeiro]] — Métricas de negócio (vendas, despesas, lucro)
- [[Painel-MCPs]] — Estado dos 32 MCPs via gateway
- [[Painel-N8n]] — Automações determinísticas (workflows + execuções)
- [[Painel-Paperclip]] — Orquestrador autónomo (suspenso, manutenção)
- [[Painel-IA]] — Overview do ecossistema Claude Code (3 camadas)
- [[Painel-Operacoes]] — Gestão operacional (tickets + cobertura PROCs)
- [[Painel-Sessoes]] — Observabilidade de sessões Claude Code
### Backend — Integrações (6)
- [[Integracao-Hetzner]] — Hetzner Cloud API (servidores + métricas)
- [[Integracao-Beszel]] — Beszel Hub (fleet monitoring)
- [[Integracao-SSH-Metrics]] — Métricas via SSH2 (EasyPanel)
- [[Integracao-WordPress]] — Plugin WP Monitor (ingestão de dados)
- [[Integracao-N8n-API]] — n8n REST API (workflows + execuções)
- [[Integracao-MCP-Gateway]] — MCP Gateway (health check 32 MCPs)
### Infraestrutura (4)
- [[Docker-Deploy]] — Docker + EasyPanel Swarm deployment
- [[Systemd-Timers]] — Systemd user timers (indexação + observabilidade)
- [[Monitorizacao-Gemini]] — Gemini CLI telemetry hooks
- [[Observabilidade-Sessions]] — SQLite sessions DB + indexer + patterns
### Cross-Cutting (2)
- [[CSR-Integration]] — Camada de Cliente (fetch, cache, reconnection)
- [[Metrics]] — Telemetria e observabilidade (multi-camada)
### Proposto (1)
- [[Painel-CronDev]] — Cron dev server (proposto, não implementado)
---
## Relacionamentos (17)
### Estrutura Core (4)
- [[DashboardEngine-exibe-Paineis]] — Dashboard-Engine exibe todos os painéis
- [[ExpressAPI-roteia-Paineis]] — Express-API fornece dados a todos os painéis
- [[MySQL-Pool-alimenta-Servicos]] — MySQL-Pool alimenta serviços de dados
- [[Docker-Deploy-serve-Dashboard]] — Docker-Deploy empacota e serve a aplicação
### Integrações Funcionais (2)
- [[DashboardEngine-integra-CSRIntegration]] — Integração com camada CSR
- [[CSRIntegration-integra-Metrics]] — Telemetria integrada
### Backend → Painéis (4)
- [[Integracao-Hetzner-alimenta-Monitor]] — Métricas Hetzner → Painel-Monitor
- [[Integracao-Beszel-alimenta-Monitor]] — Fleet Beszel → Painel-Monitor
- [[Integracao-WordPress-alimenta-Monitor]] — Sites WP → Painel-Monitor
- [[Integracao-N8n-alimenta-Paineis]] — n8n → Painel-N8n + Painel-IA
- [[Integracao-MCPGateway-alimenta-Paineis]] — MCP Gateway → Painel-MCPs + Painel-IA
### Consumo de Dados (4)
- [[Paineis-consomem-DeskCRM]] — Desk CRM (tarefas, tickets, horas)
- [[Paineis-consomem-Moloni]] — Moloni API (faturação)
- [[Paineis-consomem-SSH]] — SSH/Infraestrutura (Hetzner, CWP, EasyPanel)
- [[Paineis-consomem-MCPGateway]] — MCP Gateway (status dos 32 MCPs)
### Observabilidade (3)
- [[DashboardEngine-integra-Observabilidade]] — Integração sistema Observabilidade
- [[Systemd-Timers-alimenta-Observabilidade]] — Timers → Indexação sessions
- [[Gemini-Telemetry-alimenta-Observabilidade]] — Gemini telemetry → sessions DB
- [[Observabilidade-alimenta-PainelSessoes]] — Sessions DB → Painel-Sessoes
### Melhoria Contínua (1)
- [[DashboardEngine-integra-SelfImprovement]] — Ciclo melhoria contínua (PDCA)
- [[Metrics-alimenta-PainelMonitor]] — Métricas agregadas → Painel-Monitor
---
## Taxonomia de Relacionamentos
| Tipo | Exemplo | Significado |
|---|---|---|
| `exibe` | Dashboard-Engine → exibe → Painéis | Motor renderiza painel |
| `roteia` | Express-API → roteia → Painéis | API fornece dados via endpoints |
| `alimenta` | MySQL-Pool → alimenta → Serviços | Fonte de dados abastece consumidor |
| `serve` | Docker-Deploy → serve → Dashboard | Infra serve aplicação |
| `integra-com` | Dashboard-Engine → integra-com → CSR | Integração funcional |
| `usa` / `consome` | Painéis → consomem → Desk CRM | Consumo de dados externos |
---
## Regras Karpathy
✅ **1 conceito = 1 ficheiro** — 25 entidades + 17 relacionamentos atómicos
✅ **Atómico** — Ficheiros independentes e legíveis
✅ **Navegável** — Este índice liga todas as peças
✅ **Partilhado** — Entidades externas (Desk CRM, Hetzner) referenciadas sem duplicação
✅ **Bidireccional** — Relacionamentos documentados nos dois sentidos (via index)
---
## Estrutura de Ficheiros
```
okf/
├── index.md # Este ficheiro
├── FASE-C-VALIDACAO.md # Validação Fase C
├── entidades/ # 25 entidades atómicas
│ ├── Dashboard-Engine.md
│ ├── Express-API.md # NOVO
│ ├── MySQL-Pool.md # NOVO
│ ├── Painel-Dashboard.md
│ ├── Painel-Monitor.md
│ ├── Painel-Financeiro.md
│ ├── Painel-MCPs.md
│ ├── Painel-N8n.md
│ ├── Painel-Paperclip.md
│ ├── Painel-IA.md
│ ├── Painel-Operacoes.md
│ ├── Painel-Sessoes.md # NOVO
│ ├── Painel-CronDev.md
│ ├── Integracao-Hetzner.md # NOVO
│ ├── Integracao-Beszel.md # NOVO
│ ├── Integracao-SSH-Metrics.md # NOVO
│ ├── Integracao-WordPress.md # NOVO
│ ├── Integracao-N8n-API.md # NOVO
│ ├── Integracao-MCP-Gateway.md # NOVO
│ ├── Docker-Deploy.md # NOVO
│ ├── Systemd-Timers.md # NOVO
│ ├── Monitorizacao-Gemini.md # NOVO
│ ├── Observabilidade-Sessions.md # NOVO
│ ├── CSR-Integration.md
│ └── Metrics.md
└── relacionamentos/ # 17 arestas
├── DashboardEngine-exibe-Paineis.md
├── ExpressAPI-roteia-Paineis.md # NOVO
├── MySQL-Pool-alimenta-Servicos.md # NOVO
├── Docker-Deploy-serve-Dashboard.md # NOVO
├── DashboardEngine-integra-CSRIntegration.md
├── CSRIntegration-integra-Metrics.md
├── Integracao-Hetzner-alimenta-Monitor.md # NOVO
├── Integracao-Beszel-alimenta-Monitor.md # NOVO
├── Integracao-WordPress-alimenta-Monitor.md # NOVO
├── Integracao-N8n-alimenta-Paineis.md # NOVO
├── Integracao-MCPGateway-alimenta-Paineis.md # NOVO
├── Observabilidade-alimenta-PainelSessoes.md # NOVO
├── Systemd-Timers-alimenta-Observabilidade.md # NOVO
├── Gemini-Telemetry-alimenta-Observabilidade.md # NOVO
├── Paineis-consomem-DeskCRM.md
├── Paineis-consomem-Moloni.md
├── Paineis-consomem-SSH.md
├── Paineis-consomem-MCPGateway.md
├── DashboardEngine-integra-Observabilidade.md
├── DashboardEngine-integra-SelfImprovement.md
└── Metrics-alimenta-PainelMonitor.md
```
---
## Critérios de Conclusão
### Fase B (OKF Estrutura)
✅ Entidades: 25 (3 core + 9 painéis + 6 integrações + 4 infra + 2 cross + 1 proposto)
✅ Relacionamentos: 17 arestas (4 core + 2 funcional + 5 backend-painéis + 4 consumo + 4 observabilidade + 1 melhoria)
✅ Frontmatter: `type: Entity` / `type: Relationship` correcto
✅ Tags: stack, dashdescomplicar, tipo-entidade
✅ Navegação: okf/index.md centralizado
### Fase C (5W2H+PDCA + Relacionamentos Cruzados)
✅ **AGENTS.md:** When/How completo com referências PLAN/PROC/QR
✅ **STATUS.md:** PDCA (Plan-Do-Check-Act) ligado a docs/PLANS/ e observabilidade
✅ **Coerência:** Sem entidades órfãs; cada entidade tem ≥1 entrada + ≥1 saída
✅ **Bidireccionalidade:** Relacionamentos documentados em ambos os sentidos (via index)
✅ **Cross-Sistema:** Links a SPEC/PLAN autoritários
### Fase D (Deep OKF — 2026-06-30)
✅ **Entidades Deep:** Todas as 25 entidades com detalhe técnico completo
✅ **Integrações Mapeadas:** 6 integrações backend documentadas (Hetzner, Beszel, SSH, WP, n8n, MCP Gateway)
✅ **Infraestrutura Completa:** Docker, systemd timers, Gemini telemetry, sessions DB
✅ **Painel Sessions:** Documentado com componentes, tipos, API, filtros
✅ **Stack por Entidade:** Tecnologias, localizações, dependências, fornecedores
✅ **Total:** 25 entidades + 17 relacionamentos = 42 ficheiros OKF
---
**Deep OKF Concluído** — 2026-06-30
@@ -0,0 +1,27 @@
---
type: Relationship
layer: wiki
tags: [stack, dashdescomplicar, relacionamento, integra]
title: "CSR-Integration → integra-com → Metrics"
---
# CSR-Integration integra Metrics
CSR-Integration instrumento requisições HTTP e tempos de renderização para Metrics.
## Instrumentação
- Latência de requisições (por endpoint)
- Taxa de sucesso / erro (HTTP 2xx vs 4xx/5xx)
- Tempo de renderização (React components)
- Tamanho de payload
## Tipo de relação
`integra-com` — CSR-Integration expõe eventos para coleta de métricas.
## Implementação
- Middleware Express para logging
- Custom hooks React para performance monitoring
- `logs/performance-metrics.json` (saída)
@@ -0,0 +1,32 @@
---
type: Relationship
layer: wiki
tags: [stack, dashdescomplicar, relacionamento, exibe]
title: "Dashboard-Engine → exibe → Painéis"
---
# Dashboard-Engine exibe Painéis
Dashboard-Engine orquestra e exibe todos os painéis através da navegação por routes.
## Arestas individuais
- Dashboard-Engine → exibe → Painel-Dashboard (`/`)
- Dashboard-Engine → exibe → Painel-Monitor (`/monitor`)
- Dashboard-Engine → exibe → Painel-Financeiro (`/financial`)
- Dashboard-Engine → exibe → Painel-MCPs (`/mcps`)
- Dashboard-Engine → exibe → Painel-N8n (`/n8n`)
- Dashboard-Engine → exibe → Painel-Paperclip (`/paperclip`)
- Dashboard-Engine → exibe → Painel-IA (`/ai`)
- Dashboard-Engine → exibe → Painel-Operacoes (`/operations`)
- Dashboard-Engine → exibe → Painel-CronDev (`/cron-dev`) — proposto
## Tipo de relação
`exibe` — Dashboard-Engine é responsável por renderizar os painéis.
## Implementação
- React Router (v6+)
- Route handlers em `src/App.tsx`
- Layout wrapper em `src/components/Layout.tsx`
@@ -0,0 +1,27 @@
---
type: Relationship
layer: wiki
tags: [stack, dashdescomplicar, relacionamento, integra]
title: "Dashboard-Engine → integra-com → CSR-Integration"
---
# Dashboard-Engine integra CSR-Integration
Dashboard-Engine integra a camada CSR para fetch de dados, caching e reconnection logic.
## Fluxo
1. Dashboard-Engine renderiza Layout
2. Painéis (componentes filhos) usam hooks/Context de CSR-Integration
3. CSR-Integration faz requisições HTTP para API Express
4. Respostas são cacheadas e compartilhadas entre painéis
## Tipo de relação
`integra-com` — Dashboard-Engine depende funcionalmente de CSR-Integration.
## Touchpoints
- `src/lib/DataContext.tsx` — Context Provider
- `src/App.tsx` — wrapper CSR
- Painéis — consumem Context
@@ -0,0 +1,52 @@
---
type: Relationship
layer: wiki
tags: [stack, dashdescomplicar, relacionamento, observabilidade, sistema]
title: "Dashboard-Engine → integra-com → Observabilidade (Sistema)"
---
# Dashboard-Engine integra Observabilidade
Dashboard-Engine integra o sistema Observabilidade para visibilidade de sessões Claude Code, métricas cross-sistema e operações.
## Escopo de Integração
**Observabilidade é um sistema transversal (Desk #2059) que:**
- Fornece SPEC autoritária em `/media/ealmeida/Dados/Hub/05-Projectos/Observabilidade/SPEC.md`
- Fornece PLAN autoritário em `/media/ealmeida/Dados/Hub/05-Projectos/Observabilidade/PLAN.md`
- Indexa e expõe sessões Claude Code (replay, timeline, eventos)
- Coleta telemetria cross-projeto (IJFW, n8n, MCPs, SelfImprovement)
## Touchpoints no DashDescomplicar
1. **Painel Sessions** (`/sessions`) — Replay de sessões Claude Code
- Consome índice SQLite do Observabilidade
- Timeline + busca + filtros por agente/projecto
2. **Painel Monitor** (`/monitor`) — Métricas de infraestrutura
- Integrado com telemetria Observabilidade
- Health de MCPs, n8n, servidores VPS
3. **Painel Operações** (`/operations`) — Desk CRM + eventos cross-sistema
- Visibility de tarefas + webhooks/eventos Observabilidade
4. **Metrics Entity** (`okf/entidades/Metrics.md`)
- Coleta de eventos do Observabilidade
- Agregação em `logs/performance-metrics.json`
## Tipo de relação
`integra-com` — Dashboard-Engine depende de Observabilidade para visibilidade completa do stack
## Relacionamentos Secundários (Bidirecional)
- Metrics ← alimenta → Painel-Monitor (okf/relacionamentos/Metrics-alimenta-PainelMonitor.md)
- Painel-Sessions ← consome → Observabilidade
- Observabilidade ← expõe-em → Painel-Monitor + Painel-Operacoes
## Referências Autoritárias
- **Observabilidade SPEC:** `/media/ealmeida/Dados/Hub/05-Projectos/Observabilidade/SPEC.md`
- **Observabilidade PLAN:** `/media/ealmeida/Dados/Hub/05-Projectos/Observabilidade/PLAN.md`
- **Desk Observabilidade:** #2059
- **SelfImprovement:** #2066 (melhoria contínua da Observabilidade)
@@ -0,0 +1,63 @@
---
type: Relationship
layer: wiki
tags: [stack, dashdescomplicar, relacionamento, automação, melhoria]
title: "Dashboard-Engine → integra-com → SelfImprovement"
---
# Dashboard-Engine integra SelfImprovement
Dashboard-Engine integra o ciclo de melhoria contínua SelfImprovement para feedback, análise e decisões de priorização.
## Ciclo de Feedback
**Fluxo PDCA (em STATUS.md § ACT):**
1. **Recolher Feedback**
- Desk #65 comentários (bugs, features)
- CodeRED incidents (site down, P0)
- Logs de erro (`docker logs descomplicar_dashboard_descomplicar`)
- Métricas de saúde monitoradas (uptime, response time, error rate)
2. **Análise** (SelfImprovement)
- Identificar padrões e causa raiz
- Correlacionar com eventos do Observabilidade
- Priorizar via RICE: Reach × Impact ÷ Confidence ÷ Effort
3. **Decisão & Implementação**
- Nova feature/bugfix → PR em Gitea
- Code review + merge → deploy
- Atualizar CHANGELOG.md + PLAN.md roadmap
4. **Registar & Monitorar**
- STATUS.md atualizado com versão
- Métricas de sucesso monitoradas
- Feedback loop re-inicia
## SelfImprovement Desk Task
- **Desk #2066:** Melhoria contínua da operação
- **SPEC:** `/media/ealmeida/Dados/Hub/05-Projectos/SelfImprovement/SPEC.md`
- **PLAN:** `/media/ealmeida/Dados/Hub/05-Projectos/SelfImprovement/PLAN.md`
## Tipo de relação
`integra-com` — Dashboard-Engine alimenta SelfImprovement com feedback e métricas para decisões
## Relacionamentos Secundários
- Metrics ← coleta-observabilidade → SelfImprovement (validação de impacto)
- Observabilidade ← expõe-dados → SelfImprovement (causa raiz)
- SelfImprovement ← realimenta → Dashboard-Engine (novas features/fixes)
## Escalação de Incidentes
| Severidade | Processo | Contacto |
|------------|----------|----------|
| **P0** (site down) | CodeRED trigger imediato | `curl -X POST https://codered.descomplicar.pt/v1/trigger/RED-ZMT3FCU8` |
| **P1** (erro crítico) | Desk #65 comentário + SelfImprovement análise | RFC em #65 |
| **P2** (bugfix/feature) | Desk #65 comentário + roadmap | Próximo sprint Q3-Q4 |
## Referência à Integração
Ver STATUS.md § ACT (Melhoria Contínua) para documentação do ciclo completo.
@@ -0,0 +1,29 @@
---
type: Relationship
layer: wiki
tags: [stack, dashdescomplicar, relacionamento, docker, deploy]
title: "Docker-Deploy-serve-Dashboard"
---
# Docker-Deploy → serve → DashDescomplicar
**Origem:** [[Docker-Deploy]]
**Destino:** [[Express-API]], [[Dashboard-Engine]]
**Tipo:** `serve`
## Descrição
O Docker-Deploy empacota e serve a aplicação completa (frontend + backend) num container Docker no EasyPanel Swarm.
## Fluxo de Deploy
```
git push Gitea → Docker build → docker service update → Traefik → dash.descomplicar.pt
```
## Artefactos
- **Dockerfile**: `node:22-alpine`, build multi-stage
- **Procfile**: `web: node api/dist/server.js`
- **Volume**: `sessions.db` bind mount para persistência SQLite
- **Rede**: Traefik reverse proxy (EasyPanel)
@@ -0,0 +1,34 @@
---
type: Relationship
layer: wiki
tags: [stack, dashdescomplicar, relacionamento, api, paineis]
title: "ExpressAPI-roteia-Paineis"
---
# ExpressAPI → roteia → Painéis
**Origem:** [[Express-API]]
**Destino:** [[Painel-Dashboard]], [[Painel-Monitor]], [[Painel-Financeiro]], [[Painel-MCPs]], [[Painel-N8n]], [[Painel-Paperclip]], [[Painel-IA]], [[Painel-Operacoes]], [[Painel-Sessoes]]
**Tipo:** `roteia`
## Descrição
A Express-API expõe 15 endpoints REST que fornecem dados a todos os 9 painéis frontend. Cada painel consome um ou mais endpoints específicos:
| Painel | Endpoint(s) |
|---|---|
| Painel-Dashboard | `GET /api/dashboard` |
| Painel-Monitor | `GET /api/monitor`, `GET /api/beszel`, `GET /api/hetzner` |
| Painel-Financeiro | `GET /api/financial` |
| Painel-MCPs | `GET /api/mcps` |
| Painel-N8n | `GET /api/n8n` |
| Painel-Paperclip | `GET /api/paperclip` |
| Painel-IA | `GET /api/ai` |
| Painel-Operacoes | `GET /api/operations` |
| Painel-Sessoes | `GET /api/sessions` |
## Notas
- Vite proxy no desenvolvimento: `/api` → `http://localhost:3001`
- Em produção, Express serve o bundle estático + API na mesma porta (3001)
- Rate limit compartilhado: 100 req/15min (prod), 1000/15min (dev)
@@ -0,0 +1,32 @@
---
type: Relationship
layer: wiki
tags: [stack, dashdescomplicar, relacionamento, gemini, telemetria]
title: "Gemini-Telemetry-alimenta-Observabilidade"
---
# Monitorizacao-Gemini → alimenta → Observabilidade
**Origem:** [[Monitorizacao-Gemini]]
**Destino:** [[Observabilidade-Sessions]]
**Tipo:** `alimenta`
## Descrição
Os hooks de telemetria do Gemini CLI escrevem métricas de tool calls no mesmo ficheiro JSONL usado pelo Claude Code, unificando a observabilidade cross-engine.
## Fluxo
```
Gemini CLI (tool call)
→ gemini-pre-telemetry.py (timestamp start)
→ [tool execution]
→ gemini-telemetry.py (log JSONL)
→ ~/.claude-work/tool-invocations.jsonl
→ Observabilidade-Sessions (indexer)
→ Painel-Sessoes (consulta)
```
## Formato Unificado
Cada entrada inclui campo `engine` para distinguir origem (claude, gemini, hermes, opencode).
@@ -0,0 +1,16 @@
---
type: Relationship
layer: wiki
tags: [stack, dashdescomplicar, relacionamento, beszel, monitor]
title: "Integracao-Beszel-alimenta-Monitor"
---
# Integracao-Beszel → alimenta → Painel-Monitor
**Origem:** [[Integracao-Beszel]]
**Destino:** [[Painel-Monitor]]
**Tipo:** `alimenta`
## Descrição
A integração Beszel recolhe métricas de fleet (CPU, RAM, disco, containers, uptime) via PocketBase API e alimenta a secção "Frota Beszel" do Painel-Monitor.
@@ -0,0 +1,16 @@
---
type: Relationship
layer: wiki
tags: [stack, dashdescomplicar, relacionamento, hetzner, monitor]
title: "Integracao-Hetzner-alimenta-Monitor"
---
# Integracao-Hetzner → alimenta → Painel-Monitor
**Origem:** [[Integracao-Hetzner]]
**Destino:** [[Painel-Monitor]]
**Tipo:** `alimenta`
## Descrição
A integração Hetzner recolhe métricas de CPU, disco e rede dos VPS e sincroniza a lista de servidores. Estes dados alimentam a secção "Servidores" do Painel-Monitor, com histórico para gráficos de tendência.
@@ -0,0 +1,16 @@
---
type: Relationship
layer: wiki
tags: [stack, dashdescomplicar, relacionamento, mcp, gateway]
title: "Integracao-MCPGateway-alimenta-Paineis"
---
# Integracao-MCP-Gateway → alimenta → Painéis
**Origem:** [[Integracao-MCP-Gateway]]
**Destino:** [[Painel-MCPs]], [[Painel-IA]]
**Tipo:** `alimenta`
## Descrição
A integração MCP Gateway faz ping paralelo a 32 MCPs e fornece estado (online/offline/disabled) para o Painel-MCPs (detalhe por MCP) e Painel-IA (Camada 1 — contagem de MCPs).
@@ -0,0 +1,16 @@
---
type: Relationship
layer: wiki
tags: [stack, dashdescomplicar, relacionamento, n8n, monitor]
title: "Integracao-N8n-alimenta-Paineis"
---
# Integracao-N8n-API → alimenta → Painéis
**Origem:** [[Integracao-N8n-API]]
**Destino:** [[Painel-N8n]], [[Painel-IA]]
**Tipo:** `alimenta`
## Descrição
A integração n8n consulta a API REST do n8n (`automator.descomplicar.pt`) e fornece dados de workflows e execuções para o Painel-N8n (detalhe de workflows) e Painel-IA (Camada 2 — contagem de workflows determinísticos).
@@ -0,0 +1,16 @@
---
type: Relationship
layer: wiki
tags: [stack, dashdescomplicar, relacionamento, wordpress, monitor]
title: "Integracao-WordPress-alimenta-Monitor"
---
# Integracao-WordPress → alimenta → Painel-Monitor
**Origem:** [[Integracao-WordPress]]
**Destino:** [[Painel-Monitor]]
**Tipo:** `alimenta`
## Descrição
O plugin "Descomplicar Monitor" instalado nos sites WordPress cliente envia dados (versão WP, plugins, temas, health) via POST para a API. Estes dados alimentam as secções "Sites WordPress" e "WP Updates" do Painel-Monitor.
@@ -0,0 +1,35 @@
---
type: Relationship
layer: wiki
tags: [stack, dashdescomplicar, relacionamento, observabilidade, métricas]
title: "Metrics → alimenta → Painel-Monitor"
---
# Metrics alimenta Painel-Monitor
Painel-Monitor consome métricas de infraestrutura e performance agregadas pelo sistema Metrics.
## Fluxo de Dados
1. **Coleta:** Middleware Express + hooks React instrumentam requisições/renderização
2. **Agregação:** Logs JSON em `logs/performance-metrics.json` (rotated 7 dias)
3. **Exposição:** GET `/api/metrics` (Prometheus format ou JSON custom)
4. **Consumo:** Painel-Monitor via CSR-Integration (com cache + reconnect)
## Métricas Expostas
- **Performance:** response time (p95), latência por endpoint, tamanho de payload
- **Disponibilidade:** uptime, taxa de erro (5xx), health de dependências
- **Infraestrutura:** CPU/RAM/Disk dos servidores monitorados (via SSH)
- **Aplicação:** tempos de renderização, LCP, FID, CLS
## Tipo de relação
`alimenta` — Metrics → Painel-Monitor: fonte de verdade para observabilidade
## Referências
- Entidade Metrics: `okf/entidades/Metrics.md`
- Entidade Painel-Monitor: `okf/entidades/Painel-Monitor.md`
- Métrica de saúde: STATUS.md § CHECK
- Documentação: docs/PLANS/DashDescomplicar-PLAN.md § Métricas de Saúde
@@ -0,0 +1,37 @@
---
type: Relationship
layer: wiki
tags: [stack, dashdescomplicar, relacionamento, mysql, servicos]
title: "MySQL-Pool-alimenta-Servicos"
---
# MySQL-Pool → alimenta → Serviços
**Origem:** [[MySQL-Pool]]
**Destino:** [[Express-API]], [[Painel-Dashboard]], [[Painel-Financeiro]], [[Painel-Operacoes]]
**Tipo:** `alimenta`
## Descrição
O MySQL-Pool é a fonte de verdade para dados do Desk CRM (Perfex). Todos os serviços que precisam de dados de negócio (tarefas, tickets, facturação, leads, projectos) consultam o mesmo pool.
## Fluxo de Dados
```
MySQL-Pool (ealmeida_desk24 @ 176.9.3.158)
├── dashboard.ts → Painel-Dashboard (16 queries)
├── financial.ts → Painel-Financeiro (7 queries)
├── operations.ts → Painel-Operacoes (4 queries)
├── monitoring.ts → Painel-Monitor (1 query)
└── hetzner.ts → Painel-Monitor (sync + métricas)
```
## Tabelas Críticas
| Tabela | Consumidores |
|---|---|
| `tbltasks` | dashboard, operations |
| `tblinvoices` | financial |
| `tblexpenses` | financial |
| `tbltickets` | dashboard, operations |
| `tbl_eal_monitoring` | monitoring, hetzner, wp-monitor |
@@ -0,0 +1,26 @@
---
type: Relationship
layer: wiki
tags: [stack, dashdescomplicar, relacionamento, sessoes, observabilidade]
title: "Observabilidade-alimenta-PainelSessoes"
---
# Observabilidade-Sessions → alimenta → Painel-Sessoes
**Origem:** [[Observabilidade-Sessions]]
**Destino:** [[Painel-Sessoes]]
**Tipo:** `alimenta`
## Descrição
O sistema de observabilidade indexa sessões Claude Code (JSONL), Hermes (SQLite) e OpenCode (SQLite) na base SQLite local. O Painel-Sessoes consulta estes dados via API para listagem paginada com filtros e visualização detalhada de eventos.
## Fluxo
```
Fontes (JSONL + SQLite)
→ Systemd-Timers (indexer/import/patterns)
→ Observabilidade-Sessions (sessions.db)
→ Express-API (/api/sessions)
→ Painel-Sessoes (lista + detalhe)
```
@@ -0,0 +1,30 @@
---
type: Relationship
layer: wiki
tags: [stack, dashdescomplicar, relacionamento, consome]
title: "Painéis → usa → Desk CRM"
---
# Painéis consomem Desk CRM
Múltiplos painéis consomem dados do Desk CRM (tarefas, projetos, tickets, horas).
## Painéis que consomem
- Painel-Dashboard — hero stats (tarefas, calendário)
- Painel-Operacoes — tickets, alta prioridade, SLA metrics
- Painel-Financeiro — dados de horas e projectos para cálculo de receita
## API
- Desk CRM API (gateway: `desk.descomplicar.pt:3150`)
- Endpoints: `/tasks`, `/projects`, `/tickets`, `/timesheets`
## Tipo de relação
`usa` — Painéis consomem dados de Desk CRM para exibição.
## Autenticação
- JWT / OAuth (conforme configuração Desk)
- Credenciais em `.env`
@@ -0,0 +1,35 @@
---
type: Relationship
layer: wiki
tags: [stack, dashdescomplicar, relacionamento, consome]
title: "Painéis → usa → MCP Gateway"
---
# Painéis consomem MCP Gateway
Painel-MCPs consome estado dos 39 MCPs através do MCP Gateway.
## Dados consumidos
- Estado online/offline/disabled de cada MCP
- Health check results
- Latência de resposta
- Última actualização
## Painel que consome
- Painel-MCPs — status agregados, lista detalhada
## API
- MCP Gateway: `gateway.descomplicar.pt:3151`
- Endpoint: `GET /health`, `GET /mcps`
## Tipo de relação
`usa` — Painel-MCPs consome estado dos MCPs.
## Frequência
- Polling a cada 30-60 segundos
- Refresh manual via botão "Actualizar"
@@ -0,0 +1,35 @@
---
type: Relationship
layer: wiki
tags: [stack, dashdescomplicar, relacionamento, consome]
title: "Painéis → usa → Moloni API"
---
# Painéis consomem Moloni API
Painel-Financeiro consome dados de faturação da Moloni.
## Dados consumidos
- Documentos (Faturas, Recibos)
- Clientes
- Produtos
- Estatísticas (Receita, Despesa por período)
## Painel que consome
- Painel-Financeiro — receita, despesa, gráficos de evolução
## API
- Moloni API v2 (MCP: `moloni-api`)
- Gateway: `gateway.descomplicar.pt:3158`
## Tipo de relação
`usa` — Painel-Financeiro consome dados de faturação da Moloni.
## Frequência
- Polling a cada 15-30 minutos (configurável)
- Cache local para performance
@@ -0,0 +1,38 @@
---
type: Relationship
layer: wiki
tags: [stack, dashdescomplicar, relacionamento, consome]
title: "Painéis → usa → SSH/Infraestrutura"
---
# Painéis consomem SSH/Infraestrutura
Painel-Monitor consome dados de infraestrutura via SSH2 e APIs Hetzner.
## Dados consumidos
- Load dos servidores (CPU, RAM, Disk)
- Estado de containers Docker
- Status de serviços
- Logs de erro / alertas
- Backup status
## Painel que consome
- Painel-Monitor — load, containers, serviços, backups
## Fontes
- **SSH2**: `ssh-unified` MCP (servidores Hetzner)
- `176.9.3.158` (CWP)
- `176.9.65.41` (EasyPanel)
- **Hetzner Cloud API**: infra.descomplicar.pt
- **WP-CLI**: WordPress sites via SSH
## Tipo de relação
`usa` — Painel-Monitor consome dados de infraestrutura.
## Frequência
- Polling contínuo (WebSocket ou short-poll a cada 30s)
@@ -0,0 +1,36 @@
---
type: Relationship
layer: wiki
tags: [stack, dashdescomplicar, relacionamento, systemd, observabilidade]
title: "Systemd-Timers-alimenta-Observabilidade"
---
# Systemd-Timers → alimenta → Observabilidade
**Origem:** [[Systemd-Timers]]
**Destino:** [[Observabilidade-Sessions]]
**Tipo:** `alimenta`
## Descrição
Os systemd timers executam scripts de indexação de sessões que alimentam a base de dados SQLite de observabilidade.
## Fluxo
```
Systemd-Timers
├── observabilidade-indexer (always-on) → Watcher JSONL
├── observabilidade-worklog-import (daily 03:00) → Import Desk worklogs
└── observabilidade-patterns (Sun 23:00) → Detecção padrões
↓
Observabilidade-Sessions (sessions.db)
↓
Express-API → Painel-Sessoes
```
## Volumes
- `~/.claude-work/sessions.db` (local) ↔ `/data/sessions.db` (container via bind mount)
- `~/.claude/projects/` → fonte JSONL
- `~/.hermes/state.db` → fonte Hermes
- `~/.local/share/opencode/opencode.db` → fonte OpenCode
+899 -625
View File
File diff suppressed because it is too large Load Diff

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