// ui/src/components/superadmin/WorkerLogsPage.tsx
import React, { useState, useEffect, useCallback } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
import { Button } from '../ui/button';
import { Input } from '../ui/input';
import { Badge } from '../ui/badge';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../ui/table';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '../ui/dialog';
import { ScrollArea } from '../ui/scroll-area';
import { DatePicker } from '../ui/date-picker';
import {
Search,
RefreshCw,
Server,
AlertCircle,
ChevronLeft,
ChevronRight,
Eye,
Loader2,
ExternalLink,
} from 'lucide-react';
import { getWorkerLogs, type WorkerLog } from '@/lib/serverComm';
import { toast } from '@/lib/toast';
import { formatDateTimePKT } from '@/lib/datetime';
import { format } from 'date-fns';
export default function WorkerLogsPage() {
const [logs, setLogs] = useState<WorkerLog[]>([]);
const [loading, setLoading] = useState(true);
const [total, setTotal] = useState(0);
const [totalPages, setTotalPages] = useState(1);
const [currentPage, setCurrentPage] = useState(1);
const [selectedLog, setSelectedLog] = useState<WorkerLog | null>(null);
const [dialogOpen, setDialogOpen] = useState(false);
const [startDate, setStartDate] = useState('');
const [endDate, setEndDate] = useState('');
const [severityFilter, setSeverityFilter] = useState('all');
const [searchQuery, setSearchQuery] = useState('');
const limit = 50;
const fetchLogs = useCallback(async (page = currentPage) => {
try {
setLoading(true);
const result = await getWorkerLogs({
page,
limit,
startDate: startDate || undefined,
endDate: endDate || undefined,
severity: severityFilter !== 'all' ? severityFilter : undefined,
search: searchQuery || undefined,
});
setLogs(result.logs);
setTotal(result.total);
setTotalPages(result.totalPages);
} catch (error: any) {
toast.error(error.message || 'Failed to fetch worker logs');
setLogs([]);
} finally {
setLoading(false);
}
}, [currentPage, startDate, endDate, severityFilter, searchQuery]);
useEffect(() => {
fetchLogs(currentPage);
}, [currentPage]);
const handleFilter = () => {
setCurrentPage(1);
fetchLogs(1);
};
const handleReset = () => {
setStartDate('');
setEndDate('');
setSeverityFilter('all');
setSearchQuery('');
setCurrentPage(1);
setTimeout(() => fetchLogs(1), 0);
};
const getSeverityBadge = (severity: string) => {
if (severity === 'FATAL') {
return <Badge className="bg-red-900 text-white text-xs">FATAL</Badge>;
}
return <Badge className="bg-red-600 text-white text-xs">ERROR</Badge>;
};
const getWorkerName = (log: WorkerLog): string => {
const attrs = log.resourceAttrs as Record<string, unknown> | null;
return (attrs?.['service.name'] as string) ?? (attrs?.['faas.name'] as string) ?? '—';
};
const openDetail = (log: WorkerLog) => {
setSelectedLog(log);
setDialogOpen(true);
};
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Server className="h-5 w-5 text-muted-foreground" />
<h2 className="text-xl font-semibold">Worker Logs</h2>
<Badge variant="outline" className="ml-1">{total} errors stored</Badge>
</div>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => fetchLogs(currentPage)}
disabled={loading}
>
<RefreshCw className={`h-4 w-4 mr-1 ${loading ? 'animate-spin' : ''}`} />
Refresh
</Button>
<a
href="https://dash.cloudflare.com"
target="_blank"
rel="noopener noreferrer"
>
<Button variant="outline" size="sm">
<ExternalLink className="h-4 w-4 mr-1" />
Cloudflare Dashboard
</Button>
</a>
</div>
</div>
{/* Filters */}
<Card>
<CardContent className="pt-4">
<div className="flex flex-wrap gap-3 items-end">
<div className="flex flex-col gap-1">
<span className="text-xs text-muted-foreground">From</span>
<DatePicker
value={startDate}
onChange={(date) => setStartDate(date ? format(date, 'yyyy-MM-dd') : '')}
placeholder="Start date"
/>
</div>
<div className="flex flex-col gap-1">
<span className="text-xs text-muted-foreground">To</span>
<DatePicker
value={endDate}
onChange={(date) => setEndDate(date ? format(date, 'yyyy-MM-dd') : '')}
placeholder="End date"
/>
</div>
<div className="flex flex-col gap-1">
<span className="text-xs text-muted-foreground">Severity</span>
<Select value={severityFilter} onValueChange={setSeverityFilter}>
<SelectTrigger className="w-32">
<SelectValue placeholder="All" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All</SelectItem>
<SelectItem value="ERROR">ERROR</SelectItem>
<SelectItem value="FATAL">FATAL</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-1 flex-1 min-w-48">
<span className="text-xs text-muted-foreground">Search message</span>
<div className="relative">
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
className="pl-8"
placeholder="Search error messages..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleFilter()}
/>
</div>
</div>
<Button onClick={handleFilter} disabled={loading}>Filter</Button>
<Button variant="outline" onClick={handleReset} disabled={loading}>Reset</Button>
</div>
</CardContent>
</Card>
{/* Table */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-base">
<AlertCircle className="h-4 w-4 text-red-500" />
Error Logs
<span className="text-sm font-normal text-muted-foreground ml-1">
(stored permanently — INFO/DEBUG/WARN available in Cloudflare Dashboard)
</span>
</CardTitle>
</CardHeader>
<CardContent>
{loading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
) : logs.length === 0 ? (
<div className="text-center py-12 text-muted-foreground">
No error logs found for the selected filters.
</div>
) : (
<>
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-44">Timestamp</TableHead>
<TableHead className="w-24">Severity</TableHead>
<TableHead className="w-32">Worker</TableHead>
<TableHead>Message</TableHead>
<TableHead className="w-10"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{logs.map((log) => (
<TableRow key={log.id} className="cursor-pointer hover:bg-muted/50">
<TableCell className="text-xs text-muted-foreground whitespace-nowrap">
{formatDateTimePKT(log.timestamp)}
</TableCell>
<TableCell>{getSeverityBadge(log.severity)}</TableCell>
<TableCell className="text-xs font-mono">{getWorkerName(log)}</TableCell>
<TableCell
className="text-sm max-w-md truncate"
title={log.message}
>
{log.message || '(no message)'}
</TableCell>
<TableCell>
<Button
variant="ghost"
size="icon"
className="h-7 w-7"
onClick={() => openDetail(log)}
>
<Eye className="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
{/* Pagination */}
<div className="flex items-center justify-between mt-4 text-sm text-muted-foreground">
<span>
Showing {(currentPage - 1) * limit + 1}–
{Math.min(currentPage * limit, total)} of {total}
</span>
<div className="flex gap-1">
<Button
variant="outline"
size="icon"
className="h-8 w-8"
disabled={currentPage <= 1}
onClick={() => setCurrentPage((p) => p - 1)}
>
<ChevronLeft className="h-4 w-4" />
</Button>
<span className="px-3 py-1 border rounded text-xs">
{currentPage} / {totalPages}
</span>
<Button
variant="outline"
size="icon"
className="h-8 w-8"
disabled={currentPage >= totalPages}
onClick={() => setCurrentPage((p) => p + 1)}
>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
</div>
</>
)}
</CardContent>
</Card>
{/* Detail Dialog */}
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
{selectedLog && getSeverityBadge(selectedLog.severity)}
<span className="text-sm font-mono truncate max-w-sm">
{selectedLog?.message}
</span>
</DialogTitle>
</DialogHeader>
{selectedLog && (
<ScrollArea className="max-h-[60vh]">
<div className="space-y-4 pr-4">
<div className="grid grid-cols-2 gap-2 text-sm">
<div>
<span className="text-muted-foreground">Timestamp</span>
<p className="font-mono">{formatDateTimePKT(selectedLog.timestamp)}</p>
</div>
<div>
<span className="text-muted-foreground">Trace ID</span>
<p className="font-mono text-xs">{selectedLog.traceId ?? '—'}</p>
</div>
</div>
{selectedLog.resourceAttrs && (
<div>
<p className="text-xs font-semibold text-muted-foreground mb-1 uppercase tracking-wide">Resource Attributes</p>
<pre className="bg-muted rounded p-3 text-xs overflow-auto">
{JSON.stringify(selectedLog.resourceAttrs, null, 2)}
</pre>
</div>
)}
{selectedLog.logAttrs && (
<div>
<p className="text-xs font-semibold text-muted-foreground mb-1 uppercase tracking-wide">Log Attributes</p>
<pre className="bg-muted rounded p-3 text-xs overflow-auto">
{JSON.stringify(selectedLog.logAttrs, null, 2)}
</pre>
</div>
)}
</div>
</ScrollArea>
)}
</DialogContent>
</Dialog>
</div>
);
}