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:
Frederico Castro
2026-02-26 00:23:56 -03:00
commit 723a08d2e1
24 changed files with 8433 additions and 0 deletions

3279
public/css/styles.css Normal file

File diff suppressed because it is too large Load Diff

873
public/index.html Normal file
View 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
View 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
View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
},
};
window.Terminal = Terminal;

View 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;