diff --git a/pulse-dev/agents/backend/agent.js b/pulse-dev/agents/backend/agent.js new file mode 100644 index 0000000..012f933 --- /dev/null +++ b/pulse-dev/agents/backend/agent.js @@ -0,0 +1,56 @@ +import Redis from 'ioredis' +const redis = new Redis({ host: 'redis', port: 6379, lazyConnect: true }) +const QUEUE = 'dev-tasks' +const LOG = 'dev-logs' + +function log(level, msg) { + const line = JSON.stringify({ ts: new Date().toISOString(), level, msg }) + redis.publish(LOG, line).catch(() => {}) + console.log(`[${level}] ${msg}`) +} + +async function claimTask(task) { + await redis.hset(`agent:agent-backend:task`, task.id, JSON.stringify(task)) + await redis.hset(`agent:agent-backend`, 'status', 'busy') + await redis.hset(`agent:agent-backend`, 'current_task', task.title) + await redis.publish(LOG, JSON.stringify({ ts: new Date().toISOString(), level: 'AGENT', msg: `Backend pegou: "${task.title}"` })) + task.status = 'in_progress' + task.assignee = 'agent-backend' + await redis.set(`task:${task.id}`, JSON.stringify(task)) + log('AGENT', `▶️ Processando: ${task.title}`) + await new Promise(r => setTimeout(r, Math.random() * 6000 + 3000)) + task.status = 'done' + task.done_at = new Date().toISOString() + await redis.set(`task:${task.id}`, JSON.stringify(task)) + await redis.hset(`agent:agent-backend`, 'status', 'idle') + await redis.hdel(`agent:agent-backend:task`, task.id) + await redis.hincrby('agent:agent-backend', 'tasks_done', 1) + await awaitingTask(task) +} + +async function awaitingTask(task) { + await redis.hset(`agent:agent-backend`, 'status', 'idle') + await redis.hdel(`agent:agent-backend:task`, task.id) + log('AGENT', `✔️ Concluído: ${task.title}`) +} + +async function run() { + await redis.connect() + await redis.set('agent:agent-backend', JSON.stringify({ + id: 'agent-backend', role: 'backend', status: 'online', + started_at: new Date().toISOString(), tasks_done: 0, + })) + log('AGENT', 'Backend Agent online — aguardando tarefas') + while (true) { + const [_, taskId] = await redis.blpop(QUEUE, 60) + if (!taskId) { await new Promise(r => setTimeout(r, 1000)); continue } + const raw = await redis.get(`task:${taskId}`) + if (!raw) continue + const task = JSON.parse(raw) + if (task.domain !== 'backend' && task.domain !== 'fullstack') { + await redis.rpush(QUEUE, taskId); continue + } + await claimTask(task) + } +} +run().catch(e => { console.error(e); process.exit(1) }) diff --git a/pulse-dev/agents/devops/agent.js b/pulse-dev/agents/devops/agent.js new file mode 100644 index 0000000..5461554 --- /dev/null +++ b/pulse-dev/agents/devops/agent.js @@ -0,0 +1,52 @@ +import Redis from 'ioredis' +import { execSync } from 'child_process' +const redis = new Redis({ host: 'redis', port: 6379, lazyConnect: true }) +const QUEUE = 'dev-tasks' +const LOG = 'dev-logs' + +function log(level, msg) { + const line = JSON.stringify({ ts: new Date().toISOString(), level, msg }) + redis.publish(LOG, line).catch(() => {}) + console.log(`[${level}] ${msg}`) +} + +function exec(cmd) { + try { return execSync(cmd, { encoding: 'utf8', timeout: 30000 }).trim() } + catch(e) { return `ERROR: ${e.message}` } +} + +async function processTask(task) { + task.status = 'in_progress' + task.assignee = 'agent-devops' + await redis.set(`task:${task.id}`, JSON.stringify(task)) + log('AGENT', `▶️ Processando: ${task.title}`) + const result = exec(task.command || `echo "${task.title}"`) + task.result = result + task.status = 'done' + task.done_at = new Date().toISOString() + await redis.set(`task:${task.id}`, JSON.stringify(task)) + log('AGENT', `✔️ Concluído: ${task.title}`) + return result +} + +async function run() { + await redis.connect() + await redis.set('agent:agent-devops', JSON.stringify({ + id: 'agent-devops', role: 'devops', status: 'online', + started_at: new Date().toISOString(), tasks_done: 0, + })) + log('AGENT', 'DevOps Agent online — aguardando tarefas') + while (true) { + const [_, tid] = await redis.blpop(QUEUE, 60) + if (!tid) { await new Promise(r => setTimeout(r, 1000)); continue } + const raw = await redis.get(`task:${tid}`) + if (!raw) continue + const task = JSON.parse(raw) + if (task.domain !== 'devops') { + await redis.rpush(QUEUE, tid); continue + } + await processTask(task) + await redis.hincrby('agent:agent-devops', 'tasks_done', 1) + } +} +run().catch(e => { console.error(e); process.exit(1) }) diff --git a/pulse-dev/agents/frontend/agent.js b/pulse-dev/agents/frontend/agent.js new file mode 100644 index 0000000..8d3ba43 --- /dev/null +++ b/pulse-dev/agents/frontend/agent.js @@ -0,0 +1,55 @@ +import Redis from 'ioredis' +const redis = new Redis({ host: 'redis', port: 6379, lazyConnect: true }) +const QUEUE = 'dev-tasks' +const LOG = 'dev-logs' + +function log(level, msg) { + const line = JSON.stringify({ ts: new Date().toISOString(), level, msg }) + redis.publish(LOG, line).catch(() => {}) + console.log(`[${level}] ${msg}`) +} + +async function claimTask(task) { + await redis.hset(`agent:agent-frontend:task`, task.id, JSON.stringify(task)) + await redis.hset(`agent:agent-frontend`, 'status', 'busy') + await redis.hset(`agent:agent-frontend`, 'current_task', task.title) + await redis.publish(LOG, JSON.stringify({ ts: new Date().toISOString(), level: 'AGENT', msg: `Frontend pegou: "${task.title}"` })) + task.status = 'in_progress' + task.assignee = 'agent-frontend' + await redis.set(`task:${task.id}`, JSON.stringify(task)) + log('AGENT', `▶️ Processando: ${task.title}`) + await new Promise(r => setTimeout(r, Math.random() * 5000 + 2000)) + task.status = 'done' + task.done_at = new Date().toISOString() + await redis.set(`task:${task.id}`, JSON.stringify(task)) + await redis.hset(`agent:agent-frontend`, 'status', 'idle') + await redis.hdel(`agent:agent-frontend:task`, task.id) + awaitingTask(task) +} + +async function awaitingTask(task) { + await redis.hset(`agent:agent-frontend`, 'status', 'idle') + await redis.hdel(`agent:agent-frontend:task`, task.id) + log('AGENT', `✔️ Concluído: ${task.title}`) +} + +async function run() { + await redis.connect() + await redis.set('agent:agent-frontend', JSON.stringify({ + id: 'agent-frontend', role: 'frontend', status: 'online', + started_at: new Date().toISOString(), tasks_done: 0, + })) + log('AGENT', 'Frontend Agent online — aguardando tarefas') + while (true) { + const [_, taskId] = await redis.blpop(QUEUE, 60) + if (!taskId) { await new Promise(r => setTimeout(r, 1000)); continue } + const raw = await redis.get(`task:${taskId}`) + if (!raw) continue + const task = JSON.parse(raw) + if (task.domain !== 'frontend' && task.domain !== 'fullstack') { + await redis.rpush(QUEUE, taskId); continue + } + await claimTask(task) + } +} +run().catch(e => { console.error(e); process.exit(1) }) diff --git a/pulse-dev/agents/package.json b/pulse-dev/agents/package.json new file mode 100644 index 0000000..b74bc23 --- /dev/null +++ b/pulse-dev/agents/package.json @@ -0,0 +1,15 @@ +{ + "name": "pulse-agents", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "agent-frontend": "node /app/agents/frontend/agent.js", + "agent-backend": "node /app/agents/backend/agent.js", + "agent-devops": "node /app/agents/devops/agent.js" + }, + "dependencies": { + "ioredis": "^5.4.1", + "uuid": "^10.0.0" + } +} diff --git a/pulse-dev/backend/package.json b/pulse-dev/backend/package.json new file mode 100644 index 0000000..b5ab02a --- /dev/null +++ b/pulse-dev/backend/package.json @@ -0,0 +1,22 @@ +{ + "name": "pulse-dev-backend", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "tsx watch --dir ./src server.ts" + }, + "dependencies": { + "express": "^5.1.0", + "ioredis": "^5.4.1", + "cors": "^2.8.5", + "helmet": "^8.0.0", + "uuid": "^10.0.0" + }, + "devDependencies": { + "tsx": "^4.19.2", + "@types/express": "^5.0.0", + "@types/cors": "^2.8.17", + "@types/uuid": "^10.0.0" + } +} diff --git a/pulse-dev/backend/src/server.ts b/pulse-dev/backend/src/server.ts new file mode 100644 index 0000000..54fad1e --- /dev/null +++ b/pulse-dev/backend/src/server.ts @@ -0,0 +1,98 @@ +import express from 'express' +import cors from 'cors' +import helmet from 'helmet' +import Redis from 'ioredis' +import { v4 as uuidv4 } from 'uuid' + +const app = express() +app.use(cors()) +app.use(helmet()) +app.use(express.json()) + +// ─── Redis ────────────────────────────────────────────────────────── +const redis = new Redis({ host: 'redis', port: 6379, lazyConnect: true }) +redis.on('error', (err) => console.error('[Redis] error:', err.message)) +redis.on('ready', () => console.log('[Redis] conectado')) + +// ─── Task Queue ───────────────────────────────────────────────────── +const TASK_QUEUE = 'dev-tasks' +const LOG_CHANNEL = 'dev-logs' + +function log(level, msg) { + const line = JSON.stringify({ ts: new Date().toISOString(), level, msg }) + redis.publish(LOG_CHANNEL, line).catch(() => {}) + console.log(`[${level}] ${msg}`) +} + +// GET /tasks — lista todas +// POST /tasks — cria tarefa +// PUT /tasks/:id — atualiza status +// GET /agents — lista agentes conectados +// GET /logs/:limit — logs recentes + +app.get('/tasks', async (_req, res) => { + const keys = await redis.keys('task:*') + const tasks = [] + for (const k of keys) { + const t = await redis.get(k) + if (t) tasks.push(JSON.parse(t)) + } + tasks.sort((a, b) => b.updated_at?.localeCompare(a.updated_at) || 0) + res.json(tasks) +}) + +app.post('/tasks', async (req, res) => { + const id = uuidv4() + const task = { + id, title: req.body.title, type: req.body.type || 'feature', + priority: req.body.priority || 'medium', status: 'todo', + domain: req.body.domain || 'fullstack', assignee: null, + created_at: new Date().toISOString(), updated_at: new Date().toISOString(), + } + await redis.set(`task:${id}`, JSON.stringify(task)) + await redis.rpush('task-queue', id) + log('TASK', `Nova tarefa: "${task.title}" [${task.type}]`) + res.status(201).json(task) +}) + +app.put('/tasks/:id', async (req, res) => { + const key = `task:${req.params.id}` + const raw = await redis.get(key) + if (!raw) return res.status(404).json({ error: 'Não encontrada' }) + const task = JSON.parse(raw) + Object.assign(task, req.body, { updated_at: new Date().toISOString() }) + await redis.set(key, JSON.stringify(task)) + log('INFO', `Tarefa atualizada: "${task.title}" status=${task.status}`) + res.json(task) +}) + +app.get('/agents', async (_req, res) => { + const keys = await redis.keys('agent:*') + const agents = [] + for (const k of keys) { + const a = await redis.get(k) + if (a) agents.push(JSON.parse(a)) + } + res.json(agents) +}) + +app.get('/health', (_req, res) => { + res.json({ status: 'ok', ts: new Date().toISOString() }) +}) + +// Promover tarefa automaticamente ao criar/modificar +app.use('*', (req, res) => { res.status(404).json({ error: 'Not found' }) }) + +const PORT = 3001 +app.listen(PORT, '0.0.0.0', () => { + // Registrar agente backend + ;(async () => { + try { await redis.connect() } catch (e) { console.error('Redis connect:', e.message) } + await redis.set('agent:dev-backend', JSON.stringify({ + id: 'dev-backend', role: 'backend', status: 'online', + started_at: new Date().toISOString(), + })) + log('AGENT', `Backend API online :${PORT}`) + log('AGENT', `Redis conectado — queue: ${TASK_QUEUE}`) + })() +}) diff --git a/pulse-dev/frontend/index.html b/pulse-dev/frontend/index.html new file mode 100644 index 0000000..34d2c7a --- /dev/null +++ b/pulse-dev/frontend/index.html @@ -0,0 +1,129 @@ + + + + + +Pulse Dev — Frontend + + + + +
+

⚡ Pulse Frontend

+

Ambiente de desenvolvimento full-stack com hot reload, agents paralelos e taskboard em tempo real.

+
+
Redis
conectando…
+
TaskBoard
board.octal.tec.br
+
Backend API
verificando…
+
Hot Reload
aguardando
+
Backend Agent
+
Frontend Agent
+
DevOps Agent
+
Tasks na Queue
+
+

Stack Swarm / Clusters

+
Carregando…
+

Últimos Logs

+
Aguardando eventos…
+
+ + diff --git a/pulse-dev/taskboard/package.json b/pulse-dev/taskboard/package.json new file mode 100644 index 0000000..860d15e --- /dev/null +++ b/pulse-dev/taskboard/package.json @@ -0,0 +1,25 @@ +{ + "name": "pulse-taskboard", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite --host 0.0.0.0 --port 5174", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1", + "ioredis": "^5.4.1", + "uuid": "^10.0.0" + }, + "devDependencies": { + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@types/uuid": "^10.0.0", + "@vitejs/plugin-react": "^4.3.4", + "typescript": "~5.6.2", + "vite": "^6.0.3" + } +} diff --git a/pulse-dev/taskboard/public/index.html b/pulse-dev/taskboard/public/index.html new file mode 100644 index 0000000..226ca48 --- /dev/null +++ b/pulse-dev/taskboard/public/index.html @@ -0,0 +1,239 @@ + + + + + + Pulse TaskBoard — Dev Orchestrator + + + + +
+

⚡ Pulse TaskBoard dev-orchestrator

+
+
Tasks: 0
+
Ativas: 0
+
Agentes: 3
+
Redis: offline
+
+
+ +
+
+
📋 A Fazer 0
+
+
+ + + + +
+
+ +
+
🔨 Em Progresso 0
+
+
+ +
+
✅ Concluídas 0
+
+
+ +
+
📡 Logs em Tempo Real 0
+
+
+
+ + + + diff --git a/pulse-dev/taskboard/public/nginx.conf b/pulse-dev/taskboard/public/nginx.conf new file mode 100644 index 0000000..86c5ceb --- /dev/null +++ b/pulse-dev/taskboard/public/nginx.conf @@ -0,0 +1,8 @@ +server { + listen 80; + server_name board.octal.tec.br; + root /usr/share/nginx/html; + index index.html; + location / { try_files $uri $uri/ /index.html; } + add_header X-Content-Type-Options "nosniff"; +} diff --git a/pulse-dev/taskboard/tsconfig.json b/pulse-dev/taskboard/tsconfig.json new file mode 100644 index 0000000..71a2830 --- /dev/null +++ b/pulse-dev/taskboard/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/pulse-dev/taskboard/vite.config.ts b/pulse-dev/taskboard/vite.config.ts new file mode 100644 index 0000000..03500d3 --- /dev/null +++ b/pulse-dev/taskboard/vite.config.ts @@ -0,0 +1,11 @@ +// vite.config.ts +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [react()], + server: { + host: '0.0.0.0', + port: 5174, + }, +})