2026-06-07 22:30:33 +00:00
|
|
|
'use client';
|
|
|
|
|
|
2026-06-16 18:43:45 +00:00
|
|
|
import React, { useState, useEffect } from 'react';
|
2026-06-07 22:30:33 +00:00
|
|
|
import { Activity, CheckCircle, Clock, XCircle, Zap, Filter } from 'lucide-react';
|
2026-06-16 18:43:45 +00:00
|
|
|
import { Spinner } from '@/components/ui/Spinner';
|
2026-06-07 22:30:33 +00:00
|
|
|
|
|
|
|
|
interface LogEntry {
|
|
|
|
|
id: string;
|
|
|
|
|
timestamp: string;
|
|
|
|
|
campaign_name: string;
|
|
|
|
|
rule_name: string;
|
|
|
|
|
action: string;
|
|
|
|
|
details: string;
|
|
|
|
|
status: 'executed' | 'pending' | 'failed' | 'skipped';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const statusConfig = {
|
|
|
|
|
executed: { icon: <CheckCircle size={14} />, color: '#4ade80', bg: 'rgba(34,197,94,0.1)', label: 'Executado' },
|
|
|
|
|
pending: { icon: <Clock size={14} />, color: '#fbbf24', bg: 'rgba(245,158,11,0.1)', label: 'Pendente' },
|
|
|
|
|
failed: { icon: <XCircle size={14} />, color: '#f87171', bg: 'rgba(239,68,68,0.1)', label: 'Falhou' },
|
|
|
|
|
skipped: { icon: <Clock size={14} />, color: '#8888a0', bg: 'var(--overlay-05)', label: 'Pulado' },
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function timeAgo(dateStr: string): string {
|
|
|
|
|
const diff = Date.now() - new Date(dateStr).getTime();
|
|
|
|
|
const mins = Math.floor(diff / 60000);
|
|
|
|
|
if (mins < 60) return `${mins}min atrás`;
|
|
|
|
|
const hours = Math.floor(mins / 60);
|
|
|
|
|
if (hours < 24) return `${hours}h atrás`;
|
|
|
|
|
const days = Math.floor(hours / 24);
|
|
|
|
|
return `${days}d atrás`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default function ActivityPage() {
|
|
|
|
|
const [filter, setFilter] = useState<string>('all');
|
2026-06-16 18:43:45 +00:00
|
|
|
const [allLogs, setAllLogs] = useState<LogEntry[]>([]);
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
(async () => {
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch('/api/engine/logs');
|
|
|
|
|
const data = await res.json();
|
|
|
|
|
if (data.success) setAllLogs(data.data || []);
|
|
|
|
|
} catch {
|
|
|
|
|
/* ignora */
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false);
|
|
|
|
|
}
|
|
|
|
|
})();
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const logs = filter === 'all' ? allLogs : allLogs.filter((l) => l.status === filter);
|
2026-06-07 22:30:33 +00:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex flex-col gap-6">
|
|
|
|
|
{/* Header */}
|
|
|
|
|
<div className="flex items-center justify-between animate-fade-in-up">
|
|
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
|
<div className="w-10 h-10 rounded-xl flex items-center justify-center" style={{ background: 'rgba(34,197,94,0.15)', border: '1px solid rgba(34,197,94,0.25)' }}>
|
|
|
|
|
<Activity size={20} style={{ color: '#4ade80' }} />
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<h1 className="text-2xl font-bold text-white">Atividade</h1>
|
|
|
|
|
<p className="text-sm" style={{ color: 'var(--muted-fg)' }}>Ações executadas pelo agente de automação</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex items-center gap-1 p-1 rounded-xl" style={{ background: 'var(--overlay-03)', border: '1px solid var(--overlay-06)' }}>
|
|
|
|
|
<Filter size={14} className="ml-2" style={{ color: 'var(--muted-fg)' }} />
|
|
|
|
|
{['all', 'executed', 'pending', 'skipped', 'failed'].map((f) => (
|
|
|
|
|
<button key={f} onClick={() => setFilter(f)} className="px-3 py-1.5 rounded-lg text-xs font-medium transition-all"
|
|
|
|
|
style={{ background: filter === f ? 'var(--accent)' : 'transparent', color: filter === f ? 'white' : 'var(--muted-fg)' }}>
|
|
|
|
|
{f === 'all' ? 'Todos' : statusConfig[f as keyof typeof statusConfig].label}
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Stats */}
|
|
|
|
|
<div className="grid grid-cols-4 gap-4 animate-fade-in-up delay-1">
|
|
|
|
|
{[
|
2026-06-16 18:43:45 +00:00
|
|
|
{ label: 'Total', value: allLogs.length, color: 'var(--accent-soft-fg)' },
|
|
|
|
|
{ label: 'Executados', value: allLogs.filter((l) => l.status === 'executed').length, color: '#4ade80' },
|
|
|
|
|
{ label: 'Pendentes', value: allLogs.filter((l) => l.status === 'pending').length, color: '#fbbf24' },
|
|
|
|
|
{ label: 'Pulados', value: allLogs.filter((l) => l.status === 'skipped').length, color: '#8888a0' },
|
2026-06-07 22:30:33 +00:00
|
|
|
].map((s) => (
|
|
|
|
|
<div key={s.label} className="glass-card-static p-4 text-center">
|
|
|
|
|
<p className="text-2xl font-bold" style={{ color: s.color }}>{s.value}</p>
|
|
|
|
|
<p className="text-xs mt-1" style={{ color: 'var(--muted-fg)' }}>{s.label}</p>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Timeline */}
|
|
|
|
|
<div className="glass-card-static overflow-hidden animate-fade-in-up delay-2">
|
|
|
|
|
<div className="p-5 border-b" style={{ borderColor: 'var(--overlay-06)' }}>
|
|
|
|
|
<h2 className="text-base font-semibold text-white">Timeline</h2>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="divide-y" style={{ borderColor: 'var(--overlay-04)' }}>
|
2026-06-16 18:43:45 +00:00
|
|
|
{loading ? (
|
|
|
|
|
<div className="p-12 flex justify-center">
|
|
|
|
|
<Spinner />
|
|
|
|
|
</div>
|
|
|
|
|
) : logs.length === 0 ? (
|
2026-06-07 22:30:33 +00:00
|
|
|
<div className="p-12 text-center">
|
2026-06-16 18:43:45 +00:00
|
|
|
<p className="text-[var(--muted-fg)]">Nenhuma atividade ainda. Quando o engine agir ou recomendar algo, aparece aqui.</p>
|
2026-06-07 22:30:33 +00:00
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
logs.map((log) => {
|
|
|
|
|
const sc = statusConfig[log.status];
|
|
|
|
|
return (
|
|
|
|
|
<div key={log.id} className="flex items-start gap-4 p-5 hover:bg-white/[0.02] transition-colors">
|
|
|
|
|
{/* Timeline dot */}
|
|
|
|
|
<div className="mt-1 flex flex-col items-center">
|
|
|
|
|
<div className="w-8 h-8 rounded-full flex items-center justify-center" style={{ background: sc.bg }}>
|
|
|
|
|
<Zap size={14} style={{ color: sc.color }} />
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
{/* Content */}
|
|
|
|
|
<div className="flex-1 min-w-0">
|
|
|
|
|
<div className="flex items-center gap-2 mb-1">
|
|
|
|
|
<p className="font-medium text-white text-sm">{log.campaign_name}</p>
|
|
|
|
|
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[10px] font-medium" style={{ background: sc.bg, color: sc.color }}>
|
|
|
|
|
{sc.icon} {sc.label}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
<p className="text-xs" style={{ color: 'var(--muted-fg)' }}>
|
|
|
|
|
Regra: <span className="text-white">{log.rule_name}</span>
|
|
|
|
|
</p>
|
|
|
|
|
<p className="text-xs mt-1" style={{ color: 'var(--muted-fg)' }}>{log.details}</p>
|
|
|
|
|
</div>
|
|
|
|
|
{/* Time */}
|
|
|
|
|
<span className="text-xs whitespace-nowrap" style={{ color: 'var(--muted-fg)' }}>
|
|
|
|
|
{timeAgo(log.timestamp)}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|