import { useMutation, useQuery } from '@tanstack/react-query'
import {
AlertCircle,
CheckCircle2,
Database,
FlaskConical,
Loader2,
RotateCcw,
ScrollText,
Shield,
TriangleAlert,
} from 'lucide-react'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Separator } from '@/components/ui/separator'
import { api } from '@/lib/api-client'
import type { DoctorResponse } from '@/types'
function formatBytes(bytes: number): string {
if (bytes < 1024) return `${bytes} B`
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
}
// ─── Action Card (reusable) ──────────────────────────────────
function ActionCard({
title,
description,
icon,
onRun,
isRunning,
children,
}: {
title: string
description: string
icon: React.ReactNode
onRun?: () => void
isRunning?: boolean
children?: React.ReactNode
}) {
return (
<Card>
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="flex items-center gap-2 text-base">
{icon}
{title}
</CardTitle>
{onRun && (
<Button size="sm" onClick={onRun} disabled={isRunning}>
{isRunning ? (
<Loader2 className="h-3 w-3 animate-spin mr-1" />
) : (
<RotateCcw className="h-3 w-3 mr-1" />
)}
{isRunning ? '...' : ''}
</Button>
)}
</div>
<p className="text-xs text-muted-foreground">{description}</p>
</CardHeader>
{children && <CardContent className="pt-0">{children}</CardContent>}
</Card>
)
}
// ─── Doctor ──────────────────────────────────────────────────
function DoctorPanel() {
const { t } = useTranslation()
const doctorMutation = useMutation({
mutationFn: () => api.post<DoctorResponse>('/api/system/doctor'),
})
const handleRun = () => {
doctorMutation.mutate()
}
const data = doctorMutation.data
return (
<ActionCard
title={t('systemTools.doctor')}
description={t('systemTools.doctorDescription')}
icon={<FlaskConical className="h-4 w-4" />}
onRun={handleRun}
isRunning={doctorMutation.isPending}
>
{data && (
<div className="space-y-3">
{/* Summary */}
<div className="flex items-center gap-3">
<Badge variant={data.issues === 0 ? 'success' : 'destructive'}>
{data.issues === 0
? t('systemTools.allChecksPassed', { count: data.checks })
: t('systemTools.issuesFound', { checks: data.checks, issues: data.issues })}
</Badge>
</div>
{/* Check results */}
<div className="space-y-1.5">
{data.results.map((check) => (
<div
key={check.name}
className="flex items-start gap-2 text-sm rounded-md bg-muted/50 px-3 py-2"
>
{check.status === 'pass' && (
<CheckCircle2 className="h-4 w-4 text-success shrink-0 mt-0.5" />
)}
{check.status === 'warn' && (
<TriangleAlert className="h-4 w-4 text-warning shrink-0 mt-0.5" />
)}
{check.status === 'fail' && (
<AlertCircle className="h-4 w-4 text-error shrink-0 mt-0.5" />
)}
<span className="text-muted-foreground">{check.message}</span>
</div>
))}
</div>
{/* Action items */}
{data.action_items.length > 0 && (
<>
<Separator />
<div>
<p className="text-xs font-medium text-destructive mb-2">
{t('systemTools.actionItems')}
</p>
<ol className="list-decimal list-inside space-y-1 text-sm text-muted-foreground">
{data.action_items.map((item, i) => (
<li key={i}>{item}</li>
))}
</ol>
</div>
</>
)}
</div>
)}
{doctorMutation.isError && (
<div className="flex items-center gap-2 text-sm text-destructive mt-2">
<AlertCircle className="h-4 w-4" />
{(doctorMutation.error as Error)?.message || t('update.unknownError')}
</div>
)}
</ActionCard>
)
}
// ─── Audit Verify ────────────────────────────────────────────
function AuditVerifyPanel() {
const { t } = useTranslation()
const auditMutation = useMutation({
mutationFn: () =>
api.post<{ valid: boolean; entries_checked: number; message: string }>(
'/api/system/audit-verify',
),
})
return (
<ActionCard
title={t('systemTools.auditVerify')}
description={t('systemTools.auditVerifyDescription')}
icon={<Shield className="h-4 w-4" />}
onRun={() => auditMutation.mutate()}
isRunning={auditMutation.isPending}
>
{auditMutation.data && (
<div
className={`flex items-center gap-2 text-sm rounded-md px-3 py-2 ${
auditMutation.data.valid
? 'bg-success-subtle text-success'
: 'bg-error-subtle text-error'
}`}
>
{auditMutation.data.valid ? (
<CheckCircle2 className="h-4 w-4" />
) : (
<AlertCircle className="h-4 w-4" />
)}
{auditMutation.data.message}
</div>
)}
</ActionCard>
)
}
// ─── Backup ──────────────────────────────────────────────────
function BackupPanel() {
const { t } = useTranslation()
const backupMutation = useMutation({
mutationFn: () =>
api.post<{ success: boolean; path: string; size_bytes: number; message: string }>(
'/api/system/backup',
),
})
return (
<ActionCard
title={t('systemTools.backup')}
description={t('systemTools.backupDescription')}
icon={<Database className="h-4 w-4" />}
onRun={() => backupMutation.mutate()}
isRunning={backupMutation.isPending}
>
{backupMutation.data?.success && (
<div className="flex items-center gap-2 text-sm rounded-md bg-success-subtle text-success px-3 py-2">
<CheckCircle2 className="h-4 w-4" />
<span>
{backupMutation.data.message}
<span className="ml-2 text-xs opacity-70">
({formatBytes(backupMutation.data.size_bytes)})
</span>
</span>
</div>
)}
{backupMutation.isError && (
<div className="flex items-center gap-2 text-sm text-destructive mt-2">
<AlertCircle className="h-4 w-4" />
{(backupMutation.error as Error)?.message || t('update.unknownError')}
</div>
)}
</ActionCard>
)
}
// ─── Log Viewer ──────────────────────────────────────────────
function LogPanel() {
const { t } = useTranslation()
const [showLog, setShowLog] = useState(false)
const {
data: logData,
isLoading,
refetch,
} = useQuery({
queryKey: ['system-log'],
queryFn: () => api.get<{ lines: string[]; total: number }>('/api/system/log'),
enabled: showLog,
})
return (
<ActionCard
title={t('systemTools.log')}
description={t('systemTools.logDescription')}
icon={<ScrollText className="h-4 w-4" />}
onRun={() => {
if (showLog) {
refetch()
} else {
setShowLog(true)
}
}}
isRunning={isLoading}
>
{showLog && logData && (
<div className="rounded-md bg-primary-foreground text-primary-foreground dark:bg-card p-3 max-h-80 overflow-y-auto font-mono text-xs leading-relaxed">
{logData.lines.length === 0 ? (
<span className="text-muted-foreground">{t('systemTools.noLogEntries')}</span>
) : (
logData.lines.map((line, i) => (
<div key={i} className="whitespace-pre-wrap break-all">
<span className="text-muted-foreground select-none mr-2">
{String(i + 1).padStart(3)}
</span>
{line}
</div>
))
)}
</div>
)}
{showLog && logData && (
<p className="text-xs text-muted-foreground mt-2">
{t('systemTools.showingLines', { shown: logData.lines.length, total: logData.total })}
</p>
)}
</ActionCard>
)
}
// ─── Main Component ──────────────────────────────────────────
export function SystemToolsPanel() {
const { t } = useTranslation()
return (
<div className="space-y-4">
<div>
<h3 className="text-lg font-semibold">{t('systemTools.title')}</h3>
<p className="text-sm text-muted-foreground">{t('systemTools.subtitle')}</p>
</div>
<div className="grid gap-4 md:grid-cols-2">
<DoctorPanel />
<AuditVerifyPanel />
<BackupPanel />
<LogPanel />
</div>
</div>
)
}