113 lines
4.2 KiB
TypeScript
113 lines
4.2 KiB
TypeScript
'use client'
|
|
import { useState, useRef } from 'react'
|
|
import { useSortable } from '@dnd-kit/sortable'
|
|
import { CSS } from '@dnd-kit/utilities'
|
|
import { useUpdateSite, useDeleteSite, useAdaptSite, useSiteSelectors } from '@/hooks/useSites'
|
|
import { useQueryClient } from '@tanstack/react-query'
|
|
import type { TargetSite } from '@/lib/types'
|
|
import { cn } from '@/lib/utils'
|
|
|
|
function HealthBadge({ site }: { site: TargetSite }) {
|
|
const inCooldown = site.cooldown_until && new Date(site.cooldown_until) > new Date()
|
|
if (inCooldown) return <span className="g-badge g-badge-amber">Cooldown</span>
|
|
if (site.consecutive_failures > 2) return <span className="g-badge g-badge-red">{site.error_count} errors</span>
|
|
return <span className="g-badge g-badge-green">OK</span>
|
|
}
|
|
|
|
function ConfidenceBadge({ siteId }: { siteId: number }) {
|
|
const { data: sel } = useSiteSelectors(siteId)
|
|
if (!sel) return <span className="text-g-faint text-xs">—</span>
|
|
const cls = sel.confidence >= 70 ? 'g-badge-green' : sel.confidence >= 40 ? 'g-badge-amber' : 'g-badge-red'
|
|
return (
|
|
<span className={`g-badge ${cls}`}>
|
|
{sel.confidence}%{sel.stale ? ' ⚠' : ''}
|
|
</span>
|
|
)
|
|
}
|
|
|
|
const ADAPT_POLL_DELAY = 45_000
|
|
|
|
export default function SiteRow({ site }: { site: TargetSite }) {
|
|
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: site.id })
|
|
const updateSiteMut = useUpdateSite()
|
|
const deleteSiteMut = useDeleteSite()
|
|
const adaptSiteMut = useAdaptSite()
|
|
const qc = useQueryClient()
|
|
const [adapting, setAdapting] = useState(false)
|
|
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
|
|
const handleAdapt = () => {
|
|
adaptSiteMut.mutate(site.id, {
|
|
onSuccess: () => {
|
|
setAdapting(true)
|
|
if (timerRef.current) clearTimeout(timerRef.current)
|
|
timerRef.current = setTimeout(() => {
|
|
setAdapting(false)
|
|
qc.invalidateQueries({ queryKey: ['selectors', site.id] })
|
|
}, ADAPT_POLL_DELAY)
|
|
},
|
|
})
|
|
}
|
|
|
|
const style = { transform: CSS.Transform.toString(transform), transition }
|
|
const isAdapting = adaptSiteMut.isPending || adapting
|
|
|
|
return (
|
|
<tr ref={setNodeRef} style={style} className="group">
|
|
<td className="w-8">
|
|
<span
|
|
{...attributes} {...listeners}
|
|
className="cursor-grab text-g-faint/30 hover:text-g-faint transition-colors select-none"
|
|
>
|
|
⋮⋮
|
|
</span>
|
|
</td>
|
|
<td>
|
|
<span className="text-sm font-medium text-g-text">{site.name}</span>
|
|
</td>
|
|
<td>
|
|
<span className="text-xs font-mono text-g-faint truncate max-w-xs block">{site.url_template}</span>
|
|
</td>
|
|
<td><HealthBadge site={site} /></td>
|
|
<td><ConfidenceBadge siteId={site.id} /></td>
|
|
<td>
|
|
<label className="flex items-center gap-2 cursor-pointer w-fit">
|
|
<div
|
|
onClick={() => updateSiteMut.mutate({ id: site.id, data: { enabled: site.enabled ? 0 : 1 } })}
|
|
className={cn(
|
|
'relative w-8 h-4 rounded-full transition-colors duration-200 cursor-pointer flex-shrink-0',
|
|
site.enabled ? 'bg-g-green/30 border border-g-green/40' : 'bg-g-raised border border-g-border'
|
|
)}
|
|
>
|
|
<span className={cn(
|
|
'absolute top-0.5 w-3 h-3 rounded-full transition-transform duration-200 shadow-sm',
|
|
site.enabled ? 'left-[18px] bg-g-green' : 'left-0.5 bg-g-faint'
|
|
)} />
|
|
</div>
|
|
<span className="text-xs text-g-faint">{site.enabled ? 'On' : 'Off'}</span>
|
|
</label>
|
|
</td>
|
|
<td>
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
onClick={handleAdapt}
|
|
disabled={isAdapting}
|
|
className={cn(
|
|
'g-btn text-xs h-7',
|
|
isAdapting && 'opacity-60 cursor-not-allowed'
|
|
)}
|
|
>
|
|
{isAdapting ? 'Adapting…' : 'Adapt AI'}
|
|
</button>
|
|
<button
|
|
onClick={() => { if (confirm(`Delete "${site.name}"?`)) deleteSiteMut.mutate(site.id) }}
|
|
className="text-g-faint hover:text-g-red transition-colors text-xs opacity-0 group-hover:opacity-100"
|
|
>
|
|
✕
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
)
|
|
}
|