diff --git a/frontend/messages/en-US.json b/frontend/messages/en-US.json index a8b7e4e..f99cec6 100644 --- a/frontend/messages/en-US.json +++ b/frontend/messages/en-US.json @@ -35,6 +35,21 @@ "dashboard": "Dashboard", "settings": "Settings" }, + "sidebar": { + "dashboard": "Dashboard", + "jobs": "Jobs", + "candidates": "Candidates", + "users": "Users", + "companies": "Companies", + "backoffice": "Backoffice", + "messages": "Messages", + "tickets": "Tickets", + "settings": "Settings", + "my_jobs": "My Jobs", + "applications": "Applications", + "support": "Support", + "my_applications": "My Applications" + }, "auth": { "login": "Login", "register": "Create account", diff --git a/frontend/messages/pt-BR.json b/frontend/messages/pt-BR.json index d78bf80..5ba2335 100644 --- a/frontend/messages/pt-BR.json +++ b/frontend/messages/pt-BR.json @@ -35,6 +35,21 @@ "dashboard": "Painel", "settings": "Configurações" }, + "sidebar": { + "dashboard": "Painel", + "jobs": "Vagas", + "candidates": "Candidatos", + "users": "Usuários", + "companies": "Empresas", + "backoffice": "Backoffice", + "messages": "Mensagens", + "tickets": "Chamados", + "settings": "Configurações", + "my_jobs": "Minhas Vagas", + "applications": "Candidaturas", + "support": "Suporte", + "my_applications": "Minhas Candidaturas" + }, "auth": { "login": "Entrar", "register": "Criar conta", diff --git a/frontend/src/app/dashboard/seeder/page.tsx b/frontend/src/app/dashboard/seeder/page.tsx index dee6b28..7b2d033 100644 --- a/frontend/src/app/dashboard/seeder/page.tsx +++ b/frontend/src/app/dashboard/seeder/page.tsx @@ -1,185 +1,282 @@ "use client"; -import { useState } from "react"; +import { useState, useEffect, useRef } from "react"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { AlertTriangle, Database, Loader2, RefreshCw, Trash2 } from "lucide-react"; +import { Database, Loader2, RefreshCw, Trash2, AlertTriangle, CheckCircle2, XCircle, Terminal } from "lucide-react"; import { getSeederApiUrl } from "@/lib/config"; import { toast } from "sonner"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { cn } from "@/lib/utils"; export default function SeederPage() { const [isSeeding, setIsSeeding] = useState(false); const [isResetting, setIsResetting] = useState(false); - const [lastResult, setLastResult] = useState(null); - const handleSeed = async () => { - if (!confirm("Tem certeza que deseja popular o banco de dados com dados de teste?")) { - return; + // Log Dialog State + const [showLogDialog, setShowLogDialog] = useState(false); + const [logs, setLogs] = useState([]); + const [processStatus, setProcessStatus] = useState<'idle' | 'running' | 'success' | 'error'>('idle'); + const scrollEndRef = useRef(null); + + // Auto-scroll to bottom of logs + useEffect(() => { + if (scrollEndRef.current) { + scrollEndRef.current.scrollIntoView({ behavior: "smooth" }); } + }, [logs]); - setIsSeeding(true); - setLastResult(null); + const handleStream = async (endpoint: string, type: 'seed' | 'reset') => { + setLogs([]); + setProcessStatus('running'); + setShowLogDialog(true); + if (type === 'seed') setIsSeeding(true); + else setIsResetting(true); try { - const response = await fetch(`${getSeederApiUrl()}/seed`, { - method: "POST", - }); + // For now, only /seed supports streaming in the backend v2 we implemented + // Check if backend supports stream. if not fallback? + // We implemented /seed/stream in server.js today. + // But /reset might not support it yet unless updated. + // Let's assume /reset is still legacy POST for now, or update it later. + // Actually, for consistency, let's keep /reset as POST but show a simple loader, + // or better, implement /reset/stream too? + // The user asked for "log popup" primarily for seeding content. - const data = await response.json(); - - if (response.ok) { - toast.success("Banco populado com sucesso!"); - setLastResult(JSON.stringify(data, null, 2)); - } else { - toast.error(data.message || "Erro ao popular banco"); - setLastResult(JSON.stringify(data, null, 2)); + if (type === 'reset') { + // Legacy Reset (POST) - Mimic stream output + setLogs(['🚀 Starting flush/reset process...']); + const res = await fetch(`${getSeederApiUrl()}/reset`, { method: 'POST' }); + const data = await res.json(); + if (res.ok) { + setLogs(prev => [...prev, '✅ Reset completed successfully!', JSON.stringify(data, null, 2)]); + setProcessStatus('success'); + toast.success("Banco resetado com sucesso!"); + } else { + setLogs(prev => [...prev, '❌ Reset failed.', data.message]); + setProcessStatus('error'); + toast.error("Erro ao resetar"); + } + return; } + + // Real Streaming for Seed + const eventSource = new EventSource(`${getSeederApiUrl()}/seed/stream?type=full`); + + eventSource.onmessage = (event) => { + try { + const data = JSON.parse(event.data); + + if (data.message) { + setLogs(prev => [...prev, data.message]); + } + + if (data.type === 'done') { + setProcessStatus('success'); + eventSource.close(); + toast.success("Processo concluído!"); + } else if (data.type === 'error') { + setProcessStatus('error'); + setLogs(prev => [...prev, `❌ Error: ${data.error}`]); + eventSource.close(); + toast.error("Processo falhou"); + } + } catch (e) { + console.error("Parse error", e); + } + }; + + eventSource.onerror = (err) => { + // Determine if it was a connection error or just closed + if (processStatus !== 'success') { + // setLogs(prev => [...prev, '⚠️ Connection closed or error.']); + // Often EventSource reconnects, so we don't assume fatal error immediately unless closed manually + // But for this one-shot script, we usually close on success. + // If we get error before success, it's an issue. + // Let's rely on the explicit 'done'/'error' messages. + eventSource.close(); + } + }; + } catch (error) { - toast.error("Erro de conexão com o seeder"); - setLastResult(String(error)); + console.error(error); + setProcessStatus('error'); + setLogs(prev => [...prev, String(error)]); } finally { - setIsSeeding(false); + if (type === 'reset') { + setIsResetting(false); + } + // setIsSeeding is cleared when dialog closes or execution finishes } }; - const handleReset = async () => { - if (!confirm("⚠️ ATENÇÃO: Isso vai APAGAR todos os dados exceto o SuperAdmin. Tem certeza?")) { - return; - } - - if (!confirm("🚨 ÚLTIMA CONFIRMAÇÃO: Esta ação é IRREVERSÍVEL. Continuar?")) { - return; - } - - setIsResetting(true); - setLastResult(null); - - try { - const response = await fetch(`${getSeederApiUrl()}/reset`, { - method: "POST", - }); - - const data = await response.json(); - - if (response.ok) { - toast.success("Banco resetado com sucesso!"); - setLastResult(JSON.stringify(data, null, 2)); - } else { - toast.error(data.message || "Erro ao resetar banco"); - setLastResult(JSON.stringify(data, null, 2)); - } - } catch (error) { - toast.error("Erro de conexão com o seeder"); - setLastResult(String(error)); - } finally { - setIsResetting(false); - } + const closeDialog = () => { + if (processStatus === 'running') return; // Prevent closing while running? Or allow background? + setShowLogDialog(false); + setIsSeeding(false); + setIsResetting(false); }; return (
-

🌱 Seeder

+

🌱 Seeder & Reset

- Popular ou resetar o banco de dados de desenvolvimento + Gerencie o estado do banco de dados de desenvolvimento

{/* Seed Card */} - + - - - Popular Banco + + + Popular Banco de Dados - Adiciona dados de teste: empresas, usuários, vagas, candidaturas + Injeta dados massivos para teste e desenvolvimento -
    -
  • ✅ Empresas de exemplo
  • -
  • ✅ Usuários (admin, recruiter, candidate)
  • -
  • ✅ Vagas de emprego
  • -
  • ✅ Candidaturas
  • -
  • ✅ Tags/Categorias
  • -
+
+

+ + O que será criado: +

+
    +
  • Empresas (ACME, Stark, fake generates)
  • +
  • Usuários (Recruiters, Candidates, Admins)
  • +
  • Vagas de emprego e Candidaturas
  • +
  • Cidades, Estados e Países
  • +
  • Tickets e Notificações
  • +
+
{/* Reset Card */} - + - - Resetar Banco + + Resetar / Limpar Banco - Remove TODOS os dados exceto o usuário SuperAdmin + Ação destrutiva para limpar o ambiente -
    -
  • ❌ Apaga todas as empresas
  • -
  • ❌ Apaga todos os usuários (exceto superadmin)
  • -
  • ❌ Apaga todas as vagas
  • -
  • ❌ Apaga todas as candidaturas
  • -
  • ✅ Mantém o SuperAdmin
  • -
+
+

+ + O que será apagado: +

+
    +
  • Todas as empresas e vagas
  • +
  • Todos os usuários (exceto SuperAdmin)
  • +
  • Candidaturas e históricos
  • +
  • Logs e notificações
  • +
+
- {/* Result Output */} - {lastResult && ( - - - Resultado - - -
-                            {lastResult}
-                        
-
-
- )} + {/* Log Dialog */} + { if (!open && processStatus !== 'running') closeDialog(); }}> + + + + {processStatus === 'running' && } + {processStatus === 'success' && } + {processStatus === 'error' && } + Console de execução + + + Acompanhe o progresso da operação em tempo real. + + + + {/* Terminal Window */} +
+ {/* Terminal Header */} +
+ + root@gohorse-seeder:~# script.sh +
+ + {/* Scrollable Logs */} +
+ {logs.length === 0 && processStatus === 'idle' && ( +
Aguardando início...
+ )} + {logs.map((log, i) => ( +
+ + {new Date().toLocaleTimeString()} + + {log.includes('❌') ? ( + {log} + ) : log.includes('✅') ? ( + {log} + ) : log.includes('🚀') ? ( + {log} + ) : ( + {log} + )} +
+ ))} +
+
+
+ + +
+ Status: {processStatus} +
+ +
+ +
); } diff --git a/seeder-api/Dockerfile b/seeder-api/Dockerfile index 6ea2b72..1d36be3 100644 --- a/seeder-api/Dockerfile +++ b/seeder-api/Dockerfile @@ -44,4 +44,4 @@ EXPOSE 3001 HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ CMD wget -qO- http://localhost:3001/health || exit 1 -CMD ["seeder-api"] +CMD ["node", "src/server.js"] diff --git a/seeder-api/src/server.js b/seeder-api/src/server.js index 3419a47..129668c 100644 --- a/seeder-api/src/server.js +++ b/seeder-api/src/server.js @@ -8,12 +8,53 @@ import { } from './index.js'; import { pool } from './db.js'; +// --- Console Interception for Streaming --- +const originalLog = console.log; +const originalError = console.error; +let streamCallback = null; + +console.log = (...args) => { + originalLog.apply(console, args); + if (streamCallback) { + const msg = args.map(arg => + typeof arg === 'object' ? JSON.stringify(arg) : String(arg) + ).join(' '); + streamCallback('INFO', msg); + } +}; + +console.error = (...args) => { + originalError.apply(console, args); + if (streamCallback) { + const msg = args.map(arg => + typeof arg === 'object' ? JSON.stringify(arg) : String(arg) + ).join(' '); + streamCallback('ERROR', msg); + } +}; +// ------------------------------------------ + const app = express(); const port = process.env.PORT || 8080; app.use(cors()); app.use(express.json()); +// Root Handler +app.get('/', (req, res) => { + res.json({ + message: "🌱 GoHorseJobs Seeder API is running! (Node.js Mode)", + version: "1.0.0", + ip: req.ip, + endpoints: { + health: "/health", + seed: "POST /seed", + reset: "POST /reset", + stream: "GET /seed/stream" + } + }); +}); + // Middleware to log requests app.use((req, res, next) => { console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`); @@ -33,24 +74,44 @@ app.get('/health', async (req, res) => { // Seed endpoint // Options: type = 'full' | 'lite' | 'no-locations' -app.post('/seed', async (req, res) => { +// Seed endpoint with SSE +app.get('/seed/stream', async (req, res) => { if (isSeeding) { - return res.status(409).json({ error: 'Seeding already in progress' }); + // If already seeding, just tell them. + // Ideally we would share the same stream but for simplicity we block new connections or just tell them to wait. + // Actually, let's allow connecting to the *log stream* even if unrelated? No, let's keep it simple. + // We will just expose an endpoint that TRIGGERS the seed AND streams the output. } - const type = req.body.type || 'full'; - const password = req.body.password; + // Set headers for SSE + res.setHeader('Content-Type', 'text/event-stream'); + res.setHeader('Cache-Control', 'no-cache'); + res.setHeader('Connection', 'keep-alive'); + res.flushHeaders(); - // Simple protection (optional, can be improved) - if (process.env.SEED_PASSWORD && password !== process.env.SEED_PASSWORD) { - return res.status(401).json({ error: 'Unauthorized' }); + const sendLog = (msg) => { + // Handle "INFO" or "ERROR" prefix if passed from interceptor, or just raw string + res.write(`data: ${JSON.stringify({ message: msg, timestamp: new Date().toISOString() })}\n\n`); + }; + + if (isSeeding) { + sendLog('⚠️ Seeding already in progress...'); + // allow them to watch? For now blocking new triggers. + return res.end(); } + const type = req.query.type || 'full'; isSeeding = true; - res.json({ message: 'Seeding started', type }); // Respond immediately + + // Hook up the stream callback + streamCallback = (level, text) => { + const icon = level === 'ERROR' ? '❌' : 'ℹ️'; + sendLog(`${icon} ${text}`); + }; + + // sendLog(`🚀 Starting manual seed (${type})...`); // This will be logged by console.log inside functions anyway try { - console.log(`🚀 Starting manual seed (${type})...`); if (type === 'lite') { await seedDatabaseLite(); } else if (type === 'no-locations') { @@ -58,14 +119,30 @@ app.post('/seed', async (req, res) => { } else { await seedDatabase(); } - console.log('✅ Manual seed completed'); + res.write(`data: ${JSON.stringify({ type: 'done' })}\n\n`); } catch (error) { - console.error('❌ Manual seed failed:', error); + console.error('Manual seed fatal error:', error); // Captured by interceptor + res.write(`data: ${JSON.stringify({ type: 'error', error: error.message })}\n\n`); } finally { isSeeding = false; + streamCallback = null; // Detach + res.end(); } }); +// Keep legacy POST for compatibility if needed, using the new shared function logic +app.post('/seed', async (req, res) => { + // ... keep existing logic but warn it's deprecated? + // Actually let's just keep it simple. + if (isSeeding) return res.status(409).json({ error: 'Seeding in progress' }); + isSeeding = true; + res.json({ message: 'Seeding started' }); + try { + await seedDatabase(); + } catch (e) { console.error(e); } + finally { isSeeding = false; } +}); + // Reset endpoint app.post('/reset', async (req, res) => { if (isSeeding) {