abbashkyt-creator 7d8ce0e322 V0.1
2026-03-14 04:02:22 +03:00

298 lines
10 KiB
TypeScript

'use client'
import { useEffect, useState, useRef, useCallback } from 'react'
import { motion } from 'framer-motion'
import { cn, formatUptime, timeAgo } from '@/lib/utils'
import { useEngineStore } from '@/store/engineStore'
import {
pauseEngine,
resumeEngine,
restartEngine,
killEngine,
} from '@/lib/api/engine'
import type { TargetSite } from '@/lib/types'
const BASE = 'http://localhost:8000'
const SITE_POLL_MS = 25_000
function StatusOrb({ status }: { status: 'Running' | 'Paused' | 'Idle' }) {
const map = {
Running: { color: '#00e87b', label: 'Running', shadow: 'rgba(0,232,123,0.5)' },
Paused: { color: '#fbbf24', label: 'Paused', shadow: 'rgba(251,191,36,0.5)' },
Idle: { color: '#3d4f78', label: 'Idle', shadow: 'rgba(61,79,120,0.3)' },
}
const m = map[status] ?? map.Idle
return (
<div className="flex items-center gap-2.5">
{/* Pulsing dot + aura */}
<div className="relative flex items-center justify-center">
{status === 'Running' && (
<span
className="absolute inline-flex rounded-full opacity-60"
style={{
width: 20, height: 20,
background: m.color,
filter: 'blur(6px)',
animation: 'glow-pulse 2s ease-in-out infinite',
}}
/>
)}
<span
className="relative inline-flex rounded-full"
style={{
width: 10, height: 10,
background: m.color,
boxShadow: `0 0 10px ${m.shadow}`,
animation: status === 'Running' ? 'pulse-ring 2s ease-out infinite' : undefined,
}}
/>
</div>
<span
className="text-[13px] font-bold tracking-tight"
style={{ color: m.color }}
>
{m.label}
</span>
</div>
)
}
interface CtrlBtn {
label: string
icon: React.ReactNode
onClick: () => void
variant: 'default' | 'danger'
disabled?: boolean
}
function ControlButton({ label, icon, onClick, variant, disabled }: CtrlBtn) {
const [busy, setBusy] = useState(false)
const handle = async () => {
if (busy || disabled) return
setBusy(true)
try { await onClick() } finally {
setTimeout(() => setBusy(false), 1500)
}
}
return (
<motion.button
whileHover={!disabled ? { scale: 1.03, y: -1 } : undefined}
whileTap={!disabled ? { scale: 0.96 } : undefined}
onClick={handle}
disabled={disabled || busy}
className={cn(
'flex items-center justify-center gap-1.5 rounded-xl text-[11px] font-semibold px-3 py-2.5 transition-all duration-200',
'border disabled:opacity-40 disabled:cursor-not-allowed',
variant === 'danger'
? 'border-g-red/25 bg-g-red/8 text-g-red hover:bg-g-red/15 hover:border-g-red/40 hover:shadow-[0_0_20px_rgba(244,63,94,0.12)]'
: 'border-g-border/60 bg-g-raised/60 text-g-muted hover:text-g-text hover:border-g-line hover:bg-g-raised hover:shadow-[0_4px_16px_rgba(0,0,0,0.4)]',
)}
>
{busy ? (
<svg
className="animate-spin shrink-0"
width="11" height="11" viewBox="0 0 24 24"
fill="none" stroke="currentColor" strokeWidth="2.5"
>
<path d="M21 12a9 9 0 1 1-6.219-8.56"/>
</svg>
) : icon}
{label}
</motion.button>
)
}
function SiteRow({ site }: { site: TargetSite }) {
const isEnabled = site.enabled === 1
const isCooling = site.cooldown_until ? new Date(site.cooldown_until) > new Date() : false
const hasError = site.consecutive_failures > 0
let statusColor = '#3d4f78'
let statusLabel = 'Disabled'
if (isEnabled && isCooling) { statusColor = '#fbbf24'; statusLabel = 'Cooldown' }
else if (isEnabled && hasError) { statusColor = '#f43f5e'; statusLabel = `${site.consecutive_failures}x fail` }
else if (isEnabled) { statusColor = '#00e87b'; statusLabel = 'Active' }
return (
<div className="flex items-center gap-2.5 py-2 px-4 border-b border-g-border/15 last:border-0 group">
<span
className="w-1.5 h-1.5 rounded-full shrink-0 transition-all"
style={{
background: statusColor,
boxShadow: isEnabled && !isCooling && !hasError ? `0 0 5px ${statusColor}80` : undefined,
}}
/>
<span className="flex-1 text-[11px] text-g-muted group-hover:text-g-text transition-colors truncate font-medium">
{site.name}
</span>
<span
className="text-[10px] font-semibold shrink-0 tabular-nums"
style={{ color: statusColor }}
>
{statusLabel}
</span>
</div>
)
}
export default function EngineConsole() {
const { status, uptime_seconds, total_scanned, last_cycle, isOffline } = useEngineStore()
const [sites, setSites] = useState<TargetSite[]>([])
const [cycleAgo, setCycleAgo] = useState('—')
const aliveRef = useRef(true)
const fetchSites = useCallback(async () => {
try {
const res = await fetch(`${BASE}/api/sites`)
const data: TargetSite[] = await res.json()
if (aliveRef.current) setSites(Array.isArray(data) ? data : [])
} catch { /* silent */ }
}, [])
useEffect(() => {
aliveRef.current = true
fetchSites()
const id = setInterval(fetchSites, SITE_POLL_MS)
return () => { aliveRef.current = false; clearInterval(id) }
}, [fetchSites])
// Live relative timestamp
useEffect(() => {
const tick = () => setCycleAgo(timeAgo(last_cycle === 'Never' ? null : last_cycle))
tick()
const id = setInterval(tick, 5000)
return () => clearInterval(id)
}, [last_cycle])
const isRunning = status === 'Running'
const isPaused = status === 'Paused'
const enabledCount = sites.filter(s => s.enabled === 1).length
const controls: CtrlBtn[] = [
{
label: 'Pause',
variant: 'default',
disabled: !isRunning,
onClick: () => pauseEngine(),
icon: (
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/>
</svg>
),
},
{
label: 'Resume',
variant: 'default',
disabled: !isPaused,
onClick: () => resumeEngine(),
icon: (
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<polygon points="5 3 19 12 5 21 5 3"/>
</svg>
),
},
{
label: 'Restart',
variant: 'default',
disabled: false,
onClick: () => restartEngine(),
icon: (
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<polyline points="1 4 1 10 7 10"/><path d="M3.51 15a9 9 0 1 0 .49-3.51"/>
</svg>
),
},
{
label: 'Kill',
variant: 'danger',
disabled: false,
onClick: () => killEngine(),
icon: (
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
</svg>
),
},
]
return (
<motion.div
initial={{ opacity: 1, x: 12 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.2, duration: 0.5, ease: [0.22, 1, 0.36, 1] }}
className="g-card flex flex-col overflow-hidden"
>
{/* ── Status Hero ── */}
<div className="px-5 pt-5 pb-4 border-b border-g-border/40 space-y-4">
<div className="flex items-center justify-between">
<span className="text-[10px] font-bold uppercase tracking-[0.14em] text-g-faint">
Engine Status
</span>
{isOffline && (
<span className="g-badge g-badge-red text-[9px]">OFFLINE</span>
)}
</div>
{/* Big status */}
<StatusOrb status={status} />
{/* Telemetry strip */}
<div className="grid grid-cols-2 gap-3">
<div className="bg-g-raised/50 rounded-xl p-3 border border-g-border/30">
<p className="text-[9px] uppercase tracking-widest text-g-faint font-bold mb-1">Uptime</p>
<p className="text-sm font-bold text-g-text tabular-nums font-mono">
{formatUptime(uptime_seconds)}
</p>
</div>
<div className="bg-g-raised/50 rounded-xl p-3 border border-g-border/30">
<p className="text-[9px] uppercase tracking-widest text-g-faint font-bold mb-1">Last Scan</p>
<p className="text-sm font-bold text-g-text tabular-nums font-mono">
{cycleAgo}
</p>
</div>
<div className="bg-g-raised/50 rounded-xl p-3 border border-g-border/30 col-span-2">
<p className="text-[9px] uppercase tracking-widest text-g-faint font-bold mb-1">Lots Scanned</p>
<p className="text-sm font-bold text-g-text tabular-nums font-mono">
{total_scanned.toLocaleString()}
</p>
</div>
</div>
</div>
{/* ── Controls ── */}
<div className="px-5 py-4 border-b border-g-border/40 space-y-2.5">
<span className="text-[10px] font-bold uppercase tracking-[0.14em] text-g-faint block">
Controls
</span>
<div className="grid grid-cols-2 gap-2">
{controls.map(btn => (
<ControlButton key={btn.label} {...btn} />
))}
</div>
</div>
{/* ── Site Health ── */}
<div className="flex flex-col flex-1 min-h-0">
<div className="flex items-center gap-2 px-4 py-2.5 border-b border-g-border/25">
<span className="text-[10px] font-bold uppercase tracking-[0.14em] text-g-faint">
Sites
</span>
<span className="text-[10px] text-g-faint/50 font-mono ml-auto">
{enabledCount}/{sites.length} active
</span>
</div>
<div className="flex-1 overflow-y-auto" style={{ maxHeight: 200 }}>
{sites.length === 0 ? (
<p className="text-[11px] text-g-faint/40 text-center py-6">No sites configured</p>
) : (
sites.map(s => <SiteRow key={s.id} site={s} />)
)}
</div>
</div>
</motion.div>
)
}