Compare commits

..

3 Commits

Author SHA1 Message Date
Frederico Castro
46f999c676 Adicionar landing page e redirecionar rota raiz
- Landing page profissional com hero, features, pricing (Starter/Pro/Enterprise), FAQ
- Animações no scroll, parallax, contadores animados, glassmorphism
- Dashboard movido para /app.html, landing page agora é a página inicial
- CTAs direcionam para /app.html
2026-02-28 00:32:14 -03:00
Frederico Castro
39f0902a0f Rodar container como usuário node e corrigir resume do executor
- Dockerfile: usar USER node (UID 1000) para bypassPermissions funcionar
- Volumes mapeados para /home/node/ em vez de /root/
- Corrigir resume: voltar a usar -p para mensagens curtas de chat
- Manter stdin piping apenas em execute e summarize (prompts grandes)
2026-02-28 00:32:14 -03:00
Frederico Castro
fd3c2dc69a Adicionar botão Interromper no terminal e corrigir botão Retomar
- Botão Interromper na toolbar do terminal para matar execuções ativas
- Endpoint POST /executions/cancel-all para cancelar agentes e pipelines
- Botão aparece/esconde automaticamente conforme execuções ativas
- Corrigir condição do botão Retomar para pipelines antigas sem failedAtStep
2026-02-28 00:32:14 -03:00
8 changed files with 1927 additions and 1346 deletions

View File

@@ -4,7 +4,8 @@ COPY package*.json ./
RUN npm ci --omit=dev
RUN npm install -g @anthropic-ai/claude-code
COPY . .
RUN mkdir -p data
RUN mkdir -p data && chown -R node:node /app
USER node
ENV HOST=0.0.0.0
ENV PORT=3000
EXPOSE 3000

1386
public/app.html Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -118,6 +118,7 @@ const API = {
status() { return API.request('GET', '/system/status'); },
info() { return API.request('GET', '/system/info'); },
activeExecutions() { return API.request('GET', '/executions/active'); },
cancelAll() { return API.request('POST', '/executions/cancel-all'); },
},
settings: {

View File

@@ -555,6 +555,18 @@ const App = {
on('pipeline-execute-submit', 'click', () => PipelinesUI._executeFromModal());
on('terminal-stop-btn', 'click', async () => {
try {
await API.system.cancelAll();
Terminal.stopProcessing();
Terminal.addLine('Todas as execuções foram interrompidas.', 'error');
Toast.warning('Execuções interrompidas');
App._updateActiveBadge();
} catch (err) {
Toast.error(`Erro ao interromper: ${err.message}`);
}
});
on('terminal-clear-btn', 'click', () => {
Terminal.clear();
Terminal.disableChat();
@@ -960,6 +972,9 @@ const App = {
if (countEl) countEl.textContent = count;
if (badge) badge.style.display = count > 0 ? 'flex' : 'none';
const stopBtn = document.getElementById('terminal-stop-btn');
if (stopBtn) stopBtn.hidden = count === 0;
const terminalSelect = document.getElementById('terminal-execution-select');
if (terminalSelect && Array.isArray(active)) {
const existing = new Set(

View File

@@ -102,7 +102,7 @@ const HistoryUI = {
<i data-lucide="eye"></i>
Ver detalhes
</button>
${(exec.status === 'error' && exec.type === 'pipeline' && exec.failedAtStep !== undefined) ? `
${(exec.status === 'error' && exec.type === 'pipeline') ? `
<button class="btn btn-ghost btn-sm" data-action="resume-pipeline" data-id="${exec.id}" type="button" title="Retomar do passo ${(exec.failedAtStep || 0) + 1}">
<i data-lucide="play"></i>
Retomar

View File

@@ -362,6 +362,7 @@ export function resume(agentConfig, sessionId, message, callbacks = {}) {
const model = agentConfig.model || 'claude-sonnet-4-6';
const args = [
'--resume', sessionId,
'-p', sanitizeText(message),
'--output-format', 'stream-json',
'--verbose',
'--model', model,
@@ -378,7 +379,7 @@ export function resume(agentConfig, sessionId, message, callbacks = {}) {
const spawnOptions = {
env: cleanEnv(),
stdio: ['pipe', 'pipe', 'pipe'],
stdio: ['ignore', 'pipe', 'pipe'],
};
if (agentConfig.workingDirectory && agentConfig.workingDirectory.trim()) {
@@ -388,8 +389,6 @@ export function resume(agentConfig, sessionId, message, callbacks = {}) {
console.log(`[executor] Resumindo sessão: ${sessionId} | Execução: ${executionId}`);
const child = spawn(CLAUDE_BIN, args, spawnOptions);
child.stdin.write(sanitizeText(message));
child.stdin.end();
activeExecutions.set(executionId, {
process: child,

View File

@@ -8,7 +8,7 @@ import * as manager from '../agents/manager.js';
import { tasksStore, settingsStore, executionsStore, webhooksStore, notificationsStore, secretsStore, agentVersionsStore } from '../store/db.js';
import * as scheduler from '../agents/scheduler.js';
import * as pipeline from '../agents/pipeline.js';
import { getBinPath, updateMaxConcurrent } from '../agents/executor.js';
import { getBinPath, updateMaxConcurrent, cancelAllExecutions, getActiveExecutions } from '../agents/executor.js';
import { invalidateAgentMapCache } from '../agents/pipeline.js';
import { cached } from '../cache/index.js';
import { readdirSync, readFileSync, unlinkSync, existsSync, mkdirSync } from 'fs';
@@ -838,6 +838,23 @@ router.get('/executions/active', (req, res) => {
}
});
router.post('/executions/cancel-all', (req, res) => {
try {
const activePipelines = pipeline.getActivePipelines();
for (const p of activePipelines) {
pipeline.cancelPipeline(p.pipelineId);
}
cancelAllExecutions();
const running = executionsStore.getAll().filter(e => e.status === 'running' || e.status === 'awaiting_approval');
for (const e of running) {
executionsStore.update(e.id, { status: 'canceled', endedAt: new Date().toISOString() });
}
res.json({ cancelled: true });
} catch (err) {
res.status(500).json({ error: err.message });
}
});
router.get('/executions/recent', (req, res) => {
try {
const limit = parseInt(req.query.limit) || 20;