Compare commits
19
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4133d82319 | ||
|
|
f39c6212a3 | ||
|
|
61ae4697f9 | ||
|
|
f063a2ec8c | ||
|
|
ca9a07b1d9 | ||
|
|
2bbc12e8b4 | ||
|
|
948a9c2857 | ||
|
|
0e8a1f04ce | ||
|
|
810401e4e7 | ||
|
|
9b2dfdc412 | ||
|
|
f7acd99d8f | ||
|
|
7ac01d5a82 | ||
|
|
485a4ee67c | ||
|
|
769c63b2a8 | ||
|
|
08d2a31dc9 | ||
|
|
f733998945 | ||
|
|
9f3d14dc51 | ||
|
|
94db202de9 | ||
|
|
a594df1c7c |
@@ -5,3 +5,8 @@ dist/
|
|||||||
*.log
|
*.log
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.vite/
|
.vite/
|
||||||
|
|
||||||
|
# Artefactos gerados
|
||||||
|
graphify-out/
|
||||||
|
VALIDACAO-FASE-*.md
|
||||||
|
logs/
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|
||||||
|
---
|
||||||
@@ -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®
|
||||||
|
|||||||
@@ -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)
|
||||||
Executable
+132
@@ -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`
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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}`)
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 }> {
|
||||||
* Collect EasyPanel server metrics (CPU, RAM, disk) via API.
|
let up = 0, down = 0
|
||||||
* Replaces SSH-based collection for the Easy server.
|
const results = await Promise.allSettled(
|
||||||
*/
|
PING_SITES.map(async (site) => {
|
||||||
export async function collectEasyPanelMetrics(): Promise<boolean> {
|
const r = await checkSiteContent(site.url, site.marker)
|
||||||
const stats = await callEasyPanelAPI('monitor.getSystemStats')
|
await upsertMonitoring('site_uptime', site.host, r.status === 'up' ? 'ok' : 'failed', {
|
||||||
if (!stats) return false
|
url: site.url,
|
||||||
|
http_code: r.http_code,
|
||||||
const cpu = Math.round(stats.cpuInfo?.usedPercentage ?? 0)
|
reason: r.reason,
|
||||||
const ram = Math.round((stats.memInfo?.usedMemPercentage ?? 0) * 10) / 10
|
checked_by: 'dashboard-active',
|
||||||
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,
|
|
||||||
})
|
})
|
||||||
|
return r
|
||||||
console.log(`[EASYPANEL] Server: CPU=${cpu}%, RAM=${ram}%, Disk=${disk}%`)
|
}),
|
||||||
return true
|
)
|
||||||
|
for (const r of results) {
|
||||||
|
if (r.status === 'fulfilled' && r.value.status === 'up') up++
|
||||||
|
else down++
|
||||||
|
}
|
||||||
|
return { checked: PING_SITES.length, up, down }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Collect Docker container/task stats via EasyPanel API.
|
* Collect EasyPanel server metrics + container stats via SSH.
|
||||||
* Updates the 'container' category in monitoring DB.
|
* 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> {
|
||||||
|
const { collectSSHMetrics } = await import('./server-metrics.js')
|
||||||
|
const result = await collectSSHMetrics()
|
||||||
|
return result.success > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collect Docker Swarm service status via SSH to EasyPanel server.
|
||||||
|
* 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)
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
}
|
||||||
@@ -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 }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
}
|
||||||
@@ -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],
|
||||||
|
|||||||
@@ -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}`,
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -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',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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: [],
|
||||||
|
|||||||
@@ -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[]
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
type: Brainstorm
|
||||||
|
title: 'Brainstorm — DashDescomplicar'
|
||||||
|
timestamp: 2026-06-30
|
||||||
|
layer: wiki
|
||||||
|
---
|
||||||
|
|
||||||
|
# BRAINSTORM — DashDescomplicar
|
||||||
|
|
||||||
|
Estrutura padrão aplicada.
|
||||||
@@ -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)
|
||||||
+9
-6
@@ -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).
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|
||||||
+3
@@ -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
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
type: Spec
|
||||||
|
title: 'SPEC — DashDescomplicar'
|
||||||
|
status: draft
|
||||||
|
timestamp: 2026-06-30
|
||||||
|
layer: wiki
|
||||||
|
---
|
||||||
|
|
||||||
|
# SPEC — DashDescomplicar
|
||||||
|
|
||||||
|
Projecto em estruturação.
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
type: Note
|
||||||
|
title: 'Pesquisa — DashDescomplicar'
|
||||||
|
timestamp: 2026-06-30
|
||||||
|
layer: wiki
|
||||||
|
---
|
||||||
|
|
||||||
|
# Pesquisa
|
||||||
|
|
||||||
|
Documentar pesquisa aqui.
|
||||||
@@ -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
|
||||||
@@ -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*
|
||||||
@@ -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"
|
||||||
@@ -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()
|
||||||
@@ -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()
|
||||||
@@ -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
@@ -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)**
|
||||||
@@ -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)
|
||||||
@@ -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`
|
||||||
@@ -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
|
||||||
@@ -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`)
|
||||||
@@ -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`
|
||||||
@@ -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`
|
||||||
@@ -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`
|
||||||
@@ -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`
|
||||||
@@ -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`
|
||||||
@@ -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`
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
@@ -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
@@ -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
|
||||||
Generated
+899
-625
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
Reference in New Issue
Block a user