feat: pulse-dev — dev environment full-stack com hot reload + agentes + taskboard + vault Obsidian
- Stack Swarm 'dev': redis + taskboard + frontend HMR (Vite) + backend HMR (tsx) + 5 agents - TaskBoard standalone React com Redis pub/sub e BLPOP queue - Backend API Express: /tasks /agents /health com hot reload - Agentes: 2 FE (frontend), 2 BE (backend), 1 DevOps (parallel workers) - Vault Obsidian: /root/Obsidian-Pulse/ com estrutura Inbox/Projetos/Docker/Dev/Codex/Logs/Memorias/Templates - Skill obsidian-vault-linker instalada e documentada - Caddy labels: board.octal.tec.br + api.octal.tec.br + frontend.octal.tec.br - Protocolo task queue Redis documentado em MEMORY.md
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt-BR">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Pulse Dev — Frontend</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: 'Segoe UI', system-ui, sans-serif; background: #0f1117; color: #e4e4e7; }
|
||||
nav { background: #1e293b; padding: 12px 24px; display: flex; gap: 20px; border-bottom: 1px solid #334155; }
|
||||
nav a { color: #94a3b8; text-decoration: none; font-size: 14px; }
|
||||
nav a.active { color: #60a5fa; font-weight: 600; }
|
||||
section { padding: 32px 24px; max-width: 960px; margin: 0 auto; }
|
||||
h1 { font-size: 28px; margin-bottom: 8px; }
|
||||
h2 { font-size: 18px; margin: 24px 0 12px; color: #60a5fa; }
|
||||
p { color: #94a3b8; line-height: 1.7; font-size: 14px; }
|
||||
.status-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 12px; margin-top: 16px; }
|
||||
.card { background: #1e293b; border: 1px solid #334155; border-radius: 8px; padding: 16px; }
|
||||
.card b { color: #60a5fa; font-size: 20px; }
|
||||
.card small { color: #64748b; font-size: 12px; }
|
||||
.badge { display: inline-block; padding: 2px 8px; border-radius: 99px; font-size: 11px; margin: 2px; }
|
||||
.ok { background: #14532d; color: #86efac; }
|
||||
.warn{ background: #78350f; color: #fde68a; }
|
||||
.err { background: #7f1d1d; color: #fca5a5; }
|
||||
pre { background: #0f1117; border: 1px solid #334155; border-radius: 6px; padding: 16px; font-size: 13px; overflow-x: auto; color: #a5f3fc; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
<a href="#overview">Visão Geral</a>
|
||||
<a href="#agents">Agentes</a>
|
||||
<a href="#stacks">Stacks</a>
|
||||
<a href="#flows">Fluxos</a>
|
||||
</nav>
|
||||
<section id="overview">
|
||||
<h1>⚡ Pulse Frontend</h1>
|
||||
<p>Ambiente de desenvolvimento full-stack com hot reload, agents paralelos e taskboard em tempo real.</p>
|
||||
<div class="status-grid">
|
||||
<div class="card"><small>Redis</small><br><b id="redis-badge"><span class="warn badge">conectando…</span></b></div>
|
||||
<div class="card"><small>TaskBoard</small><br><b>board.octal.tec.br</b></div>
|
||||
<div class="card"><small>Backend API</small><br><b id="api-status">verificando…</b></div>
|
||||
<div class="card"><small>Hot Reload</small><br><b id="hmr-status">aguardando</b></div>
|
||||
<div class="card"><small>Backend Agent</small><br><b id="be-status">—</b></div>
|
||||
<div class="card"><small>Frontend Agent</small><br><b id="fe-status">—</b></div>
|
||||
<div class="card"><small>DevOps Agent</small><br><b id="ops-status">—</b></div>
|
||||
<div class="card"><small>Tasks na Queue</small><br><b id="queue-count">—</b></div>
|
||||
</div>
|
||||
<h2>Stack Swarm / Clusters</h2>
|
||||
<pre id="stack-info">Carregando…</pre>
|
||||
<h2>Últimos Logs</h2>
|
||||
<pre id="logs">Aguardando eventos…</pre>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
const API = 'http://redis:6379'
|
||||
const BACKEND_URL = 'http://localhost:3001'
|
||||
|
||||
async function update() {
|
||||
try {
|
||||
const [agentsRes, stacksRes, logsRes] = await Promise.allSettled([
|
||||
fetch(`${BACKEND_URL}/agents`),
|
||||
fetch(`${BACKEND_URL}/tasks?redis=1`),
|
||||
fetch(`${BACKEND_URL}/health`),
|
||||
])
|
||||
if (agentsRes.status === 'fulfilled') {
|
||||
const agents = await agentsRes.value.json()
|
||||
const map = {};
|
||||
agents.forEach(a => { map[a.id] = a; })
|
||||
const setStatus = (id, badgeId, labelId) => {
|
||||
const el = document.getElementById(badgeId)
|
||||
if (!el) return
|
||||
const a = map[id]
|
||||
if (!a) return
|
||||
const s = a.status
|
||||
document.getElementById(labelId).textContent = a.role || s
|
||||
el.innerHTML = `<span class="${s==='online'?'ok':s==='busy'?'warn':'err'} badge">${s}</span>`
|
||||
}
|
||||
setStatus('dev-backend', 'be-status', 'api-status')
|
||||
setStatus('agent-frontend','fe-status','hmr-status')
|
||||
setStatus('agent-devops', 'ops-status','ops-status')
|
||||
}
|
||||
if (stacksRes.status === 'fulfilled') {
|
||||
const tasks = await stacksRes.value.json()
|
||||
const counts = { todo: 0, doing: 0, done: 0 }
|
||||
tasks.forEach(t => { if (counts[t.status] !== undefined) counts[t.status]++ })
|
||||
document.getElementById('queue-count').textContent = counts.todo + counts.doing
|
||||
}
|
||||
} catch(e) {}
|
||||
}
|
||||
|
||||
async function getStackInfo() {
|
||||
const r = await fetch(`${BACKEND_URL}/health`)
|
||||
if (!r.ok) return 'Backend: timeout — verificar stack dev no Swarm'
|
||||
return`Stack Swarm — Ambiente Dev Full-Stack
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ ORQUESTRADOR │ TaskBoard │ Redis │ Pipeline │
|
||||
│ dock.pulse-ops│ board.oc. │ 6379 │ CI/CD → Gitea │
|
||||
│ (auto) │ (nginx) │ (falback)│ GitHub Actions │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ FRONTEND │ BACKEND │ WORKERS │ Deploy │
|
||||
│ Vite 5173 │ tsx:3001 │ 2 FE/2 BE│ docker stack deploy -c │
|
||||
│ (hot reload) │ (hot reload│ 1 DevOps │ runbooks/dev-stack.yml │
|
||||
│ │ + HMR) │ │ dev │
|
||||
└─────────────────────────────────────────────────────────────────┘`
|
||||
}
|
||||
|
||||
async function getLogs() {
|
||||
try {
|
||||
const r = await fetch(`${BACKEND_URL}/logs?limit=20`)
|
||||
const lines = await r.json()
|
||||
return lines.map(l => {
|
||||
const ts = ts(l.ts || Date.now())
|
||||
const col = { INFO:'#60a5fa', AGENT:'#22c55e', TASK:'#a78bfa', WARN:'#f0b429', ERROR:'#ef4444' }[l.level] || '#94a3b8'
|
||||
return `\x1b[${col}m[${ts}]\x1b[0m [\x1b[${col}m${l.level}\x1b[0m] ${l.msg || JSON.stringify(l)}`
|
||||
}).join('\n') || 'Sem logs ainda...'
|
||||
} catch(e) { return 'Logs indisponíveis' }
|
||||
}
|
||||
|
||||
function ts(ms) {
|
||||
const d = new Date(ms)
|
||||
return `${d.getHours().toString().padStart(2,'0')}:${d.getMinutes().toString().padStart(2,'0')}:${d.getSeconds().toString().padStart(2,'0')}`
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
document.getElementById('stack-info').textContent = await getStackInfo()
|
||||
await update()
|
||||
setInterval(update, 3000)
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user