| import React, { useEffect, useState } from 'react'; |
| import { motion, AnimatePresence } from 'motion/react'; |
| import { Terminal, Activity, CheckCircle2, AlertCircle, RefreshCw } from 'lucide-react'; |
|
|
| interface GhostLog { |
| id: string; |
| timestamp: number; |
| type: string; |
| payload: any; |
| } |
|
|
| export default function AutomationFeed() { |
| const [logs, setLogs] = useState<GhostLog[]>([]); |
| const [status, setStatus] = useState<any>({ status: 'idle', message: 'Waiting for feed...' }); |
| const [isLoading, setIsLoading] = useState(false); |
|
|
| const fetchLogs = async () => { |
| try { |
| const [logsRes, statusRes] = await Promise.all([ |
| fetch('/api/logs'), |
| fetch('/api/optimization-status') |
| ]); |
| const logsData = await logsRes.json(); |
| const statusData = await statusRes.json(); |
| |
| setLogs(logsData); |
| setStatus(statusData); |
| } catch (err) { |
| console.error("Failed to fetch logs:", err); |
| } |
| }; |
|
|
| useEffect(() => { |
| const interval = setInterval(fetchLogs, 3000); |
| return () => clearInterval(interval); |
| }, []); |
|
|
| const triggerOptimization = () => { |
| setIsLoading(true); |
| setTimeout(() => setIsLoading(false), 2000); |
| }; |
|
|
| const clearLogs = async () => { |
| try { |
| await fetch('/api/webhook', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ type: 'sys_purge', payload: {} }) |
| }); |
| setLogs([]); |
| } catch(e) {} |
| }; |
|
|
| return ( |
| <div className="flex flex-col gap-4"> |
| <div className="flex items-center justify-between border-b border-white/5 pb-2"> |
| <div className="flex items-center gap-2"> |
| <Terminal className="w-3 h-3 text-aura-500" /> |
| <span className="text-[10px] font-mono text-white/40 uppercase tracking-widest">Automation Bridge</span> |
| </div> |
| <button |
| onClick={clearLogs} |
| className="text-[8px] font-mono text-white/20 hover:text-white transition-colors" |
| > |
| CLEAR_CACHE |
| </button> |
| </div> |
| |
| {/* Optimization Header */} |
| <div className="p-3 bg-white/5 border border-white/10 rounded group"> |
| <div className="flex justify-between items-start mb-2"> |
| <p className="text-[9px] font-mono text-white/40 uppercase">Global Pipeline</p> |
| {status.status === 'perfect' ? ( |
| <CheckCircle2 className="w-3 h-3 text-aura-500" /> |
| ) : ( |
| <Activity className="w-3 h-3 text-aura-500 animate-[spin_3s_linear_infinite]" /> |
| )} |
| </div> |
| <p className="text-xs font-serif italic text-white mb-2">{status.message}</p> |
| |
| <div className="flex items-center gap-2"> |
| <div className="flex-1 h-1 bg-white/10 rounded-full overflow-hidden"> |
| <motion.div |
| initial={false} |
| animate={{ width: status.status === 'perfect' ? '100%' : '65%' }} |
| className="h-full bg-aura-500" |
| /> |
| </div> |
| <span className="text-[9px] font-mono text-white/40"> |
| {status.status === 'perfect' ? '10/10' : (status.lastScore ? `${status.lastScore}/10` : 'INIT')} |
| </span> |
| </div> |
| |
| <button |
| disabled={isLoading} |
| onClick={triggerOptimization} |
| className="w-full mt-3 py-1.5 bg-aura-500/10 border border-aura-500/20 text-aura-500 text-[9px] font-mono uppercase tracking-widest hover:bg-aura-500/20 transition-colors flex items-center justify-center gap-2" |
| > |
| {isLoading ? ( |
| <RefreshCw className="w-2.5 h-2.5 animate-spin" /> |
| ) : ( |
| 'Recalibrate Parameters' |
| )} |
| </button> |
| </div> |
| |
| {/* Real-time Feeds */} |
| <div className="flex-1 overflow-hidden flex flex-col gap-2"> |
| <p className="text-[9px] font-mono text-white/20 uppercase tracking-tighter">Handshake Terminal</p> |
| <div className="space-y-1.5 max-h-[140px] overflow-y-auto pr-2 custom-scrollbar"> |
| <AnimatePresence initial={false}> |
| {logs.length > 0 ? logs.map((log) => ( |
| <motion.div |
| key={log.id} |
| initial={{ opacity: 0, x: -10 }} |
| animate={{ opacity: 1, x: 0 }} |
| className="flex gap-2 text-[9px] font-mono border-l border-aura-500/30 pl-2 py-1 bg-white/[0.02] hover:bg-white/[0.05] transition-colors" |
| > |
| <span className="text-white/20 whitespace-nowrap"> |
| {new Date(log.timestamp).toLocaleTimeString().split(' ')[0]} |
| </span> |
| <span className="text-aura-500">[{log.type.toUpperCase()}]</span> |
| <span className="text-white/60 truncate"> |
| {log.type === 'report' ? `REPORT_RCVD SC:${log.payload?.statsJson?.overall}` : 'SYNC_SUCCESS'} |
| </span> |
| </motion.div> |
| )) : ( |
| <div className="text-[9px] font-mono text-white/10 italic">Waiting for Tampermonkey exfiltration...</div> |
| )} |
| </AnimatePresence> |
| </div> |
| </div> |
| |
| <div className="text-[8px] font-mono text-white/20 leading-tight border-t border-white/5 pt-2"> |
| * Ensure Borepub URL in script points to your development app address + /api/webhook |
| </div> |
| </div> |
| ); |
| } |
|
|