Versão inicial do Agents Orchestrator
Painel administrativo web para orquestração de agentes Claude Code com suporte a execução de tarefas, agendamento cron, pipelines sequenciais e terminal com streaming em tempo real via WebSocket.
This commit is contained in:
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
node_modules/
|
||||||
|
data/
|
||||||
|
.env
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
70
CLAUDE.md
Normal file
70
CLAUDE.md
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Sobre o Projeto
|
||||||
|
|
||||||
|
Painel administrativo web para orquestração de agentes Claude Code. Permite criar, configurar e executar agentes que invocam o CLI `claude` como subprocesso, com suporte a agendamento via cron e pipelines sequenciais (saída de um agente alimenta o próximo).
|
||||||
|
|
||||||
|
## Comandos
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm start # Inicia o servidor (porta 3000)
|
||||||
|
npm run dev # Inicia com --watch (hot reload automático)
|
||||||
|
```
|
||||||
|
|
||||||
|
Não há testes, linting ou build configurados.
|
||||||
|
|
||||||
|
## Arquitetura
|
||||||
|
|
||||||
|
### Backend (Node.js + Express, ESM)
|
||||||
|
|
||||||
|
```
|
||||||
|
server.js → HTTP + WebSocket (ws) na mesma porta
|
||||||
|
src/routes/api.js → Todas as rotas REST sob /api
|
||||||
|
src/agents/manager.js → CRUD de agentes + orquestra execuções e agendamentos
|
||||||
|
src/agents/executor.js → Spawna o CLI claude como child_process com stream-json
|
||||||
|
src/agents/scheduler.js → Agendamento cron via node-cron (in-memory)
|
||||||
|
src/agents/pipeline.js → Execução sequencial de steps, cada um delegando ao executor
|
||||||
|
src/store/db.js → Persistência em arquivos JSON (data/*.json)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Fluxo de execução:** API recebe POST → `manager.executeTask()` → `executor.execute()` spawna `/home/fred/.local/bin/claude` com `--output-format stream-json` → stdout é parseado linha a linha → chunks são enviados via WebSocket broadcast para o frontend.
|
||||||
|
|
||||||
|
**Pipelines:** Executam steps em sequência. Cada step usa um agente diferente. A saída de um step é passada como input do próximo via template `{{input}}`.
|
||||||
|
|
||||||
|
**Persistência:** `db.js` expõe stores (agents, tasks, pipelines) que leem/escrevem JSON em `data/`. Cada operação recarrega o arquivo inteiro. Agendamentos cron são apenas in-memory.
|
||||||
|
|
||||||
|
### Frontend (Vanilla JS, SPA)
|
||||||
|
|
||||||
|
```
|
||||||
|
public/index.html → SPA single-page com todas as seções
|
||||||
|
public/css/styles.css → Estilos (Inter + JetBrains Mono, Lucide icons)
|
||||||
|
public/js/app.js → Controlador principal, navegação, WebSocket client
|
||||||
|
public/js/api.js → Client HTTP para /api/*
|
||||||
|
public/js/components/*.js → UI por seção (dashboard, agents, tasks, schedules, pipelines, terminal, modal, toast)
|
||||||
|
```
|
||||||
|
|
||||||
|
O frontend usa objetos globais no `window` (App, API, DashboardUI, AgentsUI, etc.) sem bundler ou framework. WebSocket reconecta automaticamente com backoff exponencial.
|
||||||
|
|
||||||
|
### Endpoints REST
|
||||||
|
|
||||||
|
| Recurso | Rotas |
|
||||||
|
|---------|-------|
|
||||||
|
| Agentes | GET/POST `/api/agents`, GET/PUT/DELETE `/api/agents/:id`, POST `.../execute`, POST `.../cancel/:executionId`, GET `.../export` |
|
||||||
|
| Tarefas | GET/POST `/api/tasks`, PUT/DELETE `/api/tasks/:id` |
|
||||||
|
| Agendamentos | GET/POST `/api/schedules`, DELETE `/api/schedules/:taskId` |
|
||||||
|
| Pipelines | GET/POST `/api/pipelines`, GET/PUT/DELETE `/api/pipelines/:id`, POST `.../execute`, POST `.../cancel` |
|
||||||
|
| Sistema | GET `/api/system/status`, GET `/api/executions/active` |
|
||||||
|
|
||||||
|
### WebSocket Events
|
||||||
|
|
||||||
|
O servidor envia eventos tipados (`execution_output`, `execution_complete`, `execution_error`, `pipeline_step_start`, `pipeline_step_complete`, `pipeline_complete`, `pipeline_error`) que o frontend renderiza no terminal.
|
||||||
|
|
||||||
|
## Convenções
|
||||||
|
|
||||||
|
- Todo o código e mensagens em português brasileiro
|
||||||
|
- ESM (`"type": "module"` no package.json) — usar `import`/`export`, não `require`
|
||||||
|
- Sem TypeScript, sem bundler, sem framework frontend
|
||||||
|
- IDs gerados com `uuid` v4
|
||||||
|
- Modelo padrão dos agentes: `claude-sonnet-4-6`
|
||||||
148
README.md
Normal file
148
README.md
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
# Agents Orchestrator
|
||||||
|
|
||||||
|
Painel administrativo web para orquestração de agentes [Claude Code](https://docs.anthropic.com/en/docs/claude-code). Crie, configure e execute múltiplos agentes com diferentes personalidades, modelos e diretórios de trabalho — tudo a partir de uma interface visual.
|
||||||
|
|
||||||
|
## Funcionalidades
|
||||||
|
|
||||||
|
- **Gerenciamento de agentes** — Crie agentes com nome, system prompt, modelo (Sonnet/Opus/Haiku), diretório de trabalho e tags. Ative, desative, edite ou exclua a qualquer momento.
|
||||||
|
- **Execução de tarefas** — Execute tarefas sob demanda em qualquer agente ativo. Templates rápidos incluídos (detecção de bugs, revisão OWASP, refatoração, testes, documentação, performance).
|
||||||
|
- **Terminal em tempo real** — Acompanhe a saída dos agentes via WebSocket com streaming chunk-a-chunk. Indicador de status de conexão e filtro por execução.
|
||||||
|
- **Agendamento cron** — Agende tarefas recorrentes com expressões cron. Presets incluídos (horário, diário, semanal, mensal).
|
||||||
|
- **Pipelines** — Encadeie múltiplos agentes em fluxos sequenciais. A saída de cada passo alimenta o próximo via template `{{input}}`. Ideal para fluxos como "analisar → corrigir → testar".
|
||||||
|
- **Dashboard** — Visão geral com métricas (agentes, execuções ativas, agendamentos), atividade recente e status do sistema.
|
||||||
|
- **Exportação** — Exporte a configuração completa de qualquer agente em JSON.
|
||||||
|
|
||||||
|
## Pré-requisitos
|
||||||
|
|
||||||
|
- **Node.js** 18+
|
||||||
|
- **Claude Code CLI** instalado e autenticado (`claude` disponível no PATH)
|
||||||
|
|
||||||
|
## Instalação
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone <repo-url>
|
||||||
|
cd agents-orchestrator
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Uso
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Produção
|
||||||
|
npm start
|
||||||
|
|
||||||
|
# Desenvolvimento (hot reload)
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Acesse **http://localhost:3000** no navegador. A porta pode ser alterada via variável de ambiente `PORT`.
|
||||||
|
|
||||||
|
## Como funciona
|
||||||
|
|
||||||
|
### Criando um agente
|
||||||
|
|
||||||
|
1. Clique em **Novo Agente** no header ou na seção Agentes
|
||||||
|
2. Configure nome, system prompt, modelo e diretório de trabalho
|
||||||
|
3. Salve — o agente aparecerá como card na listagem
|
||||||
|
|
||||||
|
### Executando uma tarefa
|
||||||
|
|
||||||
|
1. No card do agente, clique em **Executar**
|
||||||
|
2. Descreva a tarefa ou use um template rápido
|
||||||
|
3. Opcionalmente adicione instruções extras
|
||||||
|
4. A execução inicia e o terminal abre automaticamente com streaming da saída
|
||||||
|
|
||||||
|
### Criando um pipeline
|
||||||
|
|
||||||
|
1. Vá em **Pipelines** → **Novo Pipeline**
|
||||||
|
2. Adicione pelo menos 2 passos, selecionando um agente para cada
|
||||||
|
3. Opcionalmente defina um template de input usando `{{input}}` para referenciar a saída do passo anterior
|
||||||
|
4. Execute o pipeline fornecendo o input inicial
|
||||||
|
|
||||||
|
### Agendando uma tarefa
|
||||||
|
|
||||||
|
1. Vá em **Agendamentos** → **Novo Agendamento**
|
||||||
|
2. Selecione o agente, descreva a tarefa e defina a expressão cron
|
||||||
|
3. A tarefa será executada automaticamente nos horários configurados
|
||||||
|
|
||||||
|
## Arquitetura
|
||||||
|
|
||||||
|
```
|
||||||
|
server.js Express + WebSocket na mesma porta
|
||||||
|
src/
|
||||||
|
routes/api.js API REST (/api/*)
|
||||||
|
agents/
|
||||||
|
manager.js CRUD + orquestração de agentes
|
||||||
|
executor.js Spawna o CLI claude como child_process
|
||||||
|
scheduler.js Agendamento cron (in-memory)
|
||||||
|
pipeline.js Execução sequencial de steps
|
||||||
|
store/db.js Persistência em JSON (data/*.json)
|
||||||
|
public/
|
||||||
|
index.html SPA single-page
|
||||||
|
css/styles.css Estilos (Inter, JetBrains Mono, Lucide)
|
||||||
|
js/
|
||||||
|
app.js Controlador principal + WebSocket client
|
||||||
|
api.js Client HTTP para a API
|
||||||
|
components/ UI por seção (dashboard, agents, tasks, etc.)
|
||||||
|
data/
|
||||||
|
agents.json Agentes cadastrados
|
||||||
|
tasks.json Templates de tarefas
|
||||||
|
pipelines.json Pipelines configurados
|
||||||
|
```
|
||||||
|
|
||||||
|
O executor invoca o binário `claude` com `--output-format stream-json`, parseia o stdout linha a linha e transmite os chunks via WebSocket para o frontend em tempo real.
|
||||||
|
|
||||||
|
## API REST
|
||||||
|
|
||||||
|
| Método | Endpoint | Descrição |
|
||||||
|
|--------|----------|-----------|
|
||||||
|
| `GET` | `/api/agents` | Listar agentes |
|
||||||
|
| `POST` | `/api/agents` | Criar agente |
|
||||||
|
| `GET` | `/api/agents/:id` | Obter agente |
|
||||||
|
| `PUT` | `/api/agents/:id` | Atualizar agente |
|
||||||
|
| `DELETE` | `/api/agents/:id` | Excluir agente |
|
||||||
|
| `POST` | `/api/agents/:id/execute` | Executar tarefa no agente |
|
||||||
|
| `POST` | `/api/agents/:id/cancel/:executionId` | Cancelar execução |
|
||||||
|
| `GET` | `/api/agents/:id/export` | Exportar agente (JSON) |
|
||||||
|
| `GET` | `/api/tasks` | Listar tarefas |
|
||||||
|
| `POST` | `/api/tasks` | Criar tarefa |
|
||||||
|
| `PUT` | `/api/tasks/:id` | Atualizar tarefa |
|
||||||
|
| `DELETE` | `/api/tasks/:id` | Excluir tarefa |
|
||||||
|
| `GET` | `/api/schedules` | Listar agendamentos |
|
||||||
|
| `POST` | `/api/schedules` | Criar agendamento |
|
||||||
|
| `DELETE` | `/api/schedules/:taskId` | Remover agendamento |
|
||||||
|
| `GET` | `/api/pipelines` | Listar pipelines |
|
||||||
|
| `POST` | `/api/pipelines` | Criar pipeline |
|
||||||
|
| `GET` | `/api/pipelines/:id` | Obter pipeline |
|
||||||
|
| `PUT` | `/api/pipelines/:id` | Atualizar pipeline |
|
||||||
|
| `DELETE` | `/api/pipelines/:id` | Excluir pipeline |
|
||||||
|
| `POST` | `/api/pipelines/:id/execute` | Executar pipeline |
|
||||||
|
| `POST` | `/api/pipelines/:id/cancel` | Cancelar pipeline |
|
||||||
|
| `GET` | `/api/system/status` | Status geral do sistema |
|
||||||
|
| `GET` | `/api/executions/active` | Execuções em andamento |
|
||||||
|
|
||||||
|
## Eventos WebSocket
|
||||||
|
|
||||||
|
O servidor envia eventos tipados via WebSocket que o frontend renderiza no terminal:
|
||||||
|
|
||||||
|
| Evento | Descrição |
|
||||||
|
|--------|-----------|
|
||||||
|
| `execution_output` | Chunk de texto da saída do agente |
|
||||||
|
| `execution_complete` | Execução finalizada com resultado |
|
||||||
|
| `execution_error` | Erro durante execução |
|
||||||
|
| `pipeline_step_start` | Início de um passo do pipeline |
|
||||||
|
| `pipeline_step_complete` | Passo do pipeline concluído |
|
||||||
|
| `pipeline_complete` | Pipeline finalizado |
|
||||||
|
| `pipeline_error` | Erro em um passo do pipeline |
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
- **Backend**: Node.js, Express, WebSocket (ws), node-cron, uuid
|
||||||
|
- **Frontend**: HTML, CSS, JavaScript vanilla (sem framework, sem bundler)
|
||||||
|
- **Ícones**: Lucide
|
||||||
|
- **Fontes**: Inter (UI), JetBrains Mono (código/terminal)
|
||||||
|
- **Persistência**: Arquivos JSON em disco
|
||||||
|
|
||||||
|
## Licença
|
||||||
|
|
||||||
|
MIT
|
||||||
885
package-lock.json
generated
Normal file
885
package-lock.json
generated
Normal file
@@ -0,0 +1,885 @@
|
|||||||
|
{
|
||||||
|
"name": "agents-orchestrator",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "agents-orchestrator",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"dependencies": {
|
||||||
|
"express": "^4.21.0",
|
||||||
|
"node-cron": "^3.0.3",
|
||||||
|
"uuid": "^10.0.0",
|
||||||
|
"ws": "^8.18.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/accepts": {
|
||||||
|
"version": "1.3.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||||
|
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-types": "~2.1.34",
|
||||||
|
"negotiator": "0.6.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/array-flatten": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/body-parser": {
|
||||||
|
"version": "1.20.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
|
||||||
|
"integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bytes": "~3.1.2",
|
||||||
|
"content-type": "~1.0.5",
|
||||||
|
"debug": "2.6.9",
|
||||||
|
"depd": "2.0.0",
|
||||||
|
"destroy": "~1.2.0",
|
||||||
|
"http-errors": "~2.0.1",
|
||||||
|
"iconv-lite": "~0.4.24",
|
||||||
|
"on-finished": "~2.4.1",
|
||||||
|
"qs": "~6.14.0",
|
||||||
|
"raw-body": "~2.5.3",
|
||||||
|
"type-is": "~1.6.18",
|
||||||
|
"unpipe": "~1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8",
|
||||||
|
"npm": "1.2.8000 || >= 1.4.16"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/bytes": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/call-bind-apply-helpers": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"function-bind": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/call-bound": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
|
"get-intrinsic": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/content-disposition": {
|
||||||
|
"version": "0.5.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
||||||
|
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": "5.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/content-type": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cookie": {
|
||||||
|
"version": "0.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||||
|
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cookie-signature": {
|
||||||
|
"version": "1.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
|
||||||
|
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/debug": {
|
||||||
|
"version": "2.6.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||||
|
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/depd": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/destroy": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8",
|
||||||
|
"npm": "1.2.8000 || >= 1.4.16"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/dunder-proto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.1",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"gopd": "^1.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ee-first": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/encodeurl": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-define-property": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-errors": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/es-object-atoms": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/escape-html": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/etag": {
|
||||||
|
"version": "1.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
||||||
|
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/express": {
|
||||||
|
"version": "4.22.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
|
||||||
|
"integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"accepts": "~1.3.8",
|
||||||
|
"array-flatten": "1.1.1",
|
||||||
|
"body-parser": "~1.20.3",
|
||||||
|
"content-disposition": "~0.5.4",
|
||||||
|
"content-type": "~1.0.4",
|
||||||
|
"cookie": "~0.7.1",
|
||||||
|
"cookie-signature": "~1.0.6",
|
||||||
|
"debug": "2.6.9",
|
||||||
|
"depd": "2.0.0",
|
||||||
|
"encodeurl": "~2.0.0",
|
||||||
|
"escape-html": "~1.0.3",
|
||||||
|
"etag": "~1.8.1",
|
||||||
|
"finalhandler": "~1.3.1",
|
||||||
|
"fresh": "~0.5.2",
|
||||||
|
"http-errors": "~2.0.0",
|
||||||
|
"merge-descriptors": "1.0.3",
|
||||||
|
"methods": "~1.1.2",
|
||||||
|
"on-finished": "~2.4.1",
|
||||||
|
"parseurl": "~1.3.3",
|
||||||
|
"path-to-regexp": "~0.1.12",
|
||||||
|
"proxy-addr": "~2.0.7",
|
||||||
|
"qs": "~6.14.0",
|
||||||
|
"range-parser": "~1.2.1",
|
||||||
|
"safe-buffer": "5.2.1",
|
||||||
|
"send": "~0.19.0",
|
||||||
|
"serve-static": "~1.16.2",
|
||||||
|
"setprototypeof": "1.2.0",
|
||||||
|
"statuses": "~2.0.1",
|
||||||
|
"type-is": "~1.6.18",
|
||||||
|
"utils-merge": "1.0.1",
|
||||||
|
"vary": "~1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/finalhandler": {
|
||||||
|
"version": "1.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
|
||||||
|
"integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "2.6.9",
|
||||||
|
"encodeurl": "~2.0.0",
|
||||||
|
"escape-html": "~1.0.3",
|
||||||
|
"on-finished": "~2.4.1",
|
||||||
|
"parseurl": "~1.3.3",
|
||||||
|
"statuses": "~2.0.2",
|
||||||
|
"unpipe": "~1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/forwarded": {
|
||||||
|
"version": "0.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||||
|
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fresh": {
|
||||||
|
"version": "0.5.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
||||||
|
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/function-bind": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-intrinsic": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bind-apply-helpers": "^1.0.2",
|
||||||
|
"es-define-property": "^1.0.1",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"es-object-atoms": "^1.1.1",
|
||||||
|
"function-bind": "^1.1.2",
|
||||||
|
"get-proto": "^1.0.1",
|
||||||
|
"gopd": "^1.2.0",
|
||||||
|
"has-symbols": "^1.1.0",
|
||||||
|
"hasown": "^2.0.2",
|
||||||
|
"math-intrinsics": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/get-proto": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"dunder-proto": "^1.0.1",
|
||||||
|
"es-object-atoms": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/gopd": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/has-symbols": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/hasown": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"function-bind": "^1.1.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/http-errors": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"depd": "~2.0.0",
|
||||||
|
"inherits": "~2.0.4",
|
||||||
|
"setprototypeof": "~1.2.0",
|
||||||
|
"statuses": "~2.0.2",
|
||||||
|
"toidentifier": "~1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/iconv-lite": {
|
||||||
|
"version": "0.4.24",
|
||||||
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||||
|
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safer-buffer": ">= 2.1.2 < 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/inherits": {
|
||||||
|
"version": "2.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||||
|
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/ipaddr.js": {
|
||||||
|
"version": "1.9.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||||
|
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/math-intrinsics": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/media-typer": {
|
||||||
|
"version": "0.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||||
|
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/merge-descriptors": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/methods": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mime": {
|
||||||
|
"version": "1.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
||||||
|
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"mime": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mime-db": {
|
||||||
|
"version": "1.52.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
|
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mime-types": {
|
||||||
|
"version": "2.1.35",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||||
|
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": "1.52.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ms": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/negotiator": {
|
||||||
|
"version": "0.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
||||||
|
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/node-cron": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"uuid": "8.3.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/node-cron/node_modules/uuid": {
|
||||||
|
"version": "8.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||||
|
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"uuid": "dist/bin/uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/object-inspect": {
|
||||||
|
"version": "1.13.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||||
|
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/on-finished": {
|
||||||
|
"version": "2.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||||
|
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ee-first": "1.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/parseurl": {
|
||||||
|
"version": "1.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||||
|
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/path-to-regexp": {
|
||||||
|
"version": "0.1.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
|
||||||
|
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/proxy-addr": {
|
||||||
|
"version": "2.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||||
|
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"forwarded": "0.2.0",
|
||||||
|
"ipaddr.js": "1.9.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/qs": {
|
||||||
|
"version": "6.14.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
|
||||||
|
"integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"side-channel": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/range-parser": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/raw-body": {
|
||||||
|
"version": "2.5.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
|
||||||
|
"integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"bytes": "~3.1.2",
|
||||||
|
"http-errors": "~2.0.1",
|
||||||
|
"iconv-lite": "~0.4.24",
|
||||||
|
"unpipe": "~1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/safe-buffer": {
|
||||||
|
"version": "5.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||||
|
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "patreon",
|
||||||
|
"url": "https://www.patreon.com/feross"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "consulting",
|
||||||
|
"url": "https://feross.org/support"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/safer-buffer": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/send": {
|
||||||
|
"version": "0.19.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
|
||||||
|
"integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"debug": "2.6.9",
|
||||||
|
"depd": "2.0.0",
|
||||||
|
"destroy": "1.2.0",
|
||||||
|
"encodeurl": "~2.0.0",
|
||||||
|
"escape-html": "~1.0.3",
|
||||||
|
"etag": "~1.8.1",
|
||||||
|
"fresh": "~0.5.2",
|
||||||
|
"http-errors": "~2.0.1",
|
||||||
|
"mime": "1.6.0",
|
||||||
|
"ms": "2.1.3",
|
||||||
|
"on-finished": "~2.4.1",
|
||||||
|
"range-parser": "~1.2.1",
|
||||||
|
"statuses": "~2.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/send/node_modules/ms": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/serve-static": {
|
||||||
|
"version": "1.16.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
|
||||||
|
"integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"encodeurl": "~2.0.0",
|
||||||
|
"escape-html": "~1.0.3",
|
||||||
|
"parseurl": "~1.3.3",
|
||||||
|
"send": "~0.19.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/setprototypeof": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
|
"node_modules/side-channel": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"object-inspect": "^1.13.3",
|
||||||
|
"side-channel-list": "^1.0.0",
|
||||||
|
"side-channel-map": "^1.0.1",
|
||||||
|
"side-channel-weakmap": "^1.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/side-channel-list": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"object-inspect": "^1.13.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/side-channel-map": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bound": "^1.0.2",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"get-intrinsic": "^1.2.5",
|
||||||
|
"object-inspect": "^1.13.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/side-channel-weakmap": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"call-bound": "^1.0.2",
|
||||||
|
"es-errors": "^1.3.0",
|
||||||
|
"get-intrinsic": "^1.2.5",
|
||||||
|
"object-inspect": "^1.13.3",
|
||||||
|
"side-channel-map": "^1.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/statuses": {
|
||||||
|
"version": "2.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||||
|
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/toidentifier": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/type-is": {
|
||||||
|
"version": "1.6.18",
|
||||||
|
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
||||||
|
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"media-typer": "0.3.0",
|
||||||
|
"mime-types": "~2.1.24"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/unpipe": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/utils-merge": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/uuid": {
|
||||||
|
"version": "10.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
|
||||||
|
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/sponsors/broofa",
|
||||||
|
"https://github.com/sponsors/ctavan"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"uuid": "dist/bin/uuid"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/vary": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.19.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
|
||||||
|
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
package.json
Normal file
17
package.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "agents-orchestrator",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Painel administrativo para orquestração de agentes Claude Code",
|
||||||
|
"main": "server.js",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"start": "node server.js",
|
||||||
|
"dev": "node --watch server.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"express": "^4.21.0",
|
||||||
|
"ws": "^8.18.0",
|
||||||
|
"node-cron": "^3.0.3",
|
||||||
|
"uuid": "^10.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
3279
public/css/styles.css
Normal file
3279
public/css/styles.css
Normal file
File diff suppressed because it is too large
Load Diff
873
public/index.html
Normal file
873
public/index.html
Normal file
@@ -0,0 +1,873 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="pt-BR">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Agents Orchestrator</title>
|
||||||
|
<link rel="stylesheet" href="css/styles.css" />
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div class="layout">
|
||||||
|
|
||||||
|
<aside class="sidebar">
|
||||||
|
<div class="sidebar-logo">
|
||||||
|
<div class="sidebar-logo-icon">
|
||||||
|
<i data-lucide="bot"></i>
|
||||||
|
</div>
|
||||||
|
<span class="sidebar-logo-text">Agents Orchestrator</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="sidebar-nav">
|
||||||
|
<ul class="sidebar-nav-list">
|
||||||
|
<li class="sidebar-nav-item">
|
||||||
|
<a href="#" class="sidebar-nav-link" data-section="dashboard">
|
||||||
|
<i data-lucide="layout-dashboard"></i>
|
||||||
|
<span>Dashboard</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="sidebar-nav-item">
|
||||||
|
<a href="#" class="sidebar-nav-link" data-section="agents">
|
||||||
|
<i data-lucide="cpu"></i>
|
||||||
|
<span>Agentes</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="sidebar-nav-item">
|
||||||
|
<a href="#" class="sidebar-nav-link" data-section="tasks">
|
||||||
|
<i data-lucide="list-checks"></i>
|
||||||
|
<span>Tarefas</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="sidebar-nav-item">
|
||||||
|
<a href="#" class="sidebar-nav-link" data-section="schedules">
|
||||||
|
<i data-lucide="clock"></i>
|
||||||
|
<span>Agendamentos</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="sidebar-nav-item" data-section="pipelines">
|
||||||
|
<a class="sidebar-nav-link" href="#" data-section="pipelines">
|
||||||
|
<i data-lucide="git-merge" class="nav-icon"></i>
|
||||||
|
<span>Pipelines</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="sidebar-nav-item">
|
||||||
|
<a href="#" class="sidebar-nav-link" data-section="terminal">
|
||||||
|
<i data-lucide="terminal"></i>
|
||||||
|
<span>Terminal</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="sidebar-nav-item">
|
||||||
|
<a href="#" class="sidebar-nav-link" data-section="settings">
|
||||||
|
<i data-lucide="settings"></i>
|
||||||
|
<span>Configurações</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="sidebar-footer">
|
||||||
|
<div class="ws-status" id="ws-status">
|
||||||
|
<span class="ws-indicator ws-indicator--disconnected" id="ws-indicator"></span>
|
||||||
|
<span class="ws-label" id="ws-label">Desconectado</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<div class="main-wrapper">
|
||||||
|
<header class="main-header">
|
||||||
|
<div class="header-breadcrumb">
|
||||||
|
<h1 class="header-title" id="header-title">Dashboard</h1>
|
||||||
|
</div>
|
||||||
|
<div class="header-actions">
|
||||||
|
<div class="header-badge" id="active-executions-badge" aria-label="Execuções ativas">
|
||||||
|
<i data-lucide="activity"></i>
|
||||||
|
<span id="active-executions-count">0</span>
|
||||||
|
<span class="header-badge-label">ativas</span>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn--ghost btn--icon-text" id="system-status-btn" type="button" aria-label="Status do sistema">
|
||||||
|
<i data-lucide="server"></i>
|
||||||
|
<span>Sistema</span>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn--primary btn--icon-text" id="new-agent-btn" type="button">
|
||||||
|
<i data-lucide="plus"></i>
|
||||||
|
<span>Novo Agente</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="main-content">
|
||||||
|
|
||||||
|
<section id="dashboard" class="section active" aria-label="Dashboard">
|
||||||
|
<div class="metrics-grid">
|
||||||
|
<article class="metric-card">
|
||||||
|
<div class="metric-card-icon metric-card-icon--indigo">
|
||||||
|
<i data-lucide="cpu"></i>
|
||||||
|
</div>
|
||||||
|
<div class="metric-card-body">
|
||||||
|
<span class="metric-card-label">Total de Agentes</span>
|
||||||
|
<span class="metric-card-value" id="metric-total-agents">0</span>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
<article class="metric-card">
|
||||||
|
<div class="metric-card-icon metric-card-icon--green">
|
||||||
|
<i data-lucide="zap"></i>
|
||||||
|
</div>
|
||||||
|
<div class="metric-card-body">
|
||||||
|
<span class="metric-card-label">Agentes Ativos</span>
|
||||||
|
<span class="metric-card-value" id="metric-active-agents">0</span>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
<article class="metric-card">
|
||||||
|
<div class="metric-card-icon metric-card-icon--purple">
|
||||||
|
<i data-lucide="play-circle"></i>
|
||||||
|
</div>
|
||||||
|
<div class="metric-card-body">
|
||||||
|
<span class="metric-card-label">Execuções Hoje</span>
|
||||||
|
<span class="metric-card-value" id="metric-executions-today">0</span>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
<article class="metric-card">
|
||||||
|
<div class="metric-card-icon metric-card-icon--orange">
|
||||||
|
<i data-lucide="clock"></i>
|
||||||
|
</div>
|
||||||
|
<div class="metric-card-body">
|
||||||
|
<span class="metric-card-label">Agendamentos</span>
|
||||||
|
<span class="metric-card-value" id="metric-schedules">0</span>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="dashboard-grid">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2 class="card-title">Atividade Recente</h2>
|
||||||
|
<button class="btn btn--ghost btn--sm" type="button" id="refresh-activity-btn">
|
||||||
|
<i data-lucide="refresh-cw"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<ul class="activity-list" id="activity-list">
|
||||||
|
<li class="activity-empty">
|
||||||
|
<i data-lucide="inbox"></i>
|
||||||
|
<span>Nenhuma execução recente</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2 class="card-title">Status do Sistema</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<ul class="system-status-list" id="system-status-list">
|
||||||
|
<li class="system-status-item">
|
||||||
|
<div class="system-status-info">
|
||||||
|
<i data-lucide="server"></i>
|
||||||
|
<span>Servidor HTTP</span>
|
||||||
|
</div>
|
||||||
|
<span class="badge badge--green">Online</span>
|
||||||
|
</li>
|
||||||
|
<li class="system-status-item">
|
||||||
|
<div class="system-status-info">
|
||||||
|
<i data-lucide="radio"></i>
|
||||||
|
<span>WebSocket</span>
|
||||||
|
</div>
|
||||||
|
<span class="badge badge--red" id="system-ws-status-badge">Desconectado</span>
|
||||||
|
</li>
|
||||||
|
<li class="system-status-item">
|
||||||
|
<div class="system-status-info">
|
||||||
|
<i data-lucide="database"></i>
|
||||||
|
<span>Armazenamento</span>
|
||||||
|
</div>
|
||||||
|
<span class="badge badge--green">OK</span>
|
||||||
|
</li>
|
||||||
|
<li class="system-status-item">
|
||||||
|
<div class="system-status-info">
|
||||||
|
<i data-lucide="terminal"></i>
|
||||||
|
<span>Claude CLI</span>
|
||||||
|
</div>
|
||||||
|
<span class="badge badge--gray" id="system-claude-status-badge">Verificando...</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="agents" class="section" aria-label="Agentes" hidden>
|
||||||
|
<div class="section-toolbar">
|
||||||
|
<div class="search-field">
|
||||||
|
<i data-lucide="search"></i>
|
||||||
|
<input type="search" placeholder="Buscar agentes..." id="agents-search" aria-label="Buscar agentes" />
|
||||||
|
</div>
|
||||||
|
<div class="section-toolbar-actions">
|
||||||
|
<select class="select" id="agents-filter-status" aria-label="Filtrar por status">
|
||||||
|
<option value="">Todos os status</option>
|
||||||
|
<option value="active">Ativo</option>
|
||||||
|
<option value="inactive">Inativo</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="agents-grid" id="agents-grid">
|
||||||
|
<div class="empty-state" id="agents-empty-state">
|
||||||
|
<div class="empty-state-icon">
|
||||||
|
<i data-lucide="bot"></i>
|
||||||
|
</div>
|
||||||
|
<h3 class="empty-state-title">Nenhum agente cadastrado</h3>
|
||||||
|
<p class="empty-state-desc">Crie seu primeiro agente para começar a orquestrar tarefas com Claude.</p>
|
||||||
|
<button class="btn btn--primary btn--icon-text" type="button" id="agents-empty-new-btn">
|
||||||
|
<i data-lucide="plus"></i>
|
||||||
|
<span>Criar Agente</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="tasks" class="section" aria-label="Tarefas" hidden>
|
||||||
|
<div class="section-toolbar">
|
||||||
|
<div class="search-field">
|
||||||
|
<i data-lucide="search"></i>
|
||||||
|
<input type="search" placeholder="Buscar tarefas..." id="tasks-search" aria-label="Buscar tarefas" />
|
||||||
|
</div>
|
||||||
|
<div class="section-toolbar-actions">
|
||||||
|
<select class="select" id="tasks-filter-category" aria-label="Filtrar por categoria">
|
||||||
|
<option value="">Todas as categorias</option>
|
||||||
|
<option value="code-review">Code Review</option>
|
||||||
|
<option value="security">Segurança</option>
|
||||||
|
<option value="refactor">Refatoração</option>
|
||||||
|
<option value="tests">Testes</option>
|
||||||
|
<option value="docs">Documentação</option>
|
||||||
|
<option value="performance">Performance</option>
|
||||||
|
</select>
|
||||||
|
<button class="btn btn--primary btn--icon-text" type="button" id="tasks-new-btn">
|
||||||
|
<i data-lucide="plus"></i>
|
||||||
|
<span>Nova Tarefa</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tasks-grid" id="tasks-grid">
|
||||||
|
<div class="empty-state" id="tasks-empty-state">
|
||||||
|
<div class="empty-state-icon">
|
||||||
|
<i data-lucide="list-checks"></i>
|
||||||
|
</div>
|
||||||
|
<h3 class="empty-state-title">Nenhuma tarefa cadastrada</h3>
|
||||||
|
<p class="empty-state-desc">Crie templates de tarefas para reutilizar em diferentes agentes.</p>
|
||||||
|
<button class="btn btn--primary btn--icon-text" type="button" id="tasks-empty-new-btn">
|
||||||
|
<i data-lucide="plus"></i>
|
||||||
|
<span>Criar Tarefa</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="schedules" class="section" aria-label="Agendamentos" hidden>
|
||||||
|
<div class="section-toolbar">
|
||||||
|
<div class="search-field">
|
||||||
|
<i data-lucide="search"></i>
|
||||||
|
<input type="search" placeholder="Buscar agendamentos..." id="schedules-search" aria-label="Buscar agendamentos" />
|
||||||
|
</div>
|
||||||
|
<div class="section-toolbar-actions">
|
||||||
|
<select class="select" id="schedules-filter-status" aria-label="Filtrar por status">
|
||||||
|
<option value="">Todos</option>
|
||||||
|
<option value="active">Ativo</option>
|
||||||
|
<option value="paused">Pausado</option>
|
||||||
|
</select>
|
||||||
|
<button class="btn btn--primary btn--icon-text" type="button" id="schedules-new-btn">
|
||||||
|
<i data-lucide="plus"></i>
|
||||||
|
<span>Novo Agendamento</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="table-wrapper">
|
||||||
|
<table class="table" id="schedules-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">Agente</th>
|
||||||
|
<th scope="col">Tarefa</th>
|
||||||
|
<th scope="col">Expressão Cron</th>
|
||||||
|
<th scope="col">Próxima Execução</th>
|
||||||
|
<th scope="col">Status</th>
|
||||||
|
<th scope="col" aria-label="Ações"></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="schedules-tbody">
|
||||||
|
<tr class="table-empty-row">
|
||||||
|
<td colspan="6">
|
||||||
|
<div class="empty-state empty-state--inline">
|
||||||
|
<i data-lucide="clock"></i>
|
||||||
|
<span>Nenhum agendamento configurado</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="pipelines" class="section" aria-label="Pipelines" hidden>
|
||||||
|
<div class="page-title">Pipelines</div>
|
||||||
|
<div class="page-subtitle">Encadeie agentes para fluxos de trabalho automatizados</div>
|
||||||
|
|
||||||
|
<div class="section-toolbar">
|
||||||
|
<div class="search-field">
|
||||||
|
<i data-lucide="search" class="search-icon" style="width:16px;height:16px"></i>
|
||||||
|
<input type="text" placeholder="Buscar pipelines..." id="pipelines-search">
|
||||||
|
</div>
|
||||||
|
<div class="section-toolbar-actions">
|
||||||
|
<button class="btn btn--primary btn--icon-text" type="button" id="pipelines-new-btn">
|
||||||
|
<i data-lucide="plus"></i>
|
||||||
|
<span>Novo Pipeline</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="pipelines-grid" class="agents-grid">
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="terminal" class="section" aria-label="Terminal" hidden>
|
||||||
|
<div class="terminal-wrapper">
|
||||||
|
<div class="terminal-toolbar">
|
||||||
|
<div class="terminal-toolbar-left">
|
||||||
|
<div class="terminal-dot terminal-dot--red"></div>
|
||||||
|
<div class="terminal-dot terminal-dot--yellow"></div>
|
||||||
|
<div class="terminal-dot terminal-dot--green"></div>
|
||||||
|
<span class="terminal-title">Output de Execução</span>
|
||||||
|
</div>
|
||||||
|
<div class="terminal-toolbar-right">
|
||||||
|
<select class="select select--sm" id="terminal-execution-select" aria-label="Selecionar execução">
|
||||||
|
<option value="">Selecionar execução...</option>
|
||||||
|
</select>
|
||||||
|
<div class="terminal-ws-indicator" id="terminal-ws-indicator">
|
||||||
|
<span class="ws-indicator ws-indicator--disconnected" id="terminal-ws-dot"></span>
|
||||||
|
<span id="terminal-ws-label">Desconectado</span>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn--ghost btn--sm btn--icon-text" type="button" id="terminal-clear-btn" aria-label="Limpar terminal">
|
||||||
|
<i data-lucide="trash-2"></i>
|
||||||
|
<span>Limpar</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="terminal-output" id="terminal-output" role="log" aria-live="polite" aria-label="Saída do terminal">
|
||||||
|
<div class="terminal-welcome">
|
||||||
|
<span class="terminal-prompt">$</span>
|
||||||
|
<span class="terminal-text">Aguardando execução de agente...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="settings" class="section" aria-label="Configurações" hidden>
|
||||||
|
<div class="settings-grid">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2 class="card-title">Configurações Gerais</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form class="settings-form" id="settings-form" novalidate>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="settings-default-model">Modelo Padrão</label>
|
||||||
|
<select class="select" id="settings-default-model" name="defaultModel">
|
||||||
|
<option value="claude-sonnet-4-6">claude-sonnet-4-6</option>
|
||||||
|
<option value="claude-opus-4-6">claude-opus-4-6</option>
|
||||||
|
<option value="claude-haiku-4-5-20251001">claude-haiku-4-5-20251001</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="settings-default-workdir">Diretório de Trabalho Padrão</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="input"
|
||||||
|
id="settings-default-workdir"
|
||||||
|
name="defaultWorkdir"
|
||||||
|
placeholder="/home/fred/projetos"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="settings-max-concurrent">Execuções Simultâneas Máximas</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
class="input"
|
||||||
|
id="settings-max-concurrent"
|
||||||
|
name="maxConcurrent"
|
||||||
|
min="1"
|
||||||
|
max="20"
|
||||||
|
placeholder="5"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="submit" class="btn btn--primary">Salvar Configurações</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h2 class="card-title">Informações do Sistema</h2>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<ul class="system-info-list" id="system-info-list">
|
||||||
|
<li class="system-info-item">
|
||||||
|
<span class="system-info-label">Versão do Servidor</span>
|
||||||
|
<span class="system-info-value" id="info-server-version">1.0.0</span>
|
||||||
|
</li>
|
||||||
|
<li class="system-info-item">
|
||||||
|
<span class="system-info-label">Versão do Node.js</span>
|
||||||
|
<span class="system-info-value font-mono" id="info-node-version">Carregando...</span>
|
||||||
|
</li>
|
||||||
|
<li class="system-info-item">
|
||||||
|
<span class="system-info-label">Versão do Claude CLI</span>
|
||||||
|
<span class="system-info-value font-mono" id="info-claude-version">Carregando...</span>
|
||||||
|
</li>
|
||||||
|
<li class="system-info-item">
|
||||||
|
<span class="system-info-label">Plataforma</span>
|
||||||
|
<span class="system-info-value font-mono" id="info-platform">Carregando...</span>
|
||||||
|
</li>
|
||||||
|
<li class="system-info-item">
|
||||||
|
<span class="system-info-label">Tempo Online</span>
|
||||||
|
<span class="system-info-value font-mono" id="info-uptime">Carregando...</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-overlay" id="agent-modal-overlay" role="dialog" aria-modal="true" aria-labelledby="agent-modal-title" hidden>
|
||||||
|
<div class="modal modal--lg">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2 class="modal-title" id="agent-modal-title">Novo Agente</h2>
|
||||||
|
<button class="modal-close" type="button" aria-label="Fechar modal" data-modal-close="agent-modal-overlay">
|
||||||
|
<i data-lucide="x"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form class="modal-form" id="agent-form" novalidate>
|
||||||
|
<input type="hidden" id="agent-form-id" name="id" />
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group form-group--grow">
|
||||||
|
<label class="form-label" for="agent-name">
|
||||||
|
Nome do Agente
|
||||||
|
<span class="form-required" aria-hidden="true">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="input"
|
||||||
|
id="agent-name"
|
||||||
|
name="name"
|
||||||
|
placeholder="Ex: Revisor de Código"
|
||||||
|
required
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="agent-status-toggle">Status</label>
|
||||||
|
<div class="toggle-wrapper">
|
||||||
|
<input type="checkbox" class="toggle-input" id="agent-status-toggle" name="active" role="switch" />
|
||||||
|
<label class="toggle-label" for="agent-status-toggle">
|
||||||
|
<span class="toggle-thumb"></span>
|
||||||
|
<span class="toggle-text-on">Ativo</span>
|
||||||
|
<span class="toggle-text-off">Inativo</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="agent-description">Descrição</label>
|
||||||
|
<textarea
|
||||||
|
class="textarea"
|
||||||
|
id="agent-description"
|
||||||
|
name="description"
|
||||||
|
rows="2"
|
||||||
|
placeholder="Descreva brevemente o propósito deste agente"
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="agent-system-prompt">
|
||||||
|
System Prompt
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
class="textarea textarea--code"
|
||||||
|
id="agent-system-prompt"
|
||||||
|
name="systemPrompt"
|
||||||
|
rows="6"
|
||||||
|
placeholder="Você é um especialista em... Seu objetivo é... Sempre responda em português..."
|
||||||
|
></textarea>
|
||||||
|
<p class="form-hint">Instruções base que definem o comportamento, personalidade e restrições do agente.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group form-group--grow">
|
||||||
|
<label class="form-label" for="agent-model">Modelo</label>
|
||||||
|
<select class="select" id="agent-model" name="model">
|
||||||
|
<option value="claude-sonnet-4-6">claude-sonnet-4-6</option>
|
||||||
|
<option value="claude-opus-4-6">claude-opus-4-6</option>
|
||||||
|
<option value="claude-haiku-4-5-20251001">claude-haiku-4-5-20251001</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group form-group--grow">
|
||||||
|
<label class="form-label" for="agent-workdir">Diretório de Trabalho</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="input"
|
||||||
|
id="agent-workdir"
|
||||||
|
name="workdir"
|
||||||
|
placeholder="/home/fred/projetos"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="agent-tags-input">Tags</label>
|
||||||
|
<div class="tags-input-wrapper" id="agent-tags-wrapper">
|
||||||
|
<div class="tags-chips" id="agent-tags-chips"></div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="tags-input"
|
||||||
|
id="agent-tags-input"
|
||||||
|
placeholder="Digite e pressione Enter para adicionar"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<input type="hidden" id="agent-tags" name="tags" value="[]" />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn--ghost" type="button" data-modal-close="agent-modal-overlay">Cancelar</button>
|
||||||
|
<button class="btn btn--primary" type="submit" form="agent-form" id="agent-form-submit">Salvar</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-overlay" id="execute-modal-overlay" role="dialog" aria-modal="true" aria-labelledby="execute-modal-title" hidden>
|
||||||
|
<div class="modal modal--md">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2 class="modal-title" id="execute-modal-title">Executar Tarefa</h2>
|
||||||
|
<button class="modal-close" type="button" aria-label="Fechar modal" data-modal-close="execute-modal-overlay">
|
||||||
|
<i data-lucide="x"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form class="modal-form" id="execute-form" novalidate>
|
||||||
|
<input type="hidden" id="execute-agent-id" name="agentId" />
|
||||||
|
|
||||||
|
<div class="form-group" id="execute-agent-select-group">
|
||||||
|
<label class="form-label" for="execute-agent-select">
|
||||||
|
Agente
|
||||||
|
<span class="form-required" aria-hidden="true">*</span>
|
||||||
|
</label>
|
||||||
|
<select class="select" id="execute-agent-select" name="agentSelect" required>
|
||||||
|
<option value="">Selecionar agente...</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="execute-task-desc">
|
||||||
|
Descrição da Tarefa
|
||||||
|
<span class="form-required" aria-hidden="true">*</span>
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
class="textarea"
|
||||||
|
id="execute-task-desc"
|
||||||
|
name="task"
|
||||||
|
rows="3"
|
||||||
|
placeholder="Descreva o que o agente deve fazer..."
|
||||||
|
required
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="execute-instructions">Instruções Adicionais</label>
|
||||||
|
<textarea
|
||||||
|
class="textarea textarea--code"
|
||||||
|
id="execute-instructions"
|
||||||
|
name="instructions"
|
||||||
|
rows="3"
|
||||||
|
placeholder="Contexto adicional, restrições ou preferências..."
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="quick-templates">
|
||||||
|
<p class="quick-templates-label">Templates rápidos</p>
|
||||||
|
<div class="quick-templates-grid">
|
||||||
|
<button class="template-btn" type="button" data-template="Analisar código e detectar bugs, vulnerabilidades e problemas de qualidade. Forneça um relatório detalhado com sugestões de correção.">
|
||||||
|
<i data-lucide="bug"></i>
|
||||||
|
<span>Detectar Bugs</span>
|
||||||
|
</button>
|
||||||
|
<button class="template-btn" type="button" data-template="Realizar revisão de segurança seguindo as diretrizes OWASP Top 10. Identificar vulnerabilidades, inputs não validados e problemas de autenticação/autorização.">
|
||||||
|
<i data-lucide="shield"></i>
|
||||||
|
<span>Revisão OWASP</span>
|
||||||
|
</button>
|
||||||
|
<button class="template-btn" type="button" data-template="Refatorar o código para melhorar legibilidade, manutenibilidade e aderência às boas práticas. Manter comportamento funcional intacto.">
|
||||||
|
<i data-lucide="wand-2"></i>
|
||||||
|
<span>Refatorar Código</span>
|
||||||
|
</button>
|
||||||
|
<button class="template-btn" type="button" data-template="Escrever testes unitários e de integração abrangentes. Garantir cobertura dos casos de sucesso, erro e edge cases.">
|
||||||
|
<i data-lucide="test-tube-2"></i>
|
||||||
|
<span>Escrever Testes</span>
|
||||||
|
</button>
|
||||||
|
<button class="template-btn" type="button" data-template="Documentar o código com JSDoc/docstrings, README atualizado, exemplos de uso e descrição de APIs públicas.">
|
||||||
|
<i data-lucide="book-open"></i>
|
||||||
|
<span>Documentar Código</span>
|
||||||
|
</button>
|
||||||
|
<button class="template-btn" type="button" data-template="Analisar e otimizar a performance do código. Identificar gargalos, reduzir complexidade algorítmica e melhorar uso de memória.">
|
||||||
|
<i data-lucide="gauge"></i>
|
||||||
|
<span>Otimizar Performance</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn--ghost" type="button" data-modal-close="execute-modal-overlay">Cancelar</button>
|
||||||
|
<button class="btn btn--primary btn--icon-text" type="submit" form="execute-form" id="execute-form-submit">
|
||||||
|
<i data-lucide="play"></i>
|
||||||
|
<span>Executar</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-overlay" id="schedule-modal-overlay" role="dialog" aria-modal="true" aria-labelledby="schedule-modal-title" hidden>
|
||||||
|
<div class="modal modal--md">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2 class="modal-title" id="schedule-modal-title">Novo Agendamento</h2>
|
||||||
|
<button class="modal-close" type="button" aria-label="Fechar modal" data-modal-close="schedule-modal-overlay">
|
||||||
|
<i data-lucide="x"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form class="modal-form" id="schedule-form" novalidate>
|
||||||
|
<input type="hidden" id="schedule-form-id" name="id" />
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="schedule-agent">
|
||||||
|
Agente
|
||||||
|
<span class="form-required" aria-hidden="true">*</span>
|
||||||
|
</label>
|
||||||
|
<select class="select" id="schedule-agent" name="agentId" required>
|
||||||
|
<option value="">Selecionar agente...</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="schedule-task">
|
||||||
|
Descrição da Tarefa
|
||||||
|
<span class="form-required" aria-hidden="true">*</span>
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
class="textarea"
|
||||||
|
id="schedule-task"
|
||||||
|
name="task"
|
||||||
|
rows="3"
|
||||||
|
placeholder="Descreva a tarefa que será executada automaticamente..."
|
||||||
|
required
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="schedule-cron">
|
||||||
|
Expressão Cron
|
||||||
|
<span class="form-required" aria-hidden="true">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="input input--mono"
|
||||||
|
id="schedule-cron"
|
||||||
|
name="cron"
|
||||||
|
placeholder="0 * * * *"
|
||||||
|
required
|
||||||
|
autocomplete="off"
|
||||||
|
aria-describedby="schedule-cron-hint"
|
||||||
|
/>
|
||||||
|
<p class="form-hint" id="schedule-cron-hint">Formato: minuto hora dia-mês mês dia-semana</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cron-presets">
|
||||||
|
<p class="cron-presets-label">Presets</p>
|
||||||
|
<div class="cron-presets-list">
|
||||||
|
<button class="cron-preset-btn" type="button" data-cron="0 * * * *">
|
||||||
|
<span class="cron-preset-name">A cada hora</span>
|
||||||
|
<span class="cron-preset-expr">0 * * * *</span>
|
||||||
|
</button>
|
||||||
|
<button class="cron-preset-btn" type="button" data-cron="0 9 * * *">
|
||||||
|
<span class="cron-preset-name">Diário (09:00)</span>
|
||||||
|
<span class="cron-preset-expr">0 9 * * *</span>
|
||||||
|
</button>
|
||||||
|
<button class="cron-preset-btn" type="button" data-cron="0 9 * * 1">
|
||||||
|
<span class="cron-preset-name">Semanal (seg 09:00)</span>
|
||||||
|
<span class="cron-preset-expr">0 9 * * 1</span>
|
||||||
|
</button>
|
||||||
|
<button class="cron-preset-btn" type="button" data-cron="0 0 1 * *">
|
||||||
|
<span class="cron-preset-name">Mensal (dia 1)</span>
|
||||||
|
<span class="cron-preset-expr">0 0 1 * *</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn--ghost" type="button" data-modal-close="schedule-modal-overlay">Cancelar</button>
|
||||||
|
<button class="btn btn--primary btn--icon-text" type="submit" form="schedule-form" id="schedule-form-submit">
|
||||||
|
<i data-lucide="clock"></i>
|
||||||
|
<span>Agendar</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-overlay" id="pipeline-modal-overlay" role="dialog" aria-modal="true" aria-labelledby="pipeline-modal-title" hidden>
|
||||||
|
<div class="modal modal--lg">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2 class="modal-title" id="pipeline-modal-title">Novo Pipeline</h2>
|
||||||
|
<button class="modal-close" type="button" aria-label="Fechar modal" data-modal-close="pipeline-modal-overlay">
|
||||||
|
<i data-lucide="x"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form class="modal-form" id="pipeline-form" novalidate>
|
||||||
|
<input type="hidden" id="pipeline-form-id">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="pipeline-name">
|
||||||
|
Nome
|
||||||
|
<span class="form-required" aria-hidden="true">*</span>
|
||||||
|
</label>
|
||||||
|
<input class="input" type="text" id="pipeline-name" placeholder="Ex: Análise e Correção de Bugs" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="pipeline-description">Descrição</label>
|
||||||
|
<textarea class="textarea" id="pipeline-description" rows="2" placeholder="Descreva o objetivo do pipeline..."></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">
|
||||||
|
Passos do Pipeline
|
||||||
|
<span class="form-required" aria-hidden="true">*</span>
|
||||||
|
</label>
|
||||||
|
<p class="form-hint mb-8">Cada passo usa um agente. O output de um passo vira o input do próximo.</p>
|
||||||
|
<div id="pipeline-steps-container"></div>
|
||||||
|
<button class="btn btn--ghost btn--sm mt-8" type="button" id="pipeline-add-step-btn">
|
||||||
|
<i data-lucide="plus" style="width:14px;height:14px"></i>
|
||||||
|
Adicionar Passo
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn--ghost" type="button" data-modal-close="pipeline-modal-overlay">Cancelar</button>
|
||||||
|
<button class="btn btn--primary" type="button" id="pipeline-form-submit">Salvar Pipeline</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-overlay" id="pipeline-execute-modal-overlay" role="dialog" aria-modal="true" aria-labelledby="pipeline-execute-title" hidden>
|
||||||
|
<div class="modal modal--md">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2 class="modal-title" id="pipeline-execute-title">Executar Pipeline</h2>
|
||||||
|
<button class="modal-close" type="button" aria-label="Fechar modal" data-modal-close="pipeline-execute-modal-overlay">
|
||||||
|
<i data-lucide="x"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<input type="hidden" id="pipeline-execute-id">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="pipeline-execute-input">
|
||||||
|
Input Inicial
|
||||||
|
<span class="form-required" aria-hidden="true">*</span>
|
||||||
|
</label>
|
||||||
|
<textarea class="textarea" id="pipeline-execute-input" rows="4" placeholder="Descreva a tarefa inicial para o pipeline..."></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn--ghost" type="button" data-modal-close="pipeline-execute-modal-overlay">Cancelar</button>
|
||||||
|
<button class="btn btn--primary btn--icon-text" type="button" id="pipeline-execute-submit">
|
||||||
|
<i data-lucide="play" style="width:14px;height:14px"></i>
|
||||||
|
<span>Executar</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-overlay" id="confirm-modal-overlay" role="dialog" aria-modal="true" aria-labelledby="confirm-modal-title" hidden>
|
||||||
|
<div class="modal modal--sm">
|
||||||
|
<div class="modal-header">
|
||||||
|
<div class="confirm-modal-icon" id="confirm-modal-icon">
|
||||||
|
<i data-lucide="alert-triangle"></i>
|
||||||
|
</div>
|
||||||
|
<h2 class="modal-title" id="confirm-modal-title">Confirmar Ação</h2>
|
||||||
|
<button class="modal-close" type="button" aria-label="Fechar modal" data-modal-close="confirm-modal-overlay">
|
||||||
|
<i data-lucide="x"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p class="confirm-modal-message" id="confirm-modal-message"></p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn--ghost" type="button" data-modal-close="confirm-modal-overlay">Cancelar</button>
|
||||||
|
<button class="btn btn--danger" type="button" id="confirm-modal-confirm-btn">Confirmar</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-overlay" id="export-modal-overlay" role="dialog" aria-modal="true" aria-labelledby="export-modal-title" hidden>
|
||||||
|
<div class="modal modal--md">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2 class="modal-title" id="export-modal-title">Exportar Agente</h2>
|
||||||
|
<button class="modal-close" type="button" aria-label="Fechar modal" data-modal-close="export-modal-overlay">
|
||||||
|
<i data-lucide="x"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="export-code-wrapper">
|
||||||
|
<div class="export-code-toolbar">
|
||||||
|
<span class="export-code-label">JSON</span>
|
||||||
|
<button class="btn btn--ghost btn--sm btn--icon-text" type="button" id="export-copy-btn" aria-label="Copiar JSON">
|
||||||
|
<i data-lucide="copy"></i>
|
||||||
|
<span>Copiar</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<pre class="export-code" id="export-code-content" tabindex="0" aria-label="Dados do agente em formato JSON"></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn--ghost" type="button" data-modal-close="export-modal-overlay">Fechar</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="toast-container" id="toast-container" aria-live="assertive" aria-atomic="false" role="region" aria-label="Notificações"></div>
|
||||||
|
|
||||||
|
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.js"></script>
|
||||||
|
<script src="js/api.js"></script>
|
||||||
|
<script src="js/components/toast.js"></script>
|
||||||
|
<script src="js/components/modal.js"></script>
|
||||||
|
<script src="js/components/terminal.js"></script>
|
||||||
|
<script src="js/components/agents.js"></script>
|
||||||
|
<script src="js/components/dashboard.js"></script>
|
||||||
|
<script src="js/components/tasks.js"></script>
|
||||||
|
<script src="js/components/schedules.js"></script>
|
||||||
|
<script src="js/components/pipelines.js"></script>
|
||||||
|
<script src="js/app.js"></script>
|
||||||
|
<script>
|
||||||
|
lucide.createIcons();
|
||||||
|
App.init();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
67
public/js/api.js
Normal file
67
public/js/api.js
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
const API = {
|
||||||
|
baseUrl: '/api',
|
||||||
|
|
||||||
|
async request(method, path, body = null) {
|
||||||
|
const options = {
|
||||||
|
method,
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
};
|
||||||
|
|
||||||
|
if (body !== null) {
|
||||||
|
options.body = JSON.stringify(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(`${API.baseUrl}${path}`, options);
|
||||||
|
|
||||||
|
if (response.status === 204) return null;
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(data.error || `Erro HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
|
||||||
|
agents: {
|
||||||
|
list() { return API.request('GET', '/agents'); },
|
||||||
|
get(id) { return API.request('GET', `/agents/${id}`); },
|
||||||
|
create(data) { return API.request('POST', '/agents', data); },
|
||||||
|
update(id, data) { return API.request('PUT', `/agents/${id}`, data); },
|
||||||
|
delete(id) { return API.request('DELETE', `/agents/${id}`); },
|
||||||
|
execute(id, task, instructions) { return API.request('POST', `/agents/${id}/execute`, { task, instructions }); },
|
||||||
|
cancel(id, executionId) { return API.request('POST', `/agents/${id}/cancel/${executionId}`); },
|
||||||
|
export(id) { return API.request('GET', `/agents/${id}/export`); },
|
||||||
|
},
|
||||||
|
|
||||||
|
tasks: {
|
||||||
|
list() { return API.request('GET', '/tasks'); },
|
||||||
|
create(data) { return API.request('POST', '/tasks', data); },
|
||||||
|
update(id, data) { return API.request('PUT', `/tasks/${id}`, data); },
|
||||||
|
delete(id) { return API.request('DELETE', `/tasks/${id}`); },
|
||||||
|
},
|
||||||
|
|
||||||
|
schedules: {
|
||||||
|
list() { return API.request('GET', '/schedules'); },
|
||||||
|
create(data) { return API.request('POST', '/schedules', data); },
|
||||||
|
delete(taskId) { return API.request('DELETE', `/schedules/${taskId}`); },
|
||||||
|
},
|
||||||
|
|
||||||
|
pipelines: {
|
||||||
|
list() { return API.request('GET', '/pipelines'); },
|
||||||
|
get(id) { return API.request('GET', `/pipelines/${id}`); },
|
||||||
|
create(data) { return API.request('POST', '/pipelines', data); },
|
||||||
|
update(id, data) { return API.request('PUT', `/pipelines/${id}`, data); },
|
||||||
|
delete(id) { return API.request('DELETE', `/pipelines/${id}`); },
|
||||||
|
execute(id, input) { return API.request('POST', `/pipelines/${id}/execute`, { input }); },
|
||||||
|
cancel(id) { return API.request('POST', `/pipelines/${id}/cancel`); },
|
||||||
|
},
|
||||||
|
|
||||||
|
system: {
|
||||||
|
status() { return API.request('GET', '/system/status'); },
|
||||||
|
activeExecutions() { return API.request('GET', '/executions/active'); },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
window.API = API;
|
||||||
545
public/js/app.js
Normal file
545
public/js/app.js
Normal file
@@ -0,0 +1,545 @@
|
|||||||
|
const App = {
|
||||||
|
currentSection: 'dashboard',
|
||||||
|
ws: null,
|
||||||
|
wsReconnectAttempts: 0,
|
||||||
|
wsReconnectTimer: null,
|
||||||
|
_initialized: false,
|
||||||
|
|
||||||
|
sectionTitles: {
|
||||||
|
dashboard: 'Dashboard',
|
||||||
|
agents: 'Agentes',
|
||||||
|
tasks: 'Tarefas',
|
||||||
|
schedules: 'Agendamentos',
|
||||||
|
pipelines: 'Pipelines',
|
||||||
|
terminal: 'Terminal',
|
||||||
|
settings: 'Configurações',
|
||||||
|
},
|
||||||
|
|
||||||
|
init() {
|
||||||
|
if (App._initialized) return;
|
||||||
|
App._initialized = true;
|
||||||
|
|
||||||
|
App.setupNavigation();
|
||||||
|
App.setupWebSocket();
|
||||||
|
App.setupEventListeners();
|
||||||
|
App.setupKeyboardShortcuts();
|
||||||
|
App.navigateTo('dashboard');
|
||||||
|
App.startPeriodicRefresh();
|
||||||
|
|
||||||
|
if (window.lucide) lucide.createIcons();
|
||||||
|
},
|
||||||
|
|
||||||
|
setupNavigation() {
|
||||||
|
document.querySelectorAll('.sidebar-nav-link[data-section]').forEach((link) => {
|
||||||
|
link.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
App.navigateTo(link.dataset.section);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const refreshBtn = document.getElementById('refresh-activity-btn');
|
||||||
|
if (refreshBtn) {
|
||||||
|
refreshBtn.addEventListener('click', () => DashboardUI.load());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
navigateTo(section) {
|
||||||
|
document.querySelectorAll('.section').forEach((el) => {
|
||||||
|
const isActive = el.id === section;
|
||||||
|
el.classList.toggle('active', isActive);
|
||||||
|
el.hidden = !isActive;
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.sidebar-nav-item').forEach((item) => {
|
||||||
|
const link = item.querySelector('.sidebar-nav-link');
|
||||||
|
item.classList.toggle('active', link && link.dataset.section === section);
|
||||||
|
});
|
||||||
|
|
||||||
|
const titleEl = document.getElementById('header-title');
|
||||||
|
if (titleEl) titleEl.textContent = App.sectionTitles[section] || section;
|
||||||
|
|
||||||
|
App.currentSection = section;
|
||||||
|
App._loadSection(section);
|
||||||
|
},
|
||||||
|
|
||||||
|
async _loadSection(section) {
|
||||||
|
try {
|
||||||
|
switch (section) {
|
||||||
|
case 'dashboard': await DashboardUI.load(); break;
|
||||||
|
case 'agents': await AgentsUI.load(); break;
|
||||||
|
case 'tasks': await TasksUI.load(); break;
|
||||||
|
case 'schedules': await SchedulesUI.load(); break;
|
||||||
|
case 'pipelines': await PipelinesUI.load(); break;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
Toast.error(`Erro ao carregar seção: ${err.message}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setupWebSocket() {
|
||||||
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
|
const url = `${protocol}//${window.location.host}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
App.ws = new WebSocket(url);
|
||||||
|
|
||||||
|
App.ws.onopen = () => {
|
||||||
|
App.updateWsStatus('connected');
|
||||||
|
App.wsReconnectAttempts = 0;
|
||||||
|
if (App.wsReconnectTimer) {
|
||||||
|
clearTimeout(App.wsReconnectTimer);
|
||||||
|
App.wsReconnectTimer = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
App.ws.onclose = () => {
|
||||||
|
App.updateWsStatus('disconnected');
|
||||||
|
App._scheduleWsReconnect();
|
||||||
|
};
|
||||||
|
|
||||||
|
App.ws.onerror = () => {
|
||||||
|
App.updateWsStatus('error');
|
||||||
|
};
|
||||||
|
|
||||||
|
App.ws.onmessage = (event) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
App.handleWsMessage(data);
|
||||||
|
} catch {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
App.updateWsStatus('error');
|
||||||
|
App._scheduleWsReconnect();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_scheduleWsReconnect() {
|
||||||
|
const delay = Math.min(1000 * Math.pow(2, App.wsReconnectAttempts), 30000);
|
||||||
|
App.wsReconnectAttempts++;
|
||||||
|
|
||||||
|
App.wsReconnectTimer = setTimeout(() => {
|
||||||
|
App.setupWebSocket();
|
||||||
|
}, delay);
|
||||||
|
},
|
||||||
|
|
||||||
|
handleWsMessage(data) {
|
||||||
|
switch (data.type) {
|
||||||
|
case 'execution_output': {
|
||||||
|
Terminal.stopProcessing();
|
||||||
|
const content = data.data?.content || '';
|
||||||
|
if (content) {
|
||||||
|
Terminal.addLine(content, 'default');
|
||||||
|
}
|
||||||
|
App._updateActiveBadge();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'execution_complete': {
|
||||||
|
Terminal.stopProcessing();
|
||||||
|
const result = data.data?.result || '';
|
||||||
|
if (result) {
|
||||||
|
Terminal.addLine(result, 'success');
|
||||||
|
} else {
|
||||||
|
Terminal.addLine('Execução concluída (sem resultado textual).', 'info');
|
||||||
|
}
|
||||||
|
if (data.data?.stderr) {
|
||||||
|
Terminal.addLine(data.data.stderr, 'error');
|
||||||
|
}
|
||||||
|
Toast.success('Execução concluída');
|
||||||
|
App.refreshCurrentSection();
|
||||||
|
App._updateActiveBadge();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'execution_error':
|
||||||
|
Terminal.stopProcessing();
|
||||||
|
Terminal.addLine(data.data?.error || 'Erro na execução', 'error');
|
||||||
|
Toast.error(`Erro na execução: ${data.data?.error || 'desconhecido'}`);
|
||||||
|
App._updateActiveBadge();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'pipeline_step_start':
|
||||||
|
Terminal.stopProcessing();
|
||||||
|
Terminal.addLine(`Pipeline passo ${data.stepIndex + 1}/${data.totalSteps}: Executando agente "${data.agentName}"...`, 'system');
|
||||||
|
Terminal.startProcessing(data.agentName);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'pipeline_step_complete':
|
||||||
|
Terminal.stopProcessing();
|
||||||
|
Terminal.addLine(`Passo ${data.stepIndex + 1} concluído.`, 'info');
|
||||||
|
Terminal.addLine(data.result || '(sem output)', 'default');
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'pipeline_complete':
|
||||||
|
Terminal.stopProcessing();
|
||||||
|
Terminal.addLine('Pipeline concluído com sucesso.', 'success');
|
||||||
|
Toast.success('Pipeline concluído');
|
||||||
|
App.refreshCurrentSection();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'pipeline_error':
|
||||||
|
Terminal.stopProcessing();
|
||||||
|
Terminal.addLine(`Erro no passo ${data.stepIndex + 1}: ${data.error}`, 'error');
|
||||||
|
Toast.error('Erro no pipeline');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateWsStatus(status) {
|
||||||
|
const indicator = document.getElementById('ws-indicator');
|
||||||
|
const label = document.getElementById('ws-label');
|
||||||
|
const terminalDot = document.getElementById('terminal-ws-dot');
|
||||||
|
const terminalLabel = document.getElementById('terminal-ws-label');
|
||||||
|
const wsBadge = document.getElementById('system-ws-status-badge');
|
||||||
|
|
||||||
|
const labels = {
|
||||||
|
connected: 'Conectado',
|
||||||
|
disconnected: 'Desconectado',
|
||||||
|
error: 'Erro de conexão',
|
||||||
|
};
|
||||||
|
|
||||||
|
const cssClass = {
|
||||||
|
connected: 'ws-indicator--connected',
|
||||||
|
disconnected: 'ws-indicator--disconnected',
|
||||||
|
error: 'ws-indicator--error',
|
||||||
|
};
|
||||||
|
|
||||||
|
const badgeClass = {
|
||||||
|
connected: 'badge--green',
|
||||||
|
disconnected: 'badge--red',
|
||||||
|
error: 'badge--red',
|
||||||
|
};
|
||||||
|
|
||||||
|
const displayLabel = labels[status] || status;
|
||||||
|
const dotClass = cssClass[status] || 'ws-indicator--disconnected';
|
||||||
|
|
||||||
|
[indicator, terminalDot].forEach((el) => {
|
||||||
|
if (!el) return;
|
||||||
|
el.className = `ws-indicator ${dotClass}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
[label, terminalLabel].forEach((el) => {
|
||||||
|
if (el) el.textContent = displayLabel;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (wsBadge) {
|
||||||
|
wsBadge.textContent = displayLabel;
|
||||||
|
wsBadge.className = `badge ${badgeClass[status] || 'badge--red'}`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setupEventListeners() {
|
||||||
|
const on = (id, event, handler) => {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if (el) el.addEventListener(event, handler);
|
||||||
|
};
|
||||||
|
|
||||||
|
on('new-agent-btn', 'click', () => AgentsUI.openCreateModal());
|
||||||
|
on('agents-empty-new-btn', 'click', () => AgentsUI.openCreateModal());
|
||||||
|
|
||||||
|
on('agent-form-submit', 'click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
AgentsUI.save();
|
||||||
|
});
|
||||||
|
|
||||||
|
on('agent-form', 'submit', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
AgentsUI.save();
|
||||||
|
});
|
||||||
|
|
||||||
|
on('execute-form-submit', 'click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
App._handleExecute();
|
||||||
|
});
|
||||||
|
|
||||||
|
on('execute-form', 'submit', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
App._handleExecute();
|
||||||
|
});
|
||||||
|
|
||||||
|
on('tasks-new-btn', 'click', () => TasksUI.openCreateModal());
|
||||||
|
on('tasks-empty-new-btn', 'click', () => TasksUI.openCreateModal());
|
||||||
|
|
||||||
|
on('schedules-new-btn', 'click', () => SchedulesUI.openCreateModal());
|
||||||
|
|
||||||
|
on('schedule-form-submit', 'click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
SchedulesUI.save();
|
||||||
|
});
|
||||||
|
|
||||||
|
on('schedule-form', 'submit', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
SchedulesUI.save();
|
||||||
|
});
|
||||||
|
|
||||||
|
on('pipelines-new-btn', 'click', () => PipelinesUI.openCreateModal());
|
||||||
|
|
||||||
|
on('pipeline-form-submit', 'click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
PipelinesUI.save();
|
||||||
|
});
|
||||||
|
|
||||||
|
on('pipeline-add-step-btn', 'click', () => PipelinesUI.addStep());
|
||||||
|
|
||||||
|
on('pipeline-execute-submit', 'click', () => PipelinesUI._executeFromModal());
|
||||||
|
|
||||||
|
on('terminal-clear-btn', 'click', () => Terminal.clear());
|
||||||
|
|
||||||
|
on('export-copy-btn', 'click', () => App._copyExportJson());
|
||||||
|
|
||||||
|
on('system-status-btn', 'click', () => App.navigateTo('dashboard'));
|
||||||
|
|
||||||
|
on('terminal-execution-select', 'change', (e) => {
|
||||||
|
Terminal.setExecutionFilter(e.target.value || null);
|
||||||
|
});
|
||||||
|
|
||||||
|
on('settings-form', 'submit', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
Toast.info('Configurações salvas');
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('agents-grid')?.addEventListener('click', (e) => {
|
||||||
|
const btn = e.target.closest('[data-action]');
|
||||||
|
if (!btn) return;
|
||||||
|
|
||||||
|
const { action, id } = btn.dataset;
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case 'execute': AgentsUI.execute(id); break;
|
||||||
|
case 'edit': AgentsUI.openEditModal(id); break;
|
||||||
|
case 'export': AgentsUI.export(id); break;
|
||||||
|
case 'delete': AgentsUI.delete(id); break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('tasks-grid')?.addEventListener('click', (e) => {
|
||||||
|
const btn = e.target.closest('[data-action]');
|
||||||
|
if (!btn) return;
|
||||||
|
|
||||||
|
const { action, id } = btn.dataset;
|
||||||
|
|
||||||
|
if (action === 'delete-task') TasksUI.delete(id);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('schedules-tbody')?.addEventListener('click', (e) => {
|
||||||
|
const btn = e.target.closest('[data-action]');
|
||||||
|
if (!btn) return;
|
||||||
|
|
||||||
|
const { action, id } = btn.dataset;
|
||||||
|
|
||||||
|
if (action === 'delete-schedule') SchedulesUI.delete(id);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('pipelines-grid')?.addEventListener('click', (e) => {
|
||||||
|
if (e.target.closest('#pipelines-empty-new-btn')) {
|
||||||
|
PipelinesUI.openCreateModal();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const btn = e.target.closest('[data-action]');
|
||||||
|
if (!btn) return;
|
||||||
|
|
||||||
|
const { action, id } = btn.dataset;
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case 'execute-pipeline': PipelinesUI.execute(id); break;
|
||||||
|
case 'edit-pipeline': PipelinesUI.openEditModal(id); break;
|
||||||
|
case 'delete-pipeline': PipelinesUI.delete(id); break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('pipeline-steps-container')?.addEventListener('click', (e) => {
|
||||||
|
const btn = e.target.closest('[data-step-action]');
|
||||||
|
if (!btn) return;
|
||||||
|
|
||||||
|
const stepAction = btn.dataset.stepAction;
|
||||||
|
const stepIndex = parseInt(btn.dataset.stepIndex, 10);
|
||||||
|
|
||||||
|
switch (stepAction) {
|
||||||
|
case 'move-up': PipelinesUI.moveStep(stepIndex, -1); break;
|
||||||
|
case 'move-down': PipelinesUI.moveStep(stepIndex, 1); break;
|
||||||
|
case 'remove': PipelinesUI.removeStep(stepIndex); break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
const template = e.target.closest('[data-template]');
|
||||||
|
if (template) {
|
||||||
|
const taskEl = document.getElementById('execute-task-desc');
|
||||||
|
if (taskEl) taskEl.value = template.dataset.template;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cronPreset = e.target.closest('[data-cron]');
|
||||||
|
if (cronPreset) {
|
||||||
|
const cronEl = document.getElementById('schedule-cron');
|
||||||
|
if (cronEl) cronEl.value = cronPreset.dataset.cron;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
App._setupTagsInput();
|
||||||
|
},
|
||||||
|
|
||||||
|
_setupTagsInput() {
|
||||||
|
const input = document.getElementById('agent-tags-input');
|
||||||
|
const chips = document.getElementById('agent-tags-chips');
|
||||||
|
const hidden = document.getElementById('agent-tags');
|
||||||
|
|
||||||
|
if (!input || !chips || !hidden) return;
|
||||||
|
|
||||||
|
const getTags = () => {
|
||||||
|
try { return JSON.parse(hidden.value || '[]'); } catch { return []; }
|
||||||
|
};
|
||||||
|
|
||||||
|
const setTags = (tags) => {
|
||||||
|
hidden.value = JSON.stringify(tags);
|
||||||
|
chips.innerHTML = tags.map((t) => `
|
||||||
|
<span class="tag-chip">
|
||||||
|
${t}
|
||||||
|
<button type="button" class="tag-remove" data-tag="${t}" aria-label="Remover tag ${t}">×</button>
|
||||||
|
</span>
|
||||||
|
`).join('');
|
||||||
|
};
|
||||||
|
|
||||||
|
input.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key !== 'Enter' && e.key !== ',') return;
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const value = input.value.trim().replace(/,$/, '');
|
||||||
|
if (!value) return;
|
||||||
|
|
||||||
|
const tags = getTags();
|
||||||
|
if (!tags.includes(value)) {
|
||||||
|
tags.push(value);
|
||||||
|
setTags(tags);
|
||||||
|
}
|
||||||
|
|
||||||
|
input.value = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
chips.addEventListener('click', (e) => {
|
||||||
|
const removeBtn = e.target.closest('.tag-remove');
|
||||||
|
if (!removeBtn) return;
|
||||||
|
|
||||||
|
const tag = removeBtn.dataset.tag;
|
||||||
|
const tags = getTags().filter((t) => t !== tag);
|
||||||
|
setTags(tags);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async _handleExecute() {
|
||||||
|
const agentId = document.getElementById('execute-agent-select')?.value
|
||||||
|
|| document.getElementById('execute-agent-id')?.value;
|
||||||
|
|
||||||
|
if (!agentId) {
|
||||||
|
Toast.warning('Selecione um agente para executar');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const task = document.getElementById('execute-task-desc')?.value.trim();
|
||||||
|
if (!task) {
|
||||||
|
Toast.warning('Descreva a tarefa a ser executada');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const instructions = document.getElementById('execute-instructions')?.value.trim() || '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const selectEl = document.getElementById('execute-agent-select');
|
||||||
|
const agentName = selectEl?.selectedOptions[0]?.text || 'Agente';
|
||||||
|
|
||||||
|
await API.agents.execute(agentId, task, instructions);
|
||||||
|
|
||||||
|
Modal.close('execute-modal-overlay');
|
||||||
|
App.navigateTo('terminal');
|
||||||
|
Toast.info('Execução iniciada');
|
||||||
|
Terminal.startProcessing(agentName);
|
||||||
|
} catch (err) {
|
||||||
|
Toast.error(`Erro ao iniciar execução: ${err.message}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async _copyExportJson() {
|
||||||
|
const jsonEl = document.getElementById('export-code-content');
|
||||||
|
if (!jsonEl) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(jsonEl.textContent);
|
||||||
|
Toast.success('JSON copiado para a área de transferência');
|
||||||
|
} catch {
|
||||||
|
Toast.error('Não foi possível copiar o JSON');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setupKeyboardShortcuts() {
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
Modal.closeAll();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isTyping = ['INPUT', 'TEXTAREA', 'SELECT'].includes(document.activeElement?.tagName);
|
||||||
|
if (isTyping) return;
|
||||||
|
|
||||||
|
if (e.key === 'n' || e.key === 'N') {
|
||||||
|
if (App.currentSection === 'agents') {
|
||||||
|
AgentsUI.openCreateModal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async refreshCurrentSection() {
|
||||||
|
await App._loadSection(App.currentSection);
|
||||||
|
},
|
||||||
|
|
||||||
|
async _updateActiveBadge() {
|
||||||
|
try {
|
||||||
|
const active = await API.system.activeExecutions();
|
||||||
|
const count = Array.isArray(active) ? active.length : 0;
|
||||||
|
|
||||||
|
const badge = document.getElementById('active-executions-badge');
|
||||||
|
const countEl = document.getElementById('active-executions-count');
|
||||||
|
|
||||||
|
if (countEl) countEl.textContent = count;
|
||||||
|
if (badge) badge.style.display = count > 0 ? 'flex' : 'none';
|
||||||
|
|
||||||
|
const terminalSelect = document.getElementById('terminal-execution-select');
|
||||||
|
if (terminalSelect && Array.isArray(active)) {
|
||||||
|
const existing = new Set(
|
||||||
|
Array.from(terminalSelect.options).map((o) => o.value).filter(Boolean)
|
||||||
|
);
|
||||||
|
|
||||||
|
active.forEach((exec) => {
|
||||||
|
const execId = exec.executionId || exec.id;
|
||||||
|
if (!existing.has(execId)) {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = execId;
|
||||||
|
const agentName = (exec.agentConfig && exec.agentConfig.agent_name) || exec.agentId || 'Agente';
|
||||||
|
option.textContent = `${agentName} — ${new Date(exec.startedAt).toLocaleTimeString('pt-BR')}`;
|
||||||
|
terminalSelect.appendChild(option);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
startPeriodicRefresh() {
|
||||||
|
setInterval(async () => {
|
||||||
|
await App._updateActiveBadge();
|
||||||
|
|
||||||
|
if (App.currentSection === 'dashboard') {
|
||||||
|
await DashboardUI.load();
|
||||||
|
}
|
||||||
|
}, 30000);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => App.init());
|
||||||
|
|
||||||
|
window.App = App;
|
||||||
309
public/js/components/agents.js
Normal file
309
public/js/components/agents.js
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
const AgentsUI = {
|
||||||
|
agents: [],
|
||||||
|
|
||||||
|
avatarColors: [
|
||||||
|
'#6366f1',
|
||||||
|
'#8b5cf6',
|
||||||
|
'#ec4899',
|
||||||
|
'#f59e0b',
|
||||||
|
'#10b981',
|
||||||
|
'#3b82f6',
|
||||||
|
'#ef4444',
|
||||||
|
'#14b8a6',
|
||||||
|
],
|
||||||
|
|
||||||
|
async load() {
|
||||||
|
try {
|
||||||
|
AgentsUI.agents = await API.agents.list();
|
||||||
|
AgentsUI.render();
|
||||||
|
} catch (err) {
|
||||||
|
Toast.error(`Erro ao carregar agentes: ${err.message}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const grid = document.getElementById('agents-grid');
|
||||||
|
const empty = document.getElementById('agents-empty-state');
|
||||||
|
|
||||||
|
if (!grid) return;
|
||||||
|
|
||||||
|
if (AgentsUI.agents.length === 0) {
|
||||||
|
if (empty) empty.style.display = 'flex';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty) empty.style.display = 'none';
|
||||||
|
|
||||||
|
const existingCards = grid.querySelectorAll('.agent-card');
|
||||||
|
existingCards.forEach((c) => c.remove());
|
||||||
|
|
||||||
|
const fragment = document.createDocumentFragment();
|
||||||
|
AgentsUI.agents.forEach((agent) => {
|
||||||
|
const wrapper = document.createElement('div');
|
||||||
|
wrapper.innerHTML = AgentsUI.renderCard(agent);
|
||||||
|
fragment.appendChild(wrapper.firstElementChild);
|
||||||
|
});
|
||||||
|
|
||||||
|
grid.appendChild(fragment);
|
||||||
|
|
||||||
|
if (window.lucide) lucide.createIcons({ nodes: [grid] });
|
||||||
|
},
|
||||||
|
|
||||||
|
renderCard(agent) {
|
||||||
|
const name = agent.agent_name || agent.name || 'Sem nome';
|
||||||
|
const color = AgentsUI.getAvatarColor(name);
|
||||||
|
const initials = AgentsUI.getInitials(name);
|
||||||
|
const statusLabel = agent.status === 'active' ? 'Ativo' : 'Inativo';
|
||||||
|
const statusClass = agent.status === 'active' ? 'badge-active' : 'badge-inactive';
|
||||||
|
const model = (agent.config && agent.config.model) || agent.model || 'claude-sonnet-4-6';
|
||||||
|
const updatedAt = AgentsUI.formatDate(agent.updated_at || agent.updatedAt || agent.created_at || agent.createdAt);
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="agent-card" data-agent-id="${agent.id}">
|
||||||
|
<div class="agent-card-body">
|
||||||
|
<div class="agent-card-top">
|
||||||
|
<div class="agent-avatar" style="background-color: ${color}" aria-hidden="true">
|
||||||
|
<span>${initials}</span>
|
||||||
|
</div>
|
||||||
|
<div class="agent-info">
|
||||||
|
<h3 class="agent-name">${name}</h3>
|
||||||
|
<span class="badge ${statusClass}">${statusLabel}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${agent.description ? `<p class="agent-description">${agent.description}</p>` : ''}
|
||||||
|
|
||||||
|
<div class="agent-meta">
|
||||||
|
<span class="agent-meta-item">
|
||||||
|
<i data-lucide="cpu"></i>
|
||||||
|
${model}
|
||||||
|
</span>
|
||||||
|
<span class="agent-meta-item">
|
||||||
|
<i data-lucide="clock"></i>
|
||||||
|
${updatedAt}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="agent-actions">
|
||||||
|
<button class="btn btn-primary btn-sm" data-action="execute" data-id="${agent.id}">
|
||||||
|
<i data-lucide="play"></i>
|
||||||
|
Executar
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-ghost btn-sm" data-action="edit" data-id="${agent.id}">
|
||||||
|
<i data-lucide="pencil"></i>
|
||||||
|
Editar
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-ghost btn-icon btn-sm" data-action="export" data-id="${agent.id}" title="Exportar agente">
|
||||||
|
<i data-lucide="download"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-ghost btn-icon btn-sm btn-danger" data-action="delete" data-id="${agent.id}" title="Excluir agente">
|
||||||
|
<i data-lucide="trash-2"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
},
|
||||||
|
|
||||||
|
openCreateModal() {
|
||||||
|
const form = document.getElementById('agent-form');
|
||||||
|
if (form) form.reset();
|
||||||
|
|
||||||
|
const idField = document.getElementById('agent-form-id');
|
||||||
|
if (idField) idField.value = '';
|
||||||
|
|
||||||
|
const titleEl = document.getElementById('agent-modal-title');
|
||||||
|
if (titleEl) titleEl.textContent = 'Novo Agente';
|
||||||
|
|
||||||
|
const toggle = document.getElementById('agent-status-toggle');
|
||||||
|
if (toggle) toggle.checked = true;
|
||||||
|
|
||||||
|
const tagsHidden = document.getElementById('agent-tags');
|
||||||
|
if (tagsHidden) tagsHidden.value = '[]';
|
||||||
|
|
||||||
|
const tagsChips = document.getElementById('agent-tags-chips');
|
||||||
|
if (tagsChips) tagsChips.innerHTML = '';
|
||||||
|
|
||||||
|
Modal.open('agent-modal-overlay');
|
||||||
|
},
|
||||||
|
|
||||||
|
async openEditModal(agentId) {
|
||||||
|
try {
|
||||||
|
const agent = await API.agents.get(agentId);
|
||||||
|
|
||||||
|
const titleEl = document.getElementById('agent-modal-title');
|
||||||
|
if (titleEl) titleEl.textContent = 'Editar Agente';
|
||||||
|
|
||||||
|
const fields = {
|
||||||
|
'agent-form-id': agent.id,
|
||||||
|
'agent-name': agent.agent_name || agent.name || '',
|
||||||
|
'agent-description': agent.description || '',
|
||||||
|
'agent-system-prompt': (agent.config && agent.config.systemPrompt) || '',
|
||||||
|
'agent-model': (agent.config && agent.config.model) || 'claude-sonnet-4-6',
|
||||||
|
'agent-workdir': (agent.config && agent.config.workingDirectory) || '',
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [fieldId, value] of Object.entries(fields)) {
|
||||||
|
const el = document.getElementById(fieldId);
|
||||||
|
if (el) el.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggle = document.getElementById('agent-status-toggle');
|
||||||
|
if (toggle) toggle.checked = agent.status === 'active';
|
||||||
|
|
||||||
|
const tags = Array.isArray(agent.tags) ? agent.tags : [];
|
||||||
|
const tagsHidden = document.getElementById('agent-tags');
|
||||||
|
if (tagsHidden) tagsHidden.value = JSON.stringify(tags);
|
||||||
|
|
||||||
|
const tagsChips = document.getElementById('agent-tags-chips');
|
||||||
|
if (tagsChips) {
|
||||||
|
tagsChips.innerHTML = tags.map((t) =>
|
||||||
|
`<span class="tag-chip">${t}<button type="button" data-tag="${t}" class="tag-remove" aria-label="Remover tag ${t}">×</button></span>`
|
||||||
|
).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
Modal.open('agent-modal-overlay');
|
||||||
|
} catch (err) {
|
||||||
|
Toast.error(`Erro ao carregar agente: ${err.message}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async save() {
|
||||||
|
const idEl = document.getElementById('agent-form-id');
|
||||||
|
const id = idEl ? idEl.value.trim() : '';
|
||||||
|
|
||||||
|
const nameEl = document.getElementById('agent-name');
|
||||||
|
if (!nameEl || !nameEl.value.trim()) {
|
||||||
|
Toast.warning('Nome do agente é obrigatório');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tagsHidden = document.getElementById('agent-tags');
|
||||||
|
let tags = [];
|
||||||
|
try {
|
||||||
|
tags = JSON.parse(tagsHidden?.value || '[]');
|
||||||
|
} catch {
|
||||||
|
tags = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggle = document.getElementById('agent-status-toggle');
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
agent_name: nameEl.value.trim(),
|
||||||
|
description: document.getElementById('agent-description')?.value.trim() || '',
|
||||||
|
status: toggle && toggle.checked ? 'active' : 'inactive',
|
||||||
|
config: {
|
||||||
|
systemPrompt: document.getElementById('agent-system-prompt')?.value.trim() || '',
|
||||||
|
model: document.getElementById('agent-model')?.value || 'claude-sonnet-4-6',
|
||||||
|
workingDirectory: document.getElementById('agent-workdir')?.value.trim() || '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (id) {
|
||||||
|
await API.agents.update(id, data);
|
||||||
|
Toast.success('Agente atualizado com sucesso');
|
||||||
|
} else {
|
||||||
|
await API.agents.create(data);
|
||||||
|
Toast.success('Agente criado com sucesso');
|
||||||
|
}
|
||||||
|
|
||||||
|
Modal.close('agent-modal-overlay');
|
||||||
|
await AgentsUI.load();
|
||||||
|
} catch (err) {
|
||||||
|
Toast.error(`Erro ao salvar agente: ${err.message}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async delete(agentId) {
|
||||||
|
const confirmed = await Modal.confirm(
|
||||||
|
'Excluir agente',
|
||||||
|
'Tem certeza que deseja excluir este agente? Esta ação não pode ser desfeita.'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!confirmed) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await API.agents.delete(agentId);
|
||||||
|
Toast.success('Agente excluído com sucesso');
|
||||||
|
await AgentsUI.load();
|
||||||
|
} catch (err) {
|
||||||
|
Toast.error(`Erro ao excluir agente: ${err.message}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async execute(agentId) {
|
||||||
|
const agent = AgentsUI.agents.find((a) => a.id === agentId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const allAgents = AgentsUI.agents.length > 0 ? AgentsUI.agents : await API.agents.list();
|
||||||
|
const selectEl = document.getElementById('execute-agent-select');
|
||||||
|
|
||||||
|
if (selectEl) {
|
||||||
|
selectEl.innerHTML = '<option value="">Selecionar agente...</option>' +
|
||||||
|
allAgents
|
||||||
|
.filter((a) => a.status === 'active')
|
||||||
|
.map((a) => `<option value="${a.id}">${a.agent_name || a.name}</option>`)
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
selectEl.value = agentId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hiddenId = document.getElementById('execute-agent-id');
|
||||||
|
if (hiddenId) hiddenId.value = agentId;
|
||||||
|
|
||||||
|
const taskEl = document.getElementById('execute-task-desc');
|
||||||
|
if (taskEl) taskEl.value = '';
|
||||||
|
|
||||||
|
const instructionsEl = document.getElementById('execute-instructions');
|
||||||
|
if (instructionsEl) instructionsEl.value = '';
|
||||||
|
|
||||||
|
Modal.open('execute-modal-overlay');
|
||||||
|
} catch (err) {
|
||||||
|
Toast.error(`Erro ao abrir modal de execução: ${err.message}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async export(agentId) {
|
||||||
|
try {
|
||||||
|
const data = await API.agents.export(agentId);
|
||||||
|
const jsonEl = document.getElementById('export-code-content');
|
||||||
|
if (jsonEl) jsonEl.textContent = JSON.stringify(data, null, 2);
|
||||||
|
Modal.open('export-modal-overlay');
|
||||||
|
} catch (err) {
|
||||||
|
Toast.error(`Erro ao exportar agente: ${err.message}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
getAvatarColor(name) {
|
||||||
|
let hash = 0;
|
||||||
|
for (let i = 0; i < name.length; i++) {
|
||||||
|
hash = name.charCodeAt(i) + ((hash << 5) - hash);
|
||||||
|
}
|
||||||
|
const index = Math.abs(hash) % AgentsUI.avatarColors.length;
|
||||||
|
return AgentsUI.avatarColors[index];
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitials(name) {
|
||||||
|
return name
|
||||||
|
.split(' ')
|
||||||
|
.slice(0, 2)
|
||||||
|
.map((w) => w[0])
|
||||||
|
.join('')
|
||||||
|
.toUpperCase();
|
||||||
|
},
|
||||||
|
|
||||||
|
formatDate(isoString) {
|
||||||
|
if (!isoString) return '—';
|
||||||
|
const date = new Date(isoString);
|
||||||
|
return date.toLocaleDateString('pt-BR', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
window.AgentsUI = AgentsUI;
|
||||||
118
public/js/components/dashboard.js
Normal file
118
public/js/components/dashboard.js
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
const DashboardUI = {
|
||||||
|
async load() {
|
||||||
|
try {
|
||||||
|
const [status, agents] = await Promise.all([
|
||||||
|
API.system.status(),
|
||||||
|
API.agents.list(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
DashboardUI.updateMetrics(status, agents);
|
||||||
|
DashboardUI.updateRecentActivity(status.executions?.list || []);
|
||||||
|
DashboardUI.updateSystemStatus(status);
|
||||||
|
} catch (err) {
|
||||||
|
Toast.error(`Erro ao carregar dashboard: ${err.message}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
updateMetrics(status, agents) {
|
||||||
|
const metrics = {
|
||||||
|
'metric-total-agents': status.agents?.total ?? (agents?.length ?? 0),
|
||||||
|
'metric-active-agents': status.agents?.active ?? 0,
|
||||||
|
'metric-executions-today': status.executions?.active ?? 0,
|
||||||
|
'metric-schedules': status.schedules?.total ?? 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const [id, target] of Object.entries(metrics)) {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if (!el) continue;
|
||||||
|
|
||||||
|
const current = parseInt(el.textContent, 10) || 0;
|
||||||
|
DashboardUI._animateCount(el, current, target);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_animateCount(el, from, to) {
|
||||||
|
const duration = 600;
|
||||||
|
const start = performance.now();
|
||||||
|
|
||||||
|
const step = (now) => {
|
||||||
|
const elapsed = now - start;
|
||||||
|
const progress = Math.min(elapsed / duration, 1);
|
||||||
|
const eased = 1 - Math.pow(1 - progress, 3);
|
||||||
|
const value = Math.round(from + (to - from) * eased);
|
||||||
|
el.textContent = value;
|
||||||
|
|
||||||
|
if (progress < 1) requestAnimationFrame(step);
|
||||||
|
};
|
||||||
|
|
||||||
|
requestAnimationFrame(step);
|
||||||
|
},
|
||||||
|
|
||||||
|
updateRecentActivity(executions) {
|
||||||
|
const list = document.getElementById('activity-list');
|
||||||
|
if (!list) return;
|
||||||
|
|
||||||
|
if (!executions || executions.length === 0) {
|
||||||
|
list.innerHTML = `
|
||||||
|
<li class="activity-empty">
|
||||||
|
<i data-lucide="inbox"></i>
|
||||||
|
<span>Nenhuma execução recente</span>
|
||||||
|
</li>
|
||||||
|
`;
|
||||||
|
if (window.lucide) lucide.createIcons({ nodes: [list] });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
list.innerHTML = executions.map((exec) => {
|
||||||
|
const statusClass = DashboardUI._statusBadgeClass(exec.status);
|
||||||
|
const statusLabel = DashboardUI._statusLabel(exec.status);
|
||||||
|
const time = exec.startedAt
|
||||||
|
? new Date(exec.startedAt).toLocaleTimeString('pt-BR')
|
||||||
|
: '—';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<li class="activity-item">
|
||||||
|
<div class="activity-item-info">
|
||||||
|
<span class="activity-item-agent">${exec.agentName || exec.agentId || 'Agente'}</span>
|
||||||
|
<span class="activity-item-task">${exec.task || ''}</span>
|
||||||
|
</div>
|
||||||
|
<div class="activity-item-meta">
|
||||||
|
<span class="badge ${statusClass}">${statusLabel}</span>
|
||||||
|
<span class="activity-item-time">${time}</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
},
|
||||||
|
|
||||||
|
updateSystemStatus(status) {
|
||||||
|
const wsBadge = document.getElementById('system-ws-status-badge');
|
||||||
|
if (wsBadge) {
|
||||||
|
const wsConnected = document.getElementById('ws-indicator')?.classList.contains('ws-indicator--connected');
|
||||||
|
wsBadge.textContent = wsConnected ? 'Conectado' : 'Desconectado';
|
||||||
|
wsBadge.className = `badge ${wsConnected ? 'badge--green' : 'badge--red'}`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_statusBadgeClass(status) {
|
||||||
|
const map = {
|
||||||
|
running: 'badge--blue',
|
||||||
|
completed: 'badge--green',
|
||||||
|
error: 'badge--red',
|
||||||
|
cancelled: 'badge--gray',
|
||||||
|
};
|
||||||
|
return map[status] || 'badge--gray';
|
||||||
|
},
|
||||||
|
|
||||||
|
_statusLabel(status) {
|
||||||
|
const map = {
|
||||||
|
running: 'Em execução',
|
||||||
|
completed: 'Concluído',
|
||||||
|
error: 'Erro',
|
||||||
|
cancelled: 'Cancelado',
|
||||||
|
};
|
||||||
|
return map[status] || status || 'Desconhecido';
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
window.DashboardUI = DashboardUI;
|
||||||
106
public/js/components/modal.js
Normal file
106
public/js/components/modal.js
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
const Modal = {
|
||||||
|
_confirmResolve: null,
|
||||||
|
|
||||||
|
open(modalId) {
|
||||||
|
const overlay = document.getElementById(modalId);
|
||||||
|
if (!overlay) return;
|
||||||
|
|
||||||
|
overlay.hidden = false;
|
||||||
|
requestAnimationFrame(() => overlay.classList.add('active'));
|
||||||
|
|
||||||
|
const firstInput = overlay.querySelector('input:not([type="hidden"]), textarea, select');
|
||||||
|
if (firstInput) {
|
||||||
|
setTimeout(() => firstInput.focus(), 50);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
close(modalId) {
|
||||||
|
const overlay = document.getElementById(modalId);
|
||||||
|
if (!overlay) return;
|
||||||
|
|
||||||
|
overlay.classList.remove('active');
|
||||||
|
setTimeout(() => { overlay.hidden = true; }, 200);
|
||||||
|
|
||||||
|
const form = overlay.querySelector('form');
|
||||||
|
if (form) form.reset();
|
||||||
|
},
|
||||||
|
|
||||||
|
closeAll() {
|
||||||
|
document.querySelectorAll('.modal-overlay').forEach((overlay) => {
|
||||||
|
if (!overlay.hidden) {
|
||||||
|
overlay.classList.remove('active');
|
||||||
|
setTimeout(() => { overlay.hidden = true; }, 200);
|
||||||
|
const form = overlay.querySelector('form');
|
||||||
|
if (form) form.reset();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
confirm(title, message) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
Modal._confirmResolve = resolve;
|
||||||
|
|
||||||
|
const titleEl = document.getElementById('confirm-modal-title');
|
||||||
|
const messageEl = document.getElementById('confirm-modal-message');
|
||||||
|
|
||||||
|
if (titleEl) titleEl.textContent = title;
|
||||||
|
if (messageEl) messageEl.textContent = message;
|
||||||
|
|
||||||
|
Modal.open('confirm-modal-overlay');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_resolveConfirm(result) {
|
||||||
|
Modal.close('confirm-modal-overlay');
|
||||||
|
if (Modal._confirmResolve) {
|
||||||
|
Modal._confirmResolve(result);
|
||||||
|
Modal._confirmResolve = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_setupListeners() {
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
if (e.target.classList.contains('modal-overlay')) {
|
||||||
|
const modalId = e.target.id;
|
||||||
|
|
||||||
|
if (modalId === 'confirm-modal-overlay') {
|
||||||
|
Modal._resolveConfirm(false);
|
||||||
|
} else {
|
||||||
|
Modal.close(modalId);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeBtn = e.target.closest('[data-modal-close]');
|
||||||
|
if (closeBtn) {
|
||||||
|
const targetId = closeBtn.dataset.modalClose;
|
||||||
|
|
||||||
|
if (targetId === 'confirm-modal-overlay') {
|
||||||
|
Modal._resolveConfirm(false);
|
||||||
|
} else {
|
||||||
|
Modal.close(targetId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key !== 'Escape') return;
|
||||||
|
|
||||||
|
const activeModal = document.querySelector('.modal-overlay.active');
|
||||||
|
if (!activeModal) return;
|
||||||
|
|
||||||
|
if (activeModal.id === 'confirm-modal-overlay') {
|
||||||
|
Modal._resolveConfirm(false);
|
||||||
|
} else {
|
||||||
|
Modal.close(activeModal.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const confirmBtn = document.getElementById('confirm-modal-confirm-btn');
|
||||||
|
if (confirmBtn) confirmBtn.addEventListener('click', () => Modal._resolveConfirm(true));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => Modal._setupListeners());
|
||||||
|
|
||||||
|
window.Modal = Modal;
|
||||||
361
public/js/components/pipelines.js
Normal file
361
public/js/components/pipelines.js
Normal file
@@ -0,0 +1,361 @@
|
|||||||
|
const PipelinesUI = {
|
||||||
|
pipelines: [],
|
||||||
|
agents: [],
|
||||||
|
_editingId: null,
|
||||||
|
_steps: [],
|
||||||
|
|
||||||
|
async load() {
|
||||||
|
try {
|
||||||
|
const [pipelines, agents] = await Promise.all([
|
||||||
|
API.pipelines.list(),
|
||||||
|
API.agents.list(),
|
||||||
|
]);
|
||||||
|
PipelinesUI.pipelines = Array.isArray(pipelines) ? pipelines : [];
|
||||||
|
PipelinesUI.agents = Array.isArray(agents) ? agents : [];
|
||||||
|
PipelinesUI.render();
|
||||||
|
} catch (err) {
|
||||||
|
Toast.error(`Erro ao carregar pipelines: ${err.message}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const grid = document.getElementById('pipelines-grid');
|
||||||
|
if (!grid) return;
|
||||||
|
|
||||||
|
const existingCards = grid.querySelectorAll('.pipeline-card');
|
||||||
|
existingCards.forEach((c) => c.remove());
|
||||||
|
|
||||||
|
const emptyState = grid.querySelector('.empty-state');
|
||||||
|
|
||||||
|
if (PipelinesUI.pipelines.length === 0) {
|
||||||
|
if (!emptyState) {
|
||||||
|
grid.insertAdjacentHTML('beforeend', PipelinesUI.renderEmpty());
|
||||||
|
}
|
||||||
|
if (window.lucide) lucide.createIcons({ nodes: [grid] });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (emptyState) emptyState.remove();
|
||||||
|
|
||||||
|
const fragment = document.createDocumentFragment();
|
||||||
|
PipelinesUI.pipelines.forEach((pipeline) => {
|
||||||
|
const wrapper = document.createElement('div');
|
||||||
|
wrapper.innerHTML = PipelinesUI.renderCard(pipeline);
|
||||||
|
fragment.appendChild(wrapper.firstElementChild);
|
||||||
|
});
|
||||||
|
|
||||||
|
grid.appendChild(fragment);
|
||||||
|
|
||||||
|
if (window.lucide) lucide.createIcons({ nodes: [grid] });
|
||||||
|
},
|
||||||
|
|
||||||
|
renderEmpty() {
|
||||||
|
return `
|
||||||
|
<div class="empty-state">
|
||||||
|
<div class="empty-state-icon">
|
||||||
|
<i data-lucide="git-merge"></i>
|
||||||
|
</div>
|
||||||
|
<h3 class="empty-state-title">Nenhum pipeline cadastrado</h3>
|
||||||
|
<p class="empty-state-desc">Crie seu primeiro pipeline para encadear agentes em fluxos automatizados.</p>
|
||||||
|
<button class="btn btn--primary btn--icon-text" type="button" id="pipelines-empty-new-btn">
|
||||||
|
<i data-lucide="plus"></i>
|
||||||
|
<span>Criar Pipeline</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
},
|
||||||
|
|
||||||
|
renderCard(pipeline) {
|
||||||
|
const steps = Array.isArray(pipeline.steps) ? pipeline.steps : [];
|
||||||
|
const stepCount = steps.length;
|
||||||
|
|
||||||
|
const flowHtml = steps.map((step, index) => {
|
||||||
|
const agentName = step.agentName || step.agentId || 'Agente';
|
||||||
|
const isLast = index === steps.length - 1;
|
||||||
|
return `
|
||||||
|
<span class="pipeline-step-badge">
|
||||||
|
<span class="pipeline-step-number">${index + 1}</span>
|
||||||
|
${agentName}
|
||||||
|
</span>
|
||||||
|
${!isLast ? '<span class="pipeline-flow-arrow">→</span>' : ''}
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="agent-card pipeline-card" data-pipeline-id="${pipeline.id}">
|
||||||
|
<div class="agent-card-body">
|
||||||
|
<div class="agent-card-top">
|
||||||
|
<div class="agent-info">
|
||||||
|
<h3 class="agent-name">${pipeline.name || 'Sem nome'}</h3>
|
||||||
|
<span class="badge badge-active">${stepCount} ${stepCount === 1 ? 'passo' : 'passos'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${pipeline.description ? `<p class="agent-description">${pipeline.description}</p>` : ''}
|
||||||
|
|
||||||
|
<div class="pipeline-flow">
|
||||||
|
${flowHtml || '<span class="agent-description">Nenhum passo configurado</span>'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="agent-actions">
|
||||||
|
<button class="btn btn-primary btn-sm" data-action="execute-pipeline" data-id="${pipeline.id}">
|
||||||
|
<i data-lucide="play"></i>
|
||||||
|
Executar
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-ghost btn-sm" data-action="edit-pipeline" data-id="${pipeline.id}">
|
||||||
|
<i data-lucide="pencil"></i>
|
||||||
|
Editar
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-ghost btn-icon btn-sm btn-danger" data-action="delete-pipeline" data-id="${pipeline.id}" title="Excluir pipeline">
|
||||||
|
<i data-lucide="trash-2"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
},
|
||||||
|
|
||||||
|
openCreateModal() {
|
||||||
|
PipelinesUI._editingId = null;
|
||||||
|
PipelinesUI._steps = [
|
||||||
|
{ agentId: '', inputTemplate: '' },
|
||||||
|
{ agentId: '', inputTemplate: '' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const titleEl = document.getElementById('pipeline-modal-title');
|
||||||
|
if (titleEl) titleEl.textContent = 'Novo Pipeline';
|
||||||
|
|
||||||
|
const idEl = document.getElementById('pipeline-form-id');
|
||||||
|
if (idEl) idEl.value = '';
|
||||||
|
|
||||||
|
const nameEl = document.getElementById('pipeline-name');
|
||||||
|
if (nameEl) nameEl.value = '';
|
||||||
|
|
||||||
|
const descEl = document.getElementById('pipeline-description');
|
||||||
|
if (descEl) descEl.value = '';
|
||||||
|
|
||||||
|
PipelinesUI.renderSteps();
|
||||||
|
Modal.open('pipeline-modal-overlay');
|
||||||
|
},
|
||||||
|
|
||||||
|
async openEditModal(pipelineId) {
|
||||||
|
try {
|
||||||
|
const pipeline = await API.pipelines.get(pipelineId);
|
||||||
|
|
||||||
|
PipelinesUI._editingId = pipelineId;
|
||||||
|
PipelinesUI._steps = Array.isArray(pipeline.steps)
|
||||||
|
? pipeline.steps.map((s) => ({ agentId: s.agentId || '', inputTemplate: s.inputTemplate || '' }))
|
||||||
|
: [];
|
||||||
|
|
||||||
|
const titleEl = document.getElementById('pipeline-modal-title');
|
||||||
|
if (titleEl) titleEl.textContent = 'Editar Pipeline';
|
||||||
|
|
||||||
|
const idEl = document.getElementById('pipeline-form-id');
|
||||||
|
if (idEl) idEl.value = pipeline.id;
|
||||||
|
|
||||||
|
const nameEl = document.getElementById('pipeline-name');
|
||||||
|
if (nameEl) nameEl.value = pipeline.name || '';
|
||||||
|
|
||||||
|
const descEl = document.getElementById('pipeline-description');
|
||||||
|
if (descEl) descEl.value = pipeline.description || '';
|
||||||
|
|
||||||
|
PipelinesUI.renderSteps();
|
||||||
|
Modal.open('pipeline-modal-overlay');
|
||||||
|
} catch (err) {
|
||||||
|
Toast.error(`Erro ao carregar pipeline: ${err.message}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
renderSteps() {
|
||||||
|
const container = document.getElementById('pipeline-steps-container');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
if (PipelinesUI._steps.length === 0) {
|
||||||
|
container.innerHTML = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const agentOptions = PipelinesUI.agents
|
||||||
|
.map((a) => `<option value="${a.id}">${a.agent_name || a.name}</option>`)
|
||||||
|
.join('');
|
||||||
|
|
||||||
|
container.innerHTML = PipelinesUI._steps.map((step, index) => {
|
||||||
|
const isFirst = index === 0;
|
||||||
|
const isLast = index === PipelinesUI._steps.length - 1;
|
||||||
|
const connectorHtml = !isLast
|
||||||
|
? '<div class="pipeline-step-connector"><i data-lucide="arrow-down" style="width:14px;height:14px"></i></div>'
|
||||||
|
: '';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="pipeline-step-row" data-step-index="${index}">
|
||||||
|
<span class="pipeline-step-number-lg">${index + 1}</span>
|
||||||
|
<div class="pipeline-step-content">
|
||||||
|
<select class="select" data-step-field="agentId" data-step-index="${index}">
|
||||||
|
<option value="">Selecionar agente...</option>
|
||||||
|
${agentOptions}
|
||||||
|
</select>
|
||||||
|
<textarea
|
||||||
|
class="textarea"
|
||||||
|
rows="2"
|
||||||
|
placeholder="{{input}} será substituído pelo output anterior"
|
||||||
|
data-step-field="inputTemplate"
|
||||||
|
data-step-index="${index}"
|
||||||
|
>${step.inputTemplate || ''}</textarea>
|
||||||
|
</div>
|
||||||
|
<div class="pipeline-step-actions">
|
||||||
|
<button class="btn btn-ghost btn-icon btn-sm" type="button" data-step-action="move-up" data-step-index="${index}" title="Mover para cima" ${isFirst ? 'disabled' : ''}>
|
||||||
|
<i data-lucide="chevron-up" style="width:14px;height:14px"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-ghost btn-icon btn-sm" type="button" data-step-action="move-down" data-step-index="${index}" title="Mover para baixo" ${isLast ? 'disabled' : ''}>
|
||||||
|
<i data-lucide="chevron-down" style="width:14px;height:14px"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-ghost btn-icon btn-sm btn-danger" type="button" data-step-action="remove" data-step-index="${index}" title="Remover passo">
|
||||||
|
<i data-lucide="x" style="width:14px;height:14px"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
${connectorHtml}
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
container.querySelectorAll('select[data-step-field="agentId"]').forEach((select) => {
|
||||||
|
const index = parseInt(select.dataset.stepIndex, 10);
|
||||||
|
select.value = PipelinesUI._steps[index].agentId || '';
|
||||||
|
});
|
||||||
|
|
||||||
|
if (window.lucide) lucide.createIcons({ nodes: [container] });
|
||||||
|
},
|
||||||
|
|
||||||
|
_syncStepsFromDOM() {
|
||||||
|
const container = document.getElementById('pipeline-steps-container');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
container.querySelectorAll('[data-step-field]').forEach((el) => {
|
||||||
|
const index = parseInt(el.dataset.stepIndex, 10);
|
||||||
|
const field = el.dataset.stepField;
|
||||||
|
if (PipelinesUI._steps[index] !== undefined) {
|
||||||
|
PipelinesUI._steps[index][field] = el.value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
addStep() {
|
||||||
|
PipelinesUI._syncStepsFromDOM();
|
||||||
|
PipelinesUI._steps.push({ agentId: '', inputTemplate: '' });
|
||||||
|
PipelinesUI.renderSteps();
|
||||||
|
},
|
||||||
|
|
||||||
|
removeStep(index) {
|
||||||
|
PipelinesUI._syncStepsFromDOM();
|
||||||
|
PipelinesUI._steps.splice(index, 1);
|
||||||
|
PipelinesUI.renderSteps();
|
||||||
|
},
|
||||||
|
|
||||||
|
moveStep(index, direction) {
|
||||||
|
PipelinesUI._syncStepsFromDOM();
|
||||||
|
const targetIndex = index + direction;
|
||||||
|
if (targetIndex < 0 || targetIndex >= PipelinesUI._steps.length) return;
|
||||||
|
const temp = PipelinesUI._steps[index];
|
||||||
|
PipelinesUI._steps[index] = PipelinesUI._steps[targetIndex];
|
||||||
|
PipelinesUI._steps[targetIndex] = temp;
|
||||||
|
PipelinesUI.renderSteps();
|
||||||
|
},
|
||||||
|
|
||||||
|
async save() {
|
||||||
|
PipelinesUI._syncStepsFromDOM();
|
||||||
|
|
||||||
|
const name = document.getElementById('pipeline-name')?.value.trim();
|
||||||
|
if (!name) {
|
||||||
|
Toast.warning('Nome do pipeline é obrigatório');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PipelinesUI._steps.length < 2) {
|
||||||
|
Toast.warning('O pipeline precisa de pelo menos 2 passos');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const invalidStep = PipelinesUI._steps.find((s) => !s.agentId);
|
||||||
|
if (invalidStep) {
|
||||||
|
Toast.warning('Todos os passos devem ter um agente selecionado');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
name,
|
||||||
|
description: document.getElementById('pipeline-description')?.value.trim() || '',
|
||||||
|
steps: PipelinesUI._steps.map((s) => ({
|
||||||
|
agentId: s.agentId,
|
||||||
|
inputTemplate: s.inputTemplate || '',
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (PipelinesUI._editingId) {
|
||||||
|
await API.pipelines.update(PipelinesUI._editingId, data);
|
||||||
|
Toast.success('Pipeline atualizado com sucesso');
|
||||||
|
} else {
|
||||||
|
await API.pipelines.create(data);
|
||||||
|
Toast.success('Pipeline criado com sucesso');
|
||||||
|
}
|
||||||
|
|
||||||
|
Modal.close('pipeline-modal-overlay');
|
||||||
|
await PipelinesUI.load();
|
||||||
|
} catch (err) {
|
||||||
|
Toast.error(`Erro ao salvar pipeline: ${err.message}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async delete(pipelineId) {
|
||||||
|
const confirmed = await Modal.confirm(
|
||||||
|
'Excluir pipeline',
|
||||||
|
'Tem certeza que deseja excluir este pipeline? Esta ação não pode ser desfeita.'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!confirmed) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await API.pipelines.delete(pipelineId);
|
||||||
|
Toast.success('Pipeline excluído com sucesso');
|
||||||
|
await PipelinesUI.load();
|
||||||
|
} catch (err) {
|
||||||
|
Toast.error(`Erro ao excluir pipeline: ${err.message}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
execute(pipelineId) {
|
||||||
|
const pipeline = PipelinesUI.pipelines.find((p) => p.id === pipelineId);
|
||||||
|
|
||||||
|
const titleEl = document.getElementById('pipeline-execute-title');
|
||||||
|
if (titleEl) titleEl.textContent = `Executar: ${pipeline ? pipeline.name : 'Pipeline'}`;
|
||||||
|
|
||||||
|
const idEl = document.getElementById('pipeline-execute-id');
|
||||||
|
if (idEl) idEl.value = pipelineId;
|
||||||
|
|
||||||
|
const inputEl = document.getElementById('pipeline-execute-input');
|
||||||
|
if (inputEl) inputEl.value = '';
|
||||||
|
|
||||||
|
Modal.open('pipeline-execute-modal-overlay');
|
||||||
|
},
|
||||||
|
|
||||||
|
async _executeFromModal() {
|
||||||
|
const pipelineId = document.getElementById('pipeline-execute-id')?.value;
|
||||||
|
const input = document.getElementById('pipeline-execute-input')?.value.trim();
|
||||||
|
|
||||||
|
if (!input) {
|
||||||
|
Toast.warning('O input inicial é obrigatório');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await API.pipelines.execute(pipelineId, input);
|
||||||
|
Modal.close('pipeline-execute-modal-overlay');
|
||||||
|
App.navigateTo('terminal');
|
||||||
|
Toast.info('Pipeline iniciado');
|
||||||
|
} catch (err) {
|
||||||
|
Toast.error(`Erro ao executar pipeline: ${err.message}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
window.PipelinesUI = PipelinesUI;
|
||||||
182
public/js/components/schedules.js
Normal file
182
public/js/components/schedules.js
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
const SchedulesUI = {
|
||||||
|
schedules: [],
|
||||||
|
|
||||||
|
async load() {
|
||||||
|
try {
|
||||||
|
SchedulesUI.schedules = await API.schedules.list();
|
||||||
|
SchedulesUI.render();
|
||||||
|
} catch (err) {
|
||||||
|
Toast.error(`Erro ao carregar agendamentos: ${err.message}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const tbody = document.getElementById('schedules-tbody');
|
||||||
|
if (!tbody) return;
|
||||||
|
|
||||||
|
if (SchedulesUI.schedules.length === 0) {
|
||||||
|
tbody.innerHTML = `
|
||||||
|
<tr class="table-empty-row">
|
||||||
|
<td colspan="6">
|
||||||
|
<div class="empty-state empty-state--inline">
|
||||||
|
<i data-lucide="clock"></i>
|
||||||
|
<span>Nenhum agendamento configurado</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
if (window.lucide) lucide.createIcons({ nodes: [tbody] });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody.innerHTML = SchedulesUI.schedules.map((schedule) => {
|
||||||
|
const cronExpr = schedule.cronExpression || schedule.cronExpr || '';
|
||||||
|
const statusClass = schedule.active ? 'badge-active' : 'badge-inactive';
|
||||||
|
const statusLabel = schedule.active ? 'Ativo' : 'Inativo';
|
||||||
|
const humanCron = SchedulesUI.cronToHuman(cronExpr);
|
||||||
|
const nextRun = schedule.nextRun
|
||||||
|
? new Date(schedule.nextRun).toLocaleString('pt-BR')
|
||||||
|
: '—';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<tr>
|
||||||
|
<td>${schedule.agentName || schedule.agentId || '—'}</td>
|
||||||
|
<td class="schedule-task-cell" title="${schedule.taskDescription || ''}">${schedule.taskDescription || '—'}</td>
|
||||||
|
<td>
|
||||||
|
<span title="${cronExpr}">${humanCron}</span>
|
||||||
|
<small class="font-mono">${cronExpr}</small>
|
||||||
|
</td>
|
||||||
|
<td>${nextRun}</td>
|
||||||
|
<td><span class="badge ${statusClass}">${statusLabel}</span></td>
|
||||||
|
<td>
|
||||||
|
<button
|
||||||
|
class="btn btn-ghost btn-sm btn-danger"
|
||||||
|
data-action="delete-schedule"
|
||||||
|
data-id="${schedule.taskId}"
|
||||||
|
title="Remover agendamento"
|
||||||
|
aria-label="Remover agendamento"
|
||||||
|
>
|
||||||
|
<i data-lucide="trash-2"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
if (window.lucide) lucide.createIcons({ nodes: [tbody] });
|
||||||
|
},
|
||||||
|
|
||||||
|
async openCreateModal() {
|
||||||
|
try {
|
||||||
|
const agents = await API.agents.list();
|
||||||
|
const select = document.getElementById('schedule-agent');
|
||||||
|
|
||||||
|
if (select) {
|
||||||
|
select.innerHTML = '<option value="">Selecionar agente...</option>' +
|
||||||
|
agents
|
||||||
|
.filter((a) => a.status === 'active')
|
||||||
|
.map((a) => `<option value="${a.id}">${a.agent_name || a.name}</option>`)
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
const taskEl = document.getElementById('schedule-task');
|
||||||
|
if (taskEl) taskEl.value = '';
|
||||||
|
|
||||||
|
const cronEl = document.getElementById('schedule-cron');
|
||||||
|
if (cronEl) cronEl.value = '';
|
||||||
|
|
||||||
|
Modal.open('schedule-modal-overlay');
|
||||||
|
} catch (err) {
|
||||||
|
Toast.error(`Erro ao abrir modal de agendamento: ${err.message}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async save() {
|
||||||
|
const agentId = document.getElementById('schedule-agent')?.value;
|
||||||
|
const taskDescription = document.getElementById('schedule-task')?.value.trim();
|
||||||
|
const cronExpression = document.getElementById('schedule-cron')?.value.trim();
|
||||||
|
|
||||||
|
if (!agentId) {
|
||||||
|
Toast.warning('Selecione um agente');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!taskDescription) {
|
||||||
|
Toast.warning('Descrição da tarefa é obrigatória');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cronExpression) {
|
||||||
|
Toast.warning('Expressão cron é obrigatória');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await API.schedules.create({ agentId, taskDescription, cronExpression });
|
||||||
|
Toast.success('Agendamento criado com sucesso');
|
||||||
|
Modal.close('schedule-modal-overlay');
|
||||||
|
await SchedulesUI.load();
|
||||||
|
} catch (err) {
|
||||||
|
Toast.error(`Erro ao criar agendamento: ${err.message}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async delete(taskId) {
|
||||||
|
const confirmed = await Modal.confirm(
|
||||||
|
'Remover agendamento',
|
||||||
|
'Tem certeza que deseja remover este agendamento?'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!confirmed) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await API.schedules.delete(taskId);
|
||||||
|
Toast.success('Agendamento removido com sucesso');
|
||||||
|
await SchedulesUI.load();
|
||||||
|
} catch (err) {
|
||||||
|
Toast.error(`Erro ao remover agendamento: ${err.message}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
cronToHuman(expression) {
|
||||||
|
if (!expression) return '—';
|
||||||
|
|
||||||
|
const presets = {
|
||||||
|
'* * * * *': 'A cada minuto',
|
||||||
|
'*/5 * * * *': 'A cada 5 minutos',
|
||||||
|
'*/10 * * * *': 'A cada 10 minutos',
|
||||||
|
'*/15 * * * *': 'A cada 15 minutos',
|
||||||
|
'*/30 * * * *': 'A cada 30 minutos',
|
||||||
|
'0 * * * *': 'A cada hora',
|
||||||
|
'0 */2 * * *': 'A cada 2 horas',
|
||||||
|
'0 */6 * * *': 'A cada 6 horas',
|
||||||
|
'0 */12 * * *': 'A cada 12 horas',
|
||||||
|
'0 0 * * *': 'Todo dia à meia-noite',
|
||||||
|
'0 9 * * *': 'Todo dia às 9h',
|
||||||
|
'0 18 * * *': 'Todo dia às 18h',
|
||||||
|
'0 0 * * 1': 'Toda segunda-feira',
|
||||||
|
'0 0 * * 1-5': 'Dias úteis à meia-noite',
|
||||||
|
'0 9 * * 1-5': 'Dias úteis às 9h',
|
||||||
|
'0 9 * * 1': 'Semanal (seg 09:00)',
|
||||||
|
'0 0 1 * *': 'Todo primeiro do mês',
|
||||||
|
'0 0 1 1 *': 'Todo 1º de janeiro',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (presets[expression]) return presets[expression];
|
||||||
|
|
||||||
|
const parts = expression.split(' ');
|
||||||
|
if (parts.length !== 5) return expression;
|
||||||
|
|
||||||
|
const [minute, hour, day, month, weekday] = parts;
|
||||||
|
|
||||||
|
if (minute.startsWith('*/')) return `A cada ${minute.slice(2)} minutos`;
|
||||||
|
if (hour.startsWith('*/') && minute === '0') return `A cada ${hour.slice(2)} horas`;
|
||||||
|
if (minute === '0' && hour !== '*' && day === '*' && month === '*' && weekday === '*') {
|
||||||
|
return `Todo dia às ${hour.padStart(2, '0')}h`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return expression;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
window.SchedulesUI = SchedulesUI;
|
||||||
191
public/js/components/tasks.js
Normal file
191
public/js/components/tasks.js
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
const TasksUI = {
|
||||||
|
tasks: [],
|
||||||
|
|
||||||
|
async load() {
|
||||||
|
try {
|
||||||
|
TasksUI.tasks = await API.tasks.list();
|
||||||
|
TasksUI.render();
|
||||||
|
} catch (err) {
|
||||||
|
Toast.error(`Erro ao carregar tarefas: ${err.message}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const container = document.getElementById('tasks-grid');
|
||||||
|
const empty = document.getElementById('tasks-empty-state');
|
||||||
|
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const existingCards = container.querySelectorAll('.task-card');
|
||||||
|
existingCards.forEach((c) => c.remove());
|
||||||
|
|
||||||
|
if (TasksUI.tasks.length === 0) {
|
||||||
|
if (empty) empty.style.display = 'flex';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty) empty.style.display = 'none';
|
||||||
|
|
||||||
|
const fragment = document.createDocumentFragment();
|
||||||
|
|
||||||
|
TasksUI.tasks.forEach((task) => {
|
||||||
|
const wrapper = document.createElement('div');
|
||||||
|
wrapper.innerHTML = TasksUI._renderCard(task);
|
||||||
|
fragment.appendChild(wrapper.firstElementChild);
|
||||||
|
});
|
||||||
|
|
||||||
|
container.appendChild(fragment);
|
||||||
|
|
||||||
|
if (window.lucide) lucide.createIcons({ nodes: [container] });
|
||||||
|
},
|
||||||
|
|
||||||
|
_renderCard(task) {
|
||||||
|
const categoryClass = TasksUI._categoryClass(task.category);
|
||||||
|
const categoryLabel = task.category || 'Geral';
|
||||||
|
const createdAt = TasksUI._formatDate(task.createdAt);
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="task-card" data-task-id="${task.id}">
|
||||||
|
<div class="task-card-header">
|
||||||
|
<h4 class="task-card-name">${task.name}</h4>
|
||||||
|
<span class="badge ${categoryClass}">${categoryLabel}</span>
|
||||||
|
</div>
|
||||||
|
${task.description ? `<p class="task-card-description">${task.description}</p>` : ''}
|
||||||
|
<div class="task-card-footer">
|
||||||
|
<span class="task-card-date">
|
||||||
|
<i data-lucide="calendar"></i>
|
||||||
|
${createdAt}
|
||||||
|
</span>
|
||||||
|
<div class="task-card-actions">
|
||||||
|
<button class="btn btn--ghost btn--sm" data-action="edit-task" data-id="${task.id}" title="Editar tarefa">
|
||||||
|
<i data-lucide="pencil"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn--ghost btn--sm btn--danger" data-action="delete-task" data-id="${task.id}" title="Excluir tarefa">
|
||||||
|
<i data-lucide="trash-2"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
},
|
||||||
|
|
||||||
|
openCreateModal() {
|
||||||
|
const container = document.getElementById('tasks-grid');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const existing = document.getElementById('task-inline-form');
|
||||||
|
if (existing) {
|
||||||
|
existing.remove();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const formHtml = `
|
||||||
|
<div class="task-card task-card--form" id="task-inline-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="task-inline-name">Nome da tarefa *</label>
|
||||||
|
<input type="text" id="task-inline-name" class="input" placeholder="Ex: Code Review de PR" required autocomplete="off">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="task-inline-category">Categoria</label>
|
||||||
|
<select id="task-inline-category" class="select">
|
||||||
|
<option value="">Selecionar...</option>
|
||||||
|
<option value="code-review">Code Review</option>
|
||||||
|
<option value="security">Segurança</option>
|
||||||
|
<option value="refactor">Refatoração</option>
|
||||||
|
<option value="tests">Testes</option>
|
||||||
|
<option value="docs">Documentação</option>
|
||||||
|
<option value="performance">Performance</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="task-inline-description">Descrição</label>
|
||||||
|
<textarea id="task-inline-description" class="textarea" rows="2" placeholder="Descreva o objetivo desta tarefa..."></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="form-actions">
|
||||||
|
<button class="btn btn--primary" id="btn-save-inline-task" type="button">Salvar</button>
|
||||||
|
<button class="btn btn--ghost" id="btn-cancel-inline-task" type="button">Cancelar</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const empty = document.getElementById('tasks-empty-state');
|
||||||
|
if (empty) empty.style.display = 'none';
|
||||||
|
|
||||||
|
container.insertAdjacentHTML('afterbegin', formHtml);
|
||||||
|
|
||||||
|
document.getElementById('btn-save-inline-task')?.addEventListener('click', () => {
|
||||||
|
const name = document.getElementById('task-inline-name')?.value.trim();
|
||||||
|
const category = document.getElementById('task-inline-category')?.value;
|
||||||
|
const description = document.getElementById('task-inline-description')?.value.trim();
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
Toast.warning('Nome da tarefa é obrigatório');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TasksUI.save({ name, category, description });
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('btn-cancel-inline-task')?.addEventListener('click', () => {
|
||||||
|
document.getElementById('task-inline-form')?.remove();
|
||||||
|
if (TasksUI.tasks.length === 0) {
|
||||||
|
const emptyEl = document.getElementById('tasks-empty-state');
|
||||||
|
if (emptyEl) emptyEl.style.display = 'flex';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('task-inline-name')?.focus();
|
||||||
|
},
|
||||||
|
|
||||||
|
async save(data) {
|
||||||
|
if (!data || !data.name) {
|
||||||
|
Toast.warning('Nome da tarefa é obrigatório');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await API.tasks.create(data);
|
||||||
|
Toast.success('Tarefa criada com sucesso');
|
||||||
|
document.getElementById('task-inline-form')?.remove();
|
||||||
|
await TasksUI.load();
|
||||||
|
} catch (err) {
|
||||||
|
Toast.error(`Erro ao salvar tarefa: ${err.message}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async delete(taskId) {
|
||||||
|
const confirmed = await Modal.confirm(
|
||||||
|
'Excluir tarefa',
|
||||||
|
'Tem certeza que deseja excluir esta tarefa?'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!confirmed) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await API.tasks.delete(taskId);
|
||||||
|
Toast.success('Tarefa excluída com sucesso');
|
||||||
|
await TasksUI.load();
|
||||||
|
} catch (err) {
|
||||||
|
Toast.error(`Erro ao excluir tarefa: ${err.message}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_categoryClass(category) {
|
||||||
|
const map = {
|
||||||
|
'code-review': 'badge--blue',
|
||||||
|
security: 'badge--red',
|
||||||
|
refactor: 'badge--purple',
|
||||||
|
tests: 'badge--green',
|
||||||
|
docs: 'badge--gray',
|
||||||
|
performance: 'badge--orange',
|
||||||
|
};
|
||||||
|
return map[(category || '').toLowerCase()] || 'badge--gray';
|
||||||
|
},
|
||||||
|
|
||||||
|
_formatDate(isoString) {
|
||||||
|
if (!isoString) return '—';
|
||||||
|
return new Date(isoString).toLocaleDateString('pt-BR');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
window.TasksUI = TasksUI;
|
||||||
108
public/js/components/terminal.js
Normal file
108
public/js/components/terminal.js
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
const Terminal = {
|
||||||
|
lines: [],
|
||||||
|
maxLines: 1000,
|
||||||
|
autoScroll: true,
|
||||||
|
executionFilter: null,
|
||||||
|
_processingInterval: null,
|
||||||
|
|
||||||
|
addLine(content, type = 'default') {
|
||||||
|
const time = new Date();
|
||||||
|
const formatted = time.toTimeString().slice(0, 8);
|
||||||
|
|
||||||
|
Terminal.lines.push({ content, type, timestamp: formatted });
|
||||||
|
|
||||||
|
if (Terminal.lines.length > Terminal.maxLines) {
|
||||||
|
Terminal.lines.shift();
|
||||||
|
}
|
||||||
|
|
||||||
|
Terminal.render();
|
||||||
|
},
|
||||||
|
|
||||||
|
startProcessing(agentName) {
|
||||||
|
Terminal.stopProcessing();
|
||||||
|
Terminal.addLine(`Agente "${agentName}" processando tarefa...`, 'system');
|
||||||
|
|
||||||
|
let dots = 0;
|
||||||
|
Terminal._processingInterval = setInterval(() => {
|
||||||
|
dots = (dots + 1) % 4;
|
||||||
|
const indicator = document.getElementById('terminal-processing');
|
||||||
|
if (indicator) {
|
||||||
|
indicator.textContent = 'Processando' + '.'.repeat(dots + 1);
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
Terminal.render();
|
||||||
|
},
|
||||||
|
|
||||||
|
stopProcessing() {
|
||||||
|
if (Terminal._processingInterval) {
|
||||||
|
clearInterval(Terminal._processingInterval);
|
||||||
|
Terminal._processingInterval = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
Terminal.stopProcessing();
|
||||||
|
Terminal.lines = [];
|
||||||
|
Terminal.executionFilter = null;
|
||||||
|
Terminal.render();
|
||||||
|
},
|
||||||
|
|
||||||
|
setExecutionFilter(executionId) {
|
||||||
|
Terminal.executionFilter = executionId;
|
||||||
|
Terminal.render();
|
||||||
|
},
|
||||||
|
|
||||||
|
scrollToBottom() {
|
||||||
|
const output = document.getElementById('terminal-output');
|
||||||
|
if (output) output.scrollTop = output.scrollHeight;
|
||||||
|
},
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const output = document.getElementById('terminal-output');
|
||||||
|
if (!output) return;
|
||||||
|
|
||||||
|
const lines = Terminal.executionFilter
|
||||||
|
? Terminal.lines.filter((l) => l.executionId === Terminal.executionFilter)
|
||||||
|
: Terminal.lines;
|
||||||
|
|
||||||
|
if (lines.length === 0 && !Terminal._processingInterval) {
|
||||||
|
output.innerHTML = `
|
||||||
|
<div class="terminal-welcome">
|
||||||
|
<span class="terminal-prompt">$</span>
|
||||||
|
<span class="terminal-text">Aguardando execução de agente...</span>
|
||||||
|
</div>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const html = lines.map((line) => {
|
||||||
|
const typeClass = line.type && line.type !== 'default' ? ' ' + line.type : '';
|
||||||
|
const escaped = Terminal._escapeHtml(line.content);
|
||||||
|
const formatted = escaped.replace(/\n/g, '<br>');
|
||||||
|
|
||||||
|
return `<div class="terminal-line${typeClass}">
|
||||||
|
<span class="timestamp">${line.timestamp}</span>
|
||||||
|
<span class="content">${formatted}</span>
|
||||||
|
</div>`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
const processing = Terminal._processingInterval
|
||||||
|
? '<div class="terminal-line system"><span class="terminal-processing-indicator"><span id="terminal-processing" class="processing-dots">Processando...</span><span class="terminal-spinner"></span></span></div>'
|
||||||
|
: '';
|
||||||
|
|
||||||
|
output.innerHTML = html + processing + '<span class="terminal-cursor blink">_</span>';
|
||||||
|
|
||||||
|
if (Terminal.autoScroll) Terminal.scrollToBottom();
|
||||||
|
},
|
||||||
|
|
||||||
|
_escapeHtml(text) {
|
||||||
|
return String(text)
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
window.Terminal = Terminal;
|
||||||
66
public/js/components/toast.js
Normal file
66
public/js/components/toast.js
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
const Toast = {
|
||||||
|
iconMap: {
|
||||||
|
success: 'check-circle',
|
||||||
|
error: 'x-circle',
|
||||||
|
info: 'info',
|
||||||
|
warning: 'alert-triangle',
|
||||||
|
},
|
||||||
|
|
||||||
|
colorMap: {
|
||||||
|
success: 'toast-success',
|
||||||
|
error: 'toast-error',
|
||||||
|
info: 'toast-info',
|
||||||
|
warning: 'toast-warning',
|
||||||
|
},
|
||||||
|
|
||||||
|
show(message, type = 'info', duration = 4000) {
|
||||||
|
const container = document.getElementById('toast-container');
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
const toast = document.createElement('div');
|
||||||
|
toast.className = `toast toast-${type}`;
|
||||||
|
|
||||||
|
const iconName = Toast.iconMap[type] || 'info';
|
||||||
|
|
||||||
|
toast.innerHTML = `
|
||||||
|
<span class="toast-icon" data-lucide="${iconName}"></span>
|
||||||
|
<span class="toast-message">${message}</span>
|
||||||
|
<button class="toast-close" aria-label="Fechar notificação">
|
||||||
|
<i data-lucide="x"></i>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const closeBtn = toast.querySelector('.toast-close');
|
||||||
|
closeBtn.addEventListener('click', () => Toast.dismiss(toast));
|
||||||
|
|
||||||
|
container.appendChild(toast);
|
||||||
|
|
||||||
|
if (window.lucide) {
|
||||||
|
lucide.createIcons({ nodes: [toast] });
|
||||||
|
}
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
toast.classList.add('toast-show');
|
||||||
|
});
|
||||||
|
|
||||||
|
if (duration > 0) {
|
||||||
|
setTimeout(() => Toast.dismiss(toast), duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
return toast;
|
||||||
|
},
|
||||||
|
|
||||||
|
dismiss(toast) {
|
||||||
|
toast.classList.remove('toast-show');
|
||||||
|
toast.classList.add('removing');
|
||||||
|
toast.addEventListener('animationend', () => toast.remove(), { once: true });
|
||||||
|
setTimeout(() => toast.remove(), 400);
|
||||||
|
},
|
||||||
|
|
||||||
|
success(message, duration) { return Toast.show(message, 'success', duration); },
|
||||||
|
error(message, duration) { return Toast.show(message, 'error', duration); },
|
||||||
|
info(message, duration) { return Toast.show(message, 'info', duration); },
|
||||||
|
warning(message, duration) { return Toast.show(message, 'warning', duration); },
|
||||||
|
};
|
||||||
|
|
||||||
|
window.Toast = Toast;
|
||||||
72
server.js
Normal file
72
server.js
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import { createServer } from 'http';
|
||||||
|
import { WebSocketServer } from 'ws';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import { dirname, join } from 'path';
|
||||||
|
import apiRouter, { setWsBroadcast } from './src/routes/api.js';
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|
||||||
|
const app = express();
|
||||||
|
const httpServer = createServer(app);
|
||||||
|
const wss = new WebSocketServer({ server: httpServer });
|
||||||
|
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||||
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
||||||
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
||||||
|
if (req.method === 'OPTIONS') return res.sendStatus(204);
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(express.static(join(__dirname, 'public')));
|
||||||
|
app.use('/api', apiRouter);
|
||||||
|
|
||||||
|
const connectedClients = new Set();
|
||||||
|
|
||||||
|
wss.on('connection', (ws) => {
|
||||||
|
connectedClients.add(ws);
|
||||||
|
|
||||||
|
ws.on('close', () => {
|
||||||
|
connectedClients.delete(ws);
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('error', () => {
|
||||||
|
connectedClients.delete(ws);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function broadcast(message) {
|
||||||
|
const payload = JSON.stringify(message);
|
||||||
|
for (const client of connectedClients) {
|
||||||
|
if (client.readyState === 1) {
|
||||||
|
client.send(payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setWsBroadcast(broadcast);
|
||||||
|
|
||||||
|
function gracefulShutdown(signal) {
|
||||||
|
console.log(`\nSinal ${signal} recebido. Encerrando servidor...`);
|
||||||
|
|
||||||
|
httpServer.close(() => {
|
||||||
|
console.log('Servidor HTTP encerrado.');
|
||||||
|
process.exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
console.error('Forçando encerramento após timeout.');
|
||||||
|
process.exit(1);
|
||||||
|
}, 10000);
|
||||||
|
}
|
||||||
|
|
||||||
|
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
||||||
|
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
||||||
|
|
||||||
|
httpServer.listen(PORT, () => {
|
||||||
|
console.log(`Painel administrativo disponível em http://localhost:${PORT}`);
|
||||||
|
console.log(`WebSocket server ativo na mesma porta.`);
|
||||||
|
});
|
||||||
189
src/agents/executor.js
Normal file
189
src/agents/executor.js
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
import { spawn } from 'child_process';
|
||||||
|
import { existsSync } from 'fs';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
|
const CLAUDE_BIN = '/home/fred/.local/bin/claude';
|
||||||
|
const DEFAULT_MODEL = 'claude-sonnet-4-6';
|
||||||
|
const activeExecutions = new Map();
|
||||||
|
|
||||||
|
function cleanEnv() {
|
||||||
|
const env = { ...process.env };
|
||||||
|
delete env.CLAUDECODE;
|
||||||
|
delete env.ANTHROPIC_API_KEY;
|
||||||
|
return env;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildArgs(agentConfig, prompt) {
|
||||||
|
const model = agentConfig.model || DEFAULT_MODEL;
|
||||||
|
const args = ['-p', prompt, '--output-format', 'stream-json', '--verbose', '--model', model];
|
||||||
|
|
||||||
|
if (agentConfig.systemPrompt) {
|
||||||
|
args.push('--system-prompt', agentConfig.systemPrompt);
|
||||||
|
}
|
||||||
|
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildPrompt(task, instructions) {
|
||||||
|
const parts = [];
|
||||||
|
if (task) parts.push(task);
|
||||||
|
if (instructions) parts.push(`\nInstruções adicionais:\n${instructions}`);
|
||||||
|
return parts.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseStreamLine(line) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed) return null;
|
||||||
|
try {
|
||||||
|
return JSON.parse(trimmed);
|
||||||
|
} catch {
|
||||||
|
return { type: 'text', content: trimmed };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractText(event) {
|
||||||
|
if (!event) return null;
|
||||||
|
|
||||||
|
if (event.type === 'assistant' && event.message?.content) {
|
||||||
|
return event.message.content
|
||||||
|
.filter((b) => b.type === 'text')
|
||||||
|
.map((b) => b.text)
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.type === 'content_block_delta' && event.delta?.text) {
|
||||||
|
return event.delta.text;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.type === 'content_block_start' && event.content_block?.text) {
|
||||||
|
return event.content_block.text;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.type === 'result') {
|
||||||
|
if (typeof event.result === 'string') return event.result;
|
||||||
|
if (event.result?.content) {
|
||||||
|
return event.result.content
|
||||||
|
.filter((b) => b.type === 'text')
|
||||||
|
.map((b) => b.text)
|
||||||
|
.join('');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.type === 'text') return event.content || null;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function execute(agentConfig, task, callbacks = {}) {
|
||||||
|
const executionId = uuidv4();
|
||||||
|
const { onData, onError, onComplete } = callbacks;
|
||||||
|
|
||||||
|
const prompt = buildPrompt(task.description || task, task.instructions);
|
||||||
|
const args = buildArgs(agentConfig, prompt);
|
||||||
|
|
||||||
|
const spawnOptions = {
|
||||||
|
env: cleanEnv(),
|
||||||
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (agentConfig.workingDirectory && agentConfig.workingDirectory.trim()) {
|
||||||
|
if (!existsSync(agentConfig.workingDirectory)) {
|
||||||
|
const err = new Error(`Diretório de trabalho não encontrado: ${agentConfig.workingDirectory}`);
|
||||||
|
if (onError) onError(err, executionId);
|
||||||
|
return executionId;
|
||||||
|
}
|
||||||
|
spawnOptions.cwd = agentConfig.workingDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[executor] Iniciando: ${executionId}`);
|
||||||
|
console.log(`[executor] Modelo: ${agentConfig.model || DEFAULT_MODEL}`);
|
||||||
|
console.log(`[executor] cwd: ${spawnOptions.cwd || process.cwd()}`);
|
||||||
|
|
||||||
|
const child = spawn(CLAUDE_BIN, args, spawnOptions);
|
||||||
|
let hadError = false;
|
||||||
|
|
||||||
|
activeExecutions.set(executionId, {
|
||||||
|
process: child,
|
||||||
|
agentConfig,
|
||||||
|
task,
|
||||||
|
startedAt: new Date().toISOString(),
|
||||||
|
executionId,
|
||||||
|
});
|
||||||
|
|
||||||
|
let outputBuffer = '';
|
||||||
|
let errorBuffer = '';
|
||||||
|
let fullText = '';
|
||||||
|
|
||||||
|
child.stdout.on('data', (chunk) => {
|
||||||
|
const raw = chunk.toString();
|
||||||
|
const lines = (outputBuffer + raw).split('\n');
|
||||||
|
outputBuffer = lines.pop();
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const parsed = parseStreamLine(line);
|
||||||
|
if (!parsed) continue;
|
||||||
|
|
||||||
|
const text = extractText(parsed);
|
||||||
|
if (text) {
|
||||||
|
fullText += text;
|
||||||
|
if (onData) onData({ type: 'chunk', content: text }, executionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
child.stderr.on('data', (chunk) => {
|
||||||
|
errorBuffer += chunk.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('error', (err) => {
|
||||||
|
console.log(`[executor][error] ${err.message}`);
|
||||||
|
hadError = true;
|
||||||
|
activeExecutions.delete(executionId);
|
||||||
|
if (onError) onError(err, executionId);
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('close', (code) => {
|
||||||
|
console.log(`[executor][close] code=${code} hadError=${hadError}`);
|
||||||
|
activeExecutions.delete(executionId);
|
||||||
|
if (hadError) return;
|
||||||
|
|
||||||
|
if (outputBuffer.trim()) {
|
||||||
|
const parsed = parseStreamLine(outputBuffer);
|
||||||
|
if (parsed) {
|
||||||
|
const text = extractText(parsed);
|
||||||
|
if (text) fullText += text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onComplete) {
|
||||||
|
onComplete(
|
||||||
|
{
|
||||||
|
executionId,
|
||||||
|
exitCode: code,
|
||||||
|
result: fullText,
|
||||||
|
stderr: errorBuffer,
|
||||||
|
},
|
||||||
|
executionId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return executionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cancel(executionId) {
|
||||||
|
const execution = activeExecutions.get(executionId);
|
||||||
|
if (!execution) return false;
|
||||||
|
|
||||||
|
execution.process.kill('SIGTERM');
|
||||||
|
activeExecutions.delete(executionId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getActiveExecutions() {
|
||||||
|
return Array.from(activeExecutions.entries()).map(([id, exec]) => ({
|
||||||
|
executionId: id,
|
||||||
|
startedAt: exec.startedAt,
|
||||||
|
agentConfig: exec.agentConfig,
|
||||||
|
}));
|
||||||
|
}
|
||||||
185
src/agents/manager.js
Normal file
185
src/agents/manager.js
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import { agentsStore } from '../store/db.js';
|
||||||
|
import * as executor from './executor.js';
|
||||||
|
import * as scheduler from './scheduler.js';
|
||||||
|
|
||||||
|
const DEFAULT_CONFIG = {
|
||||||
|
model: 'claude-sonnet-4-6',
|
||||||
|
systemPrompt: '',
|
||||||
|
workingDirectory: '',
|
||||||
|
maxTokens: 16000,
|
||||||
|
temperature: 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
function validateAgent(data) {
|
||||||
|
const errors = [];
|
||||||
|
if (!data.agent_name || typeof data.agent_name !== 'string') {
|
||||||
|
errors.push('agent_name é obrigatório e deve ser uma string');
|
||||||
|
}
|
||||||
|
if (data.config?.model && typeof data.config.model !== 'string') {
|
||||||
|
errors.push('config.model deve ser uma string');
|
||||||
|
}
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAllAgents() {
|
||||||
|
return agentsStore.getAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAgentById(id) {
|
||||||
|
return agentsStore.getById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createAgent(data) {
|
||||||
|
const errors = validateAgent(data);
|
||||||
|
if (errors.length > 0) {
|
||||||
|
throw new Error(errors.join('; '));
|
||||||
|
}
|
||||||
|
|
||||||
|
const agentData = {
|
||||||
|
agent_name: data.agent_name,
|
||||||
|
description: data.description || '',
|
||||||
|
tasks: data.tasks || [],
|
||||||
|
config: { ...DEFAULT_CONFIG, ...(data.config || {}) },
|
||||||
|
status: data.status || 'active',
|
||||||
|
assigned_host: data.assigned_host || 'localhost',
|
||||||
|
executions: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
return agentsStore.create(agentData);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateAgent(id, data) {
|
||||||
|
const existing = agentsStore.getById(id);
|
||||||
|
if (!existing) return null;
|
||||||
|
|
||||||
|
const updateData = {};
|
||||||
|
if (data.agent_name !== undefined) updateData.agent_name = data.agent_name;
|
||||||
|
if (data.description !== undefined) updateData.description = data.description;
|
||||||
|
if (data.tasks !== undefined) updateData.tasks = data.tasks;
|
||||||
|
if (data.status !== undefined) updateData.status = data.status;
|
||||||
|
if (data.assigned_host !== undefined) updateData.assigned_host = data.assigned_host;
|
||||||
|
if (data.config !== undefined) {
|
||||||
|
updateData.config = { ...existing.config, ...data.config };
|
||||||
|
}
|
||||||
|
|
||||||
|
return agentsStore.update(id, updateData);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteAgent(id) {
|
||||||
|
return agentsStore.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function executeTask(agentId, task, instructions, wsCallback) {
|
||||||
|
const agent = agentsStore.getById(agentId);
|
||||||
|
if (!agent) throw new Error(`Agente ${agentId} não encontrado`);
|
||||||
|
if (agent.status !== 'active') throw new Error(`Agente ${agentId} está inativo`);
|
||||||
|
|
||||||
|
const executionRecord = {
|
||||||
|
executionId: null,
|
||||||
|
agentId,
|
||||||
|
task: typeof task === 'string' ? task : task.description,
|
||||||
|
startedAt: new Date().toISOString(),
|
||||||
|
status: 'running',
|
||||||
|
};
|
||||||
|
|
||||||
|
const executionId = executor.execute(
|
||||||
|
agent.config,
|
||||||
|
{ description: task, instructions },
|
||||||
|
{
|
||||||
|
onData: (parsed, execId) => {
|
||||||
|
if (wsCallback) {
|
||||||
|
wsCallback({
|
||||||
|
type: 'execution_output',
|
||||||
|
executionId: execId,
|
||||||
|
agentId,
|
||||||
|
data: parsed,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (err, execId) => {
|
||||||
|
updateAgentExecution(agentId, execId, { status: 'error', error: err.message, endedAt: new Date().toISOString() });
|
||||||
|
if (wsCallback) {
|
||||||
|
wsCallback({
|
||||||
|
type: 'execution_error',
|
||||||
|
executionId: execId,
|
||||||
|
agentId,
|
||||||
|
data: { error: err.message },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onComplete: (result, execId) => {
|
||||||
|
updateAgentExecution(agentId, execId, { status: 'completed', result, endedAt: new Date().toISOString() });
|
||||||
|
if (wsCallback) {
|
||||||
|
wsCallback({
|
||||||
|
type: 'execution_complete',
|
||||||
|
executionId: execId,
|
||||||
|
agentId,
|
||||||
|
data: result,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
executionRecord.executionId = executionId;
|
||||||
|
|
||||||
|
const updatedAgent = agentsStore.getById(agentId);
|
||||||
|
const executions = [...(updatedAgent.executions || []), executionRecord];
|
||||||
|
agentsStore.update(agentId, { executions: executions.slice(-100) });
|
||||||
|
|
||||||
|
return executionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateAgentExecution(agentId, executionId, updates) {
|
||||||
|
const agent = agentsStore.getById(agentId);
|
||||||
|
if (!agent) return;
|
||||||
|
|
||||||
|
const executions = (agent.executions || []).map((exec) => {
|
||||||
|
if (exec.executionId === executionId) {
|
||||||
|
return { ...exec, ...updates };
|
||||||
|
}
|
||||||
|
return exec;
|
||||||
|
});
|
||||||
|
|
||||||
|
agentsStore.update(agentId, { executions });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function scheduleTask(agentId, taskDescription, cronExpression, wsCallback) {
|
||||||
|
const agent = agentsStore.getById(agentId);
|
||||||
|
if (!agent) throw new Error(`Agente ${agentId} não encontrado`);
|
||||||
|
|
||||||
|
const scheduleId = uuidv4();
|
||||||
|
|
||||||
|
scheduler.schedule(scheduleId, cronExpression, () => {
|
||||||
|
executeTask(agentId, taskDescription, null, wsCallback);
|
||||||
|
});
|
||||||
|
|
||||||
|
return { scheduleId, agentId, taskDescription, cronExpression };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cancelExecution(executionId) {
|
||||||
|
return executor.cancel(executionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getActiveExecutions() {
|
||||||
|
return executor.getActiveExecutions();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function exportAgent(agentId) {
|
||||||
|
const agent = agentsStore.getById(agentId);
|
||||||
|
if (!agent) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: agent.id,
|
||||||
|
agent_name: agent.agent_name,
|
||||||
|
description: agent.description,
|
||||||
|
tasks: agent.tasks,
|
||||||
|
config: agent.config,
|
||||||
|
status: agent.status,
|
||||||
|
assigned_host: agent.assigned_host,
|
||||||
|
created_at: agent.created_at,
|
||||||
|
updated_at: agent.updated_at,
|
||||||
|
executions: agent.executions || [],
|
||||||
|
};
|
||||||
|
}
|
||||||
204
src/agents/pipeline.js
Normal file
204
src/agents/pipeline.js
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
import { pipelinesStore } from '../store/db.js';
|
||||||
|
import { agentsStore } from '../store/db.js';
|
||||||
|
import * as executor from './executor.js';
|
||||||
|
|
||||||
|
const activePipelines = new Map();
|
||||||
|
|
||||||
|
function validatePipeline(data) {
|
||||||
|
const errors = [];
|
||||||
|
if (!data.name || typeof data.name !== 'string') {
|
||||||
|
errors.push('name é obrigatório e deve ser uma string');
|
||||||
|
}
|
||||||
|
if (!Array.isArray(data.steps) || data.steps.length === 0) {
|
||||||
|
errors.push('steps é obrigatório e deve ser um array não vazio');
|
||||||
|
} else {
|
||||||
|
data.steps.forEach((step, index) => {
|
||||||
|
if (!step.agentId) errors.push(`steps[${index}].agentId é obrigatório`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSteps(steps) {
|
||||||
|
return steps
|
||||||
|
.map((step, index) => ({
|
||||||
|
id: step.id || uuidv4(),
|
||||||
|
agentId: step.agentId,
|
||||||
|
order: step.order !== undefined ? step.order : index,
|
||||||
|
inputTemplate: step.inputTemplate || null,
|
||||||
|
description: step.description || '',
|
||||||
|
}))
|
||||||
|
.sort((a, b) => a.order - b.order);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTemplate(template, input) {
|
||||||
|
if (!template) return input;
|
||||||
|
return template.replace(/\{\{input\}\}/g, input);
|
||||||
|
}
|
||||||
|
|
||||||
|
function executeStepAsPromise(agentConfig, prompt, pipelineState) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const executionId = executor.execute(
|
||||||
|
agentConfig,
|
||||||
|
{ description: prompt },
|
||||||
|
{
|
||||||
|
onData: () => {},
|
||||||
|
onError: (err) => {
|
||||||
|
reject(err);
|
||||||
|
},
|
||||||
|
onComplete: (result) => {
|
||||||
|
resolve(result.result || '');
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
pipelineState.currentExecutionId = executionId;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function executePipeline(pipelineId, initialInput, wsCallback) {
|
||||||
|
const pipeline = pipelinesStore.getById(pipelineId);
|
||||||
|
if (!pipeline) throw new Error(`Pipeline ${pipelineId} não encontrado`);
|
||||||
|
|
||||||
|
const pipelineState = {
|
||||||
|
currentExecutionId: null,
|
||||||
|
currentStep: 0,
|
||||||
|
canceled: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
activePipelines.set(pipelineId, pipelineState);
|
||||||
|
|
||||||
|
const steps = buildSteps(pipeline.steps);
|
||||||
|
const results = [];
|
||||||
|
let currentInput = initialInput;
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (let i = 0; i < steps.length; i++) {
|
||||||
|
if (pipelineState.canceled) break;
|
||||||
|
|
||||||
|
const step = steps[i];
|
||||||
|
pipelineState.currentStep = i;
|
||||||
|
|
||||||
|
const agent = agentsStore.getById(step.agentId);
|
||||||
|
if (!agent) throw new Error(`Agente ${step.agentId} não encontrado no passo ${i}`);
|
||||||
|
if (agent.status !== 'active') throw new Error(`Agente ${agent.agent_name} está inativo`);
|
||||||
|
|
||||||
|
const prompt = applyTemplate(step.inputTemplate, currentInput);
|
||||||
|
|
||||||
|
if (wsCallback) {
|
||||||
|
wsCallback({
|
||||||
|
type: 'pipeline_step_start',
|
||||||
|
pipelineId,
|
||||||
|
stepIndex: i,
|
||||||
|
stepId: step.id,
|
||||||
|
agentName: agent.agent_name,
|
||||||
|
totalSteps: steps.length,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await executeStepAsPromise(agent.config, prompt, pipelineState);
|
||||||
|
|
||||||
|
if (pipelineState.canceled) break;
|
||||||
|
|
||||||
|
currentInput = result;
|
||||||
|
results.push({ stepId: step.id, agentName: agent.agent_name, result });
|
||||||
|
|
||||||
|
if (wsCallback) {
|
||||||
|
wsCallback({
|
||||||
|
type: 'pipeline_step_complete',
|
||||||
|
pipelineId,
|
||||||
|
stepIndex: i,
|
||||||
|
stepId: step.id,
|
||||||
|
result: result.slice(0, 500),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
activePipelines.delete(pipelineId);
|
||||||
|
|
||||||
|
if (!pipelineState.canceled && wsCallback) {
|
||||||
|
wsCallback({
|
||||||
|
type: 'pipeline_complete',
|
||||||
|
pipelineId,
|
||||||
|
results,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
|
} catch (err) {
|
||||||
|
activePipelines.delete(pipelineId);
|
||||||
|
|
||||||
|
if (wsCallback) {
|
||||||
|
wsCallback({
|
||||||
|
type: 'pipeline_error',
|
||||||
|
pipelineId,
|
||||||
|
stepIndex: pipelineState.currentStep,
|
||||||
|
error: err.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cancelPipeline(pipelineId) {
|
||||||
|
const state = activePipelines.get(pipelineId);
|
||||||
|
if (!state) return false;
|
||||||
|
|
||||||
|
state.canceled = true;
|
||||||
|
|
||||||
|
if (state.currentExecutionId) {
|
||||||
|
executor.cancel(state.currentExecutionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
activePipelines.delete(pipelineId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getActivePipelines() {
|
||||||
|
return Array.from(activePipelines.entries()).map(([id, state]) => ({
|
||||||
|
pipelineId: id,
|
||||||
|
currentStep: state.currentStep,
|
||||||
|
currentExecutionId: state.currentExecutionId,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createPipeline(data) {
|
||||||
|
const errors = validatePipeline(data);
|
||||||
|
if (errors.length > 0) throw new Error(errors.join('; '));
|
||||||
|
|
||||||
|
const pipelineData = {
|
||||||
|
name: data.name,
|
||||||
|
description: data.description || '',
|
||||||
|
steps: buildSteps(data.steps),
|
||||||
|
status: data.status || 'active',
|
||||||
|
};
|
||||||
|
|
||||||
|
return pipelinesStore.create(pipelineData);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updatePipeline(id, data) {
|
||||||
|
const existing = pipelinesStore.getById(id);
|
||||||
|
if (!existing) return null;
|
||||||
|
|
||||||
|
const updateData = {};
|
||||||
|
if (data.name !== undefined) updateData.name = data.name;
|
||||||
|
if (data.description !== undefined) updateData.description = data.description;
|
||||||
|
if (data.status !== undefined) updateData.status = data.status;
|
||||||
|
if (data.steps !== undefined) updateData.steps = buildSteps(data.steps);
|
||||||
|
|
||||||
|
return pipelinesStore.update(id, updateData);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deletePipeline(id) {
|
||||||
|
return pipelinesStore.delete(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPipeline(id) {
|
||||||
|
return pipelinesStore.getById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAllPipelines() {
|
||||||
|
return pipelinesStore.getAll();
|
||||||
|
}
|
||||||
84
src/agents/scheduler.js
Normal file
84
src/agents/scheduler.js
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import cron from 'node-cron';
|
||||||
|
import { EventEmitter } from 'events';
|
||||||
|
|
||||||
|
const HISTORY_LIMIT = 50;
|
||||||
|
const schedules = new Map();
|
||||||
|
const history = [];
|
||||||
|
const emitter = new EventEmitter();
|
||||||
|
|
||||||
|
function addToHistory(entry) {
|
||||||
|
history.unshift(entry);
|
||||||
|
if (history.length > HISTORY_LIMIT) {
|
||||||
|
history.splice(HISTORY_LIMIT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function schedule(taskId, cronExpr, callback) {
|
||||||
|
if (schedules.has(taskId)) {
|
||||||
|
unschedule(taskId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cron.validate(cronExpr)) {
|
||||||
|
throw new Error(`Expressão cron inválida: ${cronExpr}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const task = cron.schedule(
|
||||||
|
cronExpr,
|
||||||
|
() => {
|
||||||
|
const firedAt = new Date().toISOString();
|
||||||
|
addToHistory({ taskId, cronExpr, firedAt });
|
||||||
|
emitter.emit('scheduled-task', { taskId, firedAt });
|
||||||
|
if (callback) callback({ taskId, firedAt });
|
||||||
|
},
|
||||||
|
{ scheduled: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
schedules.set(taskId, {
|
||||||
|
taskId,
|
||||||
|
cronExpr,
|
||||||
|
task,
|
||||||
|
active: true,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return { taskId, cronExpr };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unschedule(taskId) {
|
||||||
|
const entry = schedules.get(taskId);
|
||||||
|
if (!entry) return false;
|
||||||
|
|
||||||
|
entry.task.stop();
|
||||||
|
schedules.delete(taskId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setActive(taskId, active) {
|
||||||
|
const entry = schedules.get(taskId);
|
||||||
|
if (!entry) return false;
|
||||||
|
|
||||||
|
if (active) {
|
||||||
|
entry.task.start();
|
||||||
|
} else {
|
||||||
|
entry.task.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.active = active;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSchedules() {
|
||||||
|
return Array.from(schedules.values()).map(({ task: _, ...rest }) => rest);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getHistory() {
|
||||||
|
return [...history];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function on(event, listener) {
|
||||||
|
emitter.on(event, listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function off(event, listener) {
|
||||||
|
emitter.off(event, listener);
|
||||||
|
}
|
||||||
280
src/routes/api.js
Normal file
280
src/routes/api.js
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
import { Router } from 'express';
|
||||||
|
import * as manager from '../agents/manager.js';
|
||||||
|
import { tasksStore } from '../store/db.js';
|
||||||
|
import * as scheduler from '../agents/scheduler.js';
|
||||||
|
import * as pipeline from '../agents/pipeline.js';
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
let wsbroadcast = null;
|
||||||
|
|
||||||
|
export function setWsBroadcast(fn) {
|
||||||
|
wsbroadcast = fn;
|
||||||
|
}
|
||||||
|
|
||||||
|
function wsCallback(message) {
|
||||||
|
if (wsbroadcast) wsbroadcast(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
router.get('/agents', (req, res) => {
|
||||||
|
try {
|
||||||
|
res.json(manager.getAllAgents());
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/agents/:id', (req, res) => {
|
||||||
|
try {
|
||||||
|
const agent = manager.getAgentById(req.params.id);
|
||||||
|
if (!agent) return res.status(404).json({ error: 'Agente não encontrado' });
|
||||||
|
res.json(agent);
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/agents', (req, res) => {
|
||||||
|
try {
|
||||||
|
const agent = manager.createAgent(req.body);
|
||||||
|
res.status(201).json(agent);
|
||||||
|
} catch (err) {
|
||||||
|
res.status(400).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.put('/agents/:id', (req, res) => {
|
||||||
|
try {
|
||||||
|
const agent = manager.updateAgent(req.params.id, req.body);
|
||||||
|
if (!agent) return res.status(404).json({ error: 'Agente não encontrado' });
|
||||||
|
res.json(agent);
|
||||||
|
} catch (err) {
|
||||||
|
res.status(400).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.delete('/agents/:id', (req, res) => {
|
||||||
|
try {
|
||||||
|
const deleted = manager.deleteAgent(req.params.id);
|
||||||
|
if (!deleted) return res.status(404).json({ error: 'Agente não encontrado' });
|
||||||
|
res.status(204).send();
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/agents/:id/execute', (req, res) => {
|
||||||
|
try {
|
||||||
|
const { task, instructions } = req.body;
|
||||||
|
if (!task) return res.status(400).json({ error: 'task é obrigatório' });
|
||||||
|
|
||||||
|
const executionId = manager.executeTask(req.params.id, task, instructions, wsCallback);
|
||||||
|
res.status(202).json({ executionId, status: 'started' });
|
||||||
|
} catch (err) {
|
||||||
|
const status = err.message.includes('não encontrado') ? 404 : 400;
|
||||||
|
res.status(status).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/agents/:id/cancel/:executionId', (req, res) => {
|
||||||
|
try {
|
||||||
|
const cancelled = manager.cancelExecution(req.params.executionId);
|
||||||
|
if (!cancelled) return res.status(404).json({ error: 'Execução não encontrada ou já finalizada' });
|
||||||
|
res.json({ cancelled: true });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/agents/:id/export', (req, res) => {
|
||||||
|
try {
|
||||||
|
const exported = manager.exportAgent(req.params.id);
|
||||||
|
if (!exported) return res.status(404).json({ error: 'Agente não encontrado' });
|
||||||
|
res.json(exported);
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/tasks', (req, res) => {
|
||||||
|
try {
|
||||||
|
res.json(tasksStore.getAll());
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/tasks', (req, res) => {
|
||||||
|
try {
|
||||||
|
if (!req.body.name) return res.status(400).json({ error: 'name é obrigatório' });
|
||||||
|
const task = tasksStore.create(req.body);
|
||||||
|
res.status(201).json(task);
|
||||||
|
} catch (err) {
|
||||||
|
res.status(400).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.put('/tasks/:id', (req, res) => {
|
||||||
|
try {
|
||||||
|
const task = tasksStore.update(req.params.id, req.body);
|
||||||
|
if (!task) return res.status(404).json({ error: 'Tarefa não encontrada' });
|
||||||
|
res.json(task);
|
||||||
|
} catch (err) {
|
||||||
|
res.status(400).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.delete('/tasks/:id', (req, res) => {
|
||||||
|
try {
|
||||||
|
const deleted = tasksStore.delete(req.params.id);
|
||||||
|
if (!deleted) return res.status(404).json({ error: 'Tarefa não encontrada' });
|
||||||
|
res.status(204).send();
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/schedules', (req, res) => {
|
||||||
|
try {
|
||||||
|
const { agentId, taskDescription, cronExpression } = req.body;
|
||||||
|
if (!agentId || !taskDescription || !cronExpression) {
|
||||||
|
return res.status(400).json({ error: 'agentId, taskDescription e cronExpression são obrigatórios' });
|
||||||
|
}
|
||||||
|
const result = manager.scheduleTask(agentId, taskDescription, cronExpression, wsCallback);
|
||||||
|
res.status(201).json(result);
|
||||||
|
} catch (err) {
|
||||||
|
const status = err.message.includes('não encontrado') ? 404 : 400;
|
||||||
|
res.status(status).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/schedules', (req, res) => {
|
||||||
|
try {
|
||||||
|
res.json(scheduler.getSchedules());
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.delete('/schedules/:taskId', (req, res) => {
|
||||||
|
try {
|
||||||
|
const removed = scheduler.unschedule(req.params.taskId);
|
||||||
|
if (!removed) return res.status(404).json({ error: 'Agendamento não encontrado' });
|
||||||
|
res.status(204).send();
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/pipelines', (req, res) => {
|
||||||
|
try {
|
||||||
|
res.json(pipeline.getAllPipelines());
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/pipelines/:id', (req, res) => {
|
||||||
|
try {
|
||||||
|
const found = pipeline.getPipeline(req.params.id);
|
||||||
|
if (!found) return res.status(404).json({ error: 'Pipeline não encontrado' });
|
||||||
|
res.json(found);
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/pipelines', (req, res) => {
|
||||||
|
try {
|
||||||
|
const created = pipeline.createPipeline(req.body);
|
||||||
|
res.status(201).json(created);
|
||||||
|
} catch (err) {
|
||||||
|
res.status(400).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.put('/pipelines/:id', (req, res) => {
|
||||||
|
try {
|
||||||
|
const updated = pipeline.updatePipeline(req.params.id, req.body);
|
||||||
|
if (!updated) return res.status(404).json({ error: 'Pipeline não encontrado' });
|
||||||
|
res.json(updated);
|
||||||
|
} catch (err) {
|
||||||
|
res.status(400).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.delete('/pipelines/:id', (req, res) => {
|
||||||
|
try {
|
||||||
|
const deleted = pipeline.deletePipeline(req.params.id);
|
||||||
|
if (!deleted) return res.status(404).json({ error: 'Pipeline não encontrado' });
|
||||||
|
res.status(204).send();
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/pipelines/:id/execute', (req, res) => {
|
||||||
|
try {
|
||||||
|
const { input } = req.body;
|
||||||
|
if (!input) return res.status(400).json({ error: 'input é obrigatório' });
|
||||||
|
|
||||||
|
pipeline.executePipeline(req.params.id, input, wsCallback).catch(() => {});
|
||||||
|
res.status(202).json({ pipelineId: req.params.id, status: 'started' });
|
||||||
|
} catch (err) {
|
||||||
|
const status = err.message.includes('não encontrado') ? 404 : 400;
|
||||||
|
res.status(status).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/pipelines/:id/cancel', (req, res) => {
|
||||||
|
try {
|
||||||
|
const cancelled = pipeline.cancelPipeline(req.params.id);
|
||||||
|
if (!cancelled) return res.status(404).json({ error: 'Pipeline não está em execução' });
|
||||||
|
res.json({ cancelled: true });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/system/status', (req, res) => {
|
||||||
|
try {
|
||||||
|
const agents = manager.getAllAgents();
|
||||||
|
const activeExecutions = manager.getActiveExecutions();
|
||||||
|
const schedules = scheduler.getSchedules();
|
||||||
|
const pipelines = pipeline.getAllPipelines();
|
||||||
|
const activePipelines = pipeline.getActivePipelines();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
agents: {
|
||||||
|
total: agents.length,
|
||||||
|
active: agents.filter((a) => a.status === 'active').length,
|
||||||
|
inactive: agents.filter((a) => a.status === 'inactive').length,
|
||||||
|
},
|
||||||
|
executions: {
|
||||||
|
active: activeExecutions.length,
|
||||||
|
list: activeExecutions,
|
||||||
|
},
|
||||||
|
schedules: {
|
||||||
|
total: schedules.length,
|
||||||
|
active: schedules.filter((s) => s.active).length,
|
||||||
|
},
|
||||||
|
pipelines: {
|
||||||
|
total: pipelines.length,
|
||||||
|
active: pipelines.filter((p) => p.status === 'active').length,
|
||||||
|
running: activePipelines.length,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
router.get('/executions/active', (req, res) => {
|
||||||
|
try {
|
||||||
|
res.json(manager.getActiveExecutions());
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
89
src/store/db.js
Normal file
89
src/store/db.js
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
|
||||||
|
import { dirname } from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const DATA_DIR = `${__dirname}/../../data`;
|
||||||
|
const AGENTS_FILE = `${DATA_DIR}/agents.json`;
|
||||||
|
const TASKS_FILE = `${DATA_DIR}/tasks.json`;
|
||||||
|
const PIPELINES_FILE = `${DATA_DIR}/pipelines.json`;
|
||||||
|
|
||||||
|
function ensureDataDir() {
|
||||||
|
if (!existsSync(DATA_DIR)) {
|
||||||
|
mkdirSync(DATA_DIR, { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadFile(filePath) {
|
||||||
|
ensureDataDir();
|
||||||
|
if (!existsSync(filePath)) {
|
||||||
|
writeFileSync(filePath, JSON.stringify([]), 'utf8');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.parse(readFileSync(filePath, 'utf8'));
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveFile(filePath, data) {
|
||||||
|
ensureDataDir();
|
||||||
|
writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
function createStore(filePath) {
|
||||||
|
return {
|
||||||
|
load: () => loadFile(filePath),
|
||||||
|
|
||||||
|
save: (data) => saveFile(filePath, data),
|
||||||
|
|
||||||
|
getAll: () => loadFile(filePath),
|
||||||
|
|
||||||
|
getById: (id) => {
|
||||||
|
const items = loadFile(filePath);
|
||||||
|
return items.find((item) => item.id === id) || null;
|
||||||
|
},
|
||||||
|
|
||||||
|
create: (data) => {
|
||||||
|
const items = loadFile(filePath);
|
||||||
|
const newItem = {
|
||||||
|
id: uuidv4(),
|
||||||
|
...data,
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
updated_at: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
items.push(newItem);
|
||||||
|
saveFile(filePath, items);
|
||||||
|
return newItem;
|
||||||
|
},
|
||||||
|
|
||||||
|
update: (id, data) => {
|
||||||
|
const items = loadFile(filePath);
|
||||||
|
const index = items.findIndex((item) => item.id === id);
|
||||||
|
if (index === -1) return null;
|
||||||
|
items[index] = {
|
||||||
|
...items[index],
|
||||||
|
...data,
|
||||||
|
id,
|
||||||
|
updated_at: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
saveFile(filePath, items);
|
||||||
|
return items[index];
|
||||||
|
},
|
||||||
|
|
||||||
|
delete: (id) => {
|
||||||
|
const items = loadFile(filePath);
|
||||||
|
const index = items.findIndex((item) => item.id === id);
|
||||||
|
if (index === -1) return false;
|
||||||
|
items.splice(index, 1);
|
||||||
|
saveFile(filePath, items);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const agentsStore = createStore(AGENTS_FILE);
|
||||||
|
export const tasksStore = createStore(TASKS_FILE);
|
||||||
|
export const pipelinesStore = createStore(PIPELINES_FILE);
|
||||||
Reference in New Issue
Block a user