refactor: modularize error logs tab

This commit is contained in:
2025-12-27 17:34:49 +00:00
parent 43b904a0ca
commit bd3779820a
9 changed files with 528 additions and 763 deletions
@@ -1,801 +1,81 @@
"use client"
import { useState, useEffect } from 'react'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui'
import { Button } from '@/components/ui'
import { Badge } from '@/components/ui'
import { ScrollArea } from '@/components/ui'
import { Input } from '@/components/ui'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui'
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui'
import { Warning, CheckCircle, Info, Trash, Broom } from '@phosphor-icons/react'
import { Database } from '@/lib/database'
import type { ErrorLog } from '@/lib/db/error-logs'
import { useState } from 'react'
import { Card, CardContent, CardHeader } from '@/components/ui'
import type { User } from '@/lib/level-types'
import { toast } from 'sonner'
import { clearErrorLogs, deleteErrorLog, markErrorResolved } from './error-logs/errorLogActions'
import { ErrorLogControls } from './error-logs/ErrorLogControls'
import { ErrorLogList } from './error-logs/ErrorLogList'
import { ErrorLogStats } from './error-logs/ErrorLogStats'
import { ClearLogsDialog } from './error-logs/ClearLogsDialog'
import { filterLogs, useErrorLogFilters } from './error-logs/useErrorLogFilters'
import { useErrorLogs } from './error-logs/useErrorLogs'
interface ErrorLogsTabProps {
user?: User // Optional: If provided, filters logs by user's tenantId (for God tier)
}
export function ErrorLogsTab({ user }: ErrorLogsTabProps) {
const [logs, setLogs] = useState<ErrorLog[]>([])
const [loading, setLoading] = useState(false)
const [filterLevel, setFilterLevel] = useState<string>('all')
const [filterResolved, setFilterResolved] = useState<string>('all')
const { logs, loading, stats, reload, isSuperGod } = useErrorLogs(user)
const { filters, setFilterLevel, setFilterResolution } = useErrorLogFilters()
const [showClearDialog, setShowClearDialog] = useState(false)
const [clearOnlyResolved, setClearOnlyResolved] = useState(false)
const [stats, setStats] = useState({
total: 0,
errors: 0,
warnings: 0,
info: 0,
resolved: 0,
unresolved: 0,
})
// Determine access level based on user role
const isSuperGod = user?.role === 'supergod'
const tenantId = user?.tenantId
useEffect(() => {
loadLogs()
}, [])
const loadLogs = async () => {
setLoading(true)
try {
// SuperGod sees all logs, God sees only their tenant's logs
const options = isSuperGod ? {} : { tenantId }
const data = await Database.getErrorLogs(options)
setLogs(data)
calculateStats(data)
} catch (err) {
toast.error('Failed to load error logs')
console.error('Error loading logs:', err)
} finally {
setLoading(false)
}
}
const calculateStats = (logs: ErrorLog[]) => {
setStats({
total: logs.length,
errors: logs.filter(l => l.level === 'error').length,
warnings: logs.filter(l => l.level === 'warning').length,
info: logs.filter(l => l.level === 'info').length,
resolved: logs.filter(l => l.resolved).length,
unresolved: logs.filter(l => !l.resolved).length,
})
}
const filteredLogs = filterLogs(logs, filters)
const handleMarkResolved = async (id: string) => {
try {
await Database.updateErrorLog(id, {
resolved: true,
resolvedAt: Date.now(),
resolvedBy: user?.username || 'admin',
})
await loadLogs()
toast.success('Error log marked as resolved')
} catch (err) {
toast.error('Failed to update error log')
}
await markErrorResolved(id, reload, user)
}
const handleDeleteLog = async (id: string) => {
try {
await Database.deleteErrorLog(id)
await loadLogs()
toast.success('Error log deleted')
} catch (err) {
toast.error('Failed to delete error log')
}
await deleteErrorLog(id, reload)
}
const handleClearLogs = async () => {
try {
const count = await Database.clearErrorLogs(clearOnlyResolved)
await loadLogs()
toast.success(`Cleared ${count} error log${count !== 1 ? 's' : ''}`)
setShowClearDialog(false)
} catch (err) {
toast.error('Failed to clear error logs')
}
await clearErrorLogs(clearOnlyResolved, reload, () => setShowClearDialog(false))
}
const getLevelIcon = (level: string) => {
switch (level) {
case 'error':
return <Warning className="w-5 h-5" weight="fill" />
case 'warning':
return <Warning className="w-5 h-5" />
case 'info':
return <Info className="w-5 h-5" />
default:
return <Info className="w-5 h-5" />
}
const openClearDialog = (onlyResolved: boolean) => {
setClearOnlyResolved(onlyResolved)
setShowClearDialog(true)
}
const getLevelColor = (level: string) => {
switch (level) {
case 'error':
return 'bg-red-500/20 text-red-400 border-red-500/50'
case 'warning':
return 'bg-yellow-500/20 text-yellow-400 border-yellow-500/50'
case 'info':
return 'bg-blue-500/20 text-blue-400 border-blue-500/50'
default:
return 'bg-gray-500/20 text-gray-400 border-gray-500/50'
}
}
const filteredLogs = logs.filter(log => {
if (filterLevel !== 'all' && log.level !== filterLevel) return false
if (filterResolved === 'resolved' && !log.resolved) return false
if (filterResolved === 'unresolved' && log.resolved) return false
return true
})
const scopeDescription = isSuperGod
? 'All error logs across all tenants'
: `Error logs for your tenant only`
return (
<div className="space-y-6">
<div className="grid gap-4 md:grid-cols-3 lg:grid-cols-6">
<Card className="bg-black/40 border-white/10">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-gray-400">Total</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-white">{stats.total}</div>
</CardContent>
</Card>
<Card className="bg-black/40 border-white/10">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-gray-400">Errors</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-red-400">{stats.errors}</div>
</CardContent>
</Card>
<Card className="bg-black/40 border-white/10">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-gray-400">Warnings</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-yellow-400">{stats.warnings}</div>
</CardContent>
</Card>
<Card className="bg-black/40 border-white/10">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-gray-400">Info</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-blue-400">{stats.info}</div>
</CardContent>
</Card>
<Card className="bg-black/40 border-white/10">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-gray-400">Resolved</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-green-400">{stats.resolved}</div>
</CardContent>
</Card>
<Card className="bg-black/40 border-white/10">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-gray-400">Unresolved</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-orange-400">{stats.unresolved}</div>
</CardContent>
</Card>
</div>
<ErrorLogStats stats={stats} />
<Card className="bg-black/40 border-white/10 text-white">
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>System Error Logs</CardTitle>
<CardDescription className="text-gray-400">
{scopeDescription}
</CardDescription>
</div>
<div className="flex items-center gap-2">
<Button onClick={loadLogs} disabled={loading} size="sm" variant="outline" className="border-white/20 text-white hover:bg-white/10">
{loading ? 'Loading...' : 'Refresh'}
</Button>
{isSuperGod && (
<>
<Button
onClick={() => {
setClearOnlyResolved(false)
setShowClearDialog(true)
}}
size="sm"
variant="outline"
className="border-red-500/50 text-red-400 hover:bg-red-500/20"
>
<Broom className="w-4 h-4 mr-2" />
Clear All
</Button>
<Button
onClick={() => {
setClearOnlyResolved(true)
setShowClearDialog(true)
}}
size="sm"
variant="outline"
className="border-green-500/50 text-green-400 hover:bg-green-500/20"
>
<Broom className="w-4 h-4 mr-2" />
Clear Resolved
</Button>
</>
)}
</div>
</div>
<div className="flex gap-2 mt-4">
<Select value={filterLevel} onValueChange={setFilterLevel}>
<SelectTrigger className="w-[180px] bg-white/5 border-white/10 text-white">
<SelectValue placeholder="Filter by level" />
</SelectTrigger>
<SelectContent className="bg-slate-900 border-white/10 text-white">
<SelectItem value="all">All Levels</SelectItem>
<SelectItem value="error">Errors</SelectItem>
<SelectItem value="warning">Warnings</SelectItem>
<SelectItem value="info">Info</SelectItem>
</SelectContent>
</Select>
<Select value={filterResolved} onValueChange={setFilterResolved}>
<SelectTrigger className="w-[180px] bg-white/5 border-white/10 text-white">
<SelectValue placeholder="Filter by status" />
</SelectTrigger>
<SelectContent className="bg-slate-900 border-white/10 text-white">
<SelectItem value="all">All Status</SelectItem>
<SelectItem value="resolved">Resolved</SelectItem>
<SelectItem value="unresolved">Unresolved</SelectItem>
</SelectContent>
</Select>
</div>
<ErrorLogControls
filterLevel={filters.level}
filterResolution={filters.resolution}
setFilterLevel={setFilterLevel}
setFilterResolution={setFilterResolution}
onRefresh={reload}
loading={loading}
user={user}
onRequestClear={openClearDialog}
/>
</CardHeader>
<CardContent>
<ScrollArea className="h-[600px] pr-4">
<div className="space-y-3">
{filteredLogs.length === 0 && !loading && (
<div className="py-12 text-center text-gray-400">
No error logs found
</div>
)}
{filteredLogs.map((log) => (
<Card
key={log.id}
className={`bg-white/5 border-white/10 ${log.resolved ? 'opacity-60' : ''}`}
>
<CardContent className="p-4">
<div className="flex items-start justify-between gap-4">
<div className="flex-1 space-y-2">
<div className="flex items-center gap-2">
<div className={`p-2 rounded ${getLevelColor(log.level)}`}>
{getLevelIcon(log.level)}
</div>
<Badge variant="outline" className={getLevelColor(log.level)}>
{log.level.toUpperCase()}
</Badge>
{log.resolved && (
<Badge variant="outline" className="bg-green-500/20 text-green-400 border-green-500/50">
<CheckCircle className="w-3 h-3 mr-1" />
Resolved
</Badge>
)}
<span className="text-xs text-gray-400">
{new Date(log.timestamp).toLocaleString()}
</span>
{isSuperGod && log.tenantId && (
<Badge variant="outline" className="bg-purple-500/20 text-purple-400 border-purple-500/50">
Tenant: {log.tenantId}
</Badge>
)}
</div>
<div>
<p className="text-white font-medium">{log.message}</p>
{log.source && (
<p className="text-xs text-gray-400 mt-1">
Source: {log.source}
</p>
)}
{log.username && (
<p className="text-xs text-gray-400 mt-1">
User: {log.username} {log.userId && `(${log.userId})`}
</p>
)}
</div>
{log.stack && (
<details className="text-xs text-gray-400 bg-black/40 p-2 rounded">
<summary className="cursor-pointer hover:text-white">
Stack trace
</summary>
<pre className="mt-2 overflow-x-auto whitespace-pre-wrap">
{log.stack}
</pre>
</details>
)}
{log.context && (
<details className="text-xs text-gray-400 bg-black/40 p-2 rounded">
<summary className="cursor-pointer hover:text-white">
Context
</summary>
<pre className="mt-2 overflow-x-auto whitespace-pre-wrap">
{JSON.stringify(JSON.parse(log.context), null, 2)}
</pre>
</details>
)}
{log.resolved && log.resolvedAt && (
<p className="text-xs text-green-400">
Resolved on {new Date(log.resolvedAt).toLocaleString()}
{log.resolvedBy && ` by ${log.resolvedBy}`}
</p>
)}
</div>
<div className="flex flex-col gap-2">
{!log.resolved && (
<Button
onClick={() => handleMarkResolved(log.id)}
size="sm"
variant="outline"
className="border-green-500/50 text-green-400 hover:bg-green-500/20"
>
<CheckCircle className="w-4 h-4 mr-2" />
Resolve
</Button>
)}
{isSuperGod && (
<Button
onClick={() => handleDeleteLog(log.id)}
size="sm"
variant="outline"
className="border-red-500/50 text-red-400 hover:bg-red-500/20"
>
<Trash className="w-4 h-4 mr-2" />
Delete
</Button>
)}
</div>
</div>
</CardContent>
</Card>
))}
</div>
</ScrollArea>
<ErrorLogList
logs={filteredLogs}
loading={loading}
onResolve={handleMarkResolved}
onDelete={handleDeleteLog}
user={user}
/>
</CardContent>
</Card>
{isSuperGod && (
<AlertDialog open={showClearDialog} onOpenChange={setShowClearDialog}>
<AlertDialogContent className="bg-slate-900 border-white/10 text-white">
<AlertDialogHeader>
<AlertDialogTitle className="flex items-center gap-2 text-amber-300">
<Warning className="w-6 h-6" weight="fill" />
Confirm Clear Error Logs
</AlertDialogTitle>
<AlertDialogDescription className="text-gray-400">
{clearOnlyResolved
? 'This will permanently delete all resolved error logs. This action cannot be undone.'
: 'This will permanently delete ALL error logs. This action cannot be undone.'}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel className="border-white/20 text-white hover:bg-white/10">
Cancel
</AlertDialogCancel>
<AlertDialogAction
onClick={handleClearLogs}
className="bg-gradient-to-r from-red-600 to-orange-600 hover:from-red-700 hover:to-orange-700"
>
Clear Logs
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<ClearLogsDialog
open={showClearDialog}
onOpenChange={setShowClearDialog}
clearOnlyResolved={clearOnlyResolved}
onConfirm={handleClearLogs}
/>
)}
</div>
)
}
const [logs, setLogs] = useState<ErrorLog[]>([])
const [loading, setLoading] = useState(false)
const [filterLevel, setFilterLevel] = useState<string>('all')
const [filterResolved, setFilterResolved] = useState<string>('all')
const [showClearDialog, setShowClearDialog] = useState(false)
const [clearOnlyResolved, setClearOnlyResolved] = useState(false)
const [stats, setStats] = useState({
total: 0,
errors: 0,
warnings: 0,
info: 0,
resolved: 0,
unresolved: 0,
})
useEffect(() => {
loadLogs()
}, [])
const loadLogs = async () => {
setLoading(true)
try {
const data = await Database.getErrorLogs()
setLogs(data)
calculateStats(data)
} catch (error) {
toast.error('Failed to load error logs')
console.error('Error loading logs:', error)
} finally {
setLoading(false)
}
}
const calculateStats = (logs: ErrorLog[]) => {
setStats({
total: logs.length,
errors: logs.filter(l => l.level === 'error').length,
warnings: logs.filter(l => l.level === 'warning').length,
info: logs.filter(l => l.level === 'info').length,
resolved: logs.filter(l => l.resolved).length,
unresolved: logs.filter(l => !l.resolved).length,
})
}
const handleMarkResolved = async (id: string) => {
try {
await Database.updateErrorLog(id, {
resolved: true,
resolvedAt: Date.now(),
resolvedBy: 'supergod',
})
await loadLogs()
toast.success('Error log marked as resolved')
} catch (error) {
toast.error('Failed to update error log')
}
}
const handleDeleteLog = async (id: string) => {
try {
await Database.deleteErrorLog(id)
await loadLogs()
toast.success('Error log deleted')
} catch (error) {
toast.error('Failed to delete error log')
}
}
const handleClearLogs = async () => {
try {
const count = await Database.clearErrorLogs(clearOnlyResolved)
await loadLogs()
toast.success(`Cleared ${count} error log${count !== 1 ? 's' : ''}`)
setShowClearDialog(false)
} catch (error) {
toast.error('Failed to clear error logs')
}
}
const getLevelIcon = (level: string) => {
switch (level) {
case 'error':
return <Warning className="w-5 h-5" weight="fill" />
case 'warning':
return <Warning className="w-5 h-5" />
case 'info':
return <Info className="w-5 h-5" />
default:
return <Info className="w-5 h-5" />
}
}
const getLevelColor = (level: string) => {
switch (level) {
case 'error':
return 'bg-red-500/20 text-red-400 border-red-500/50'
case 'warning':
return 'bg-yellow-500/20 text-yellow-400 border-yellow-500/50'
case 'info':
return 'bg-blue-500/20 text-blue-400 border-blue-500/50'
default:
return 'bg-gray-500/20 text-gray-400 border-gray-500/50'
}
}
const filteredLogs = logs.filter(log => {
if (filterLevel !== 'all' && log.level !== filterLevel) return false
if (filterResolved === 'resolved' && !log.resolved) return false
if (filterResolved === 'unresolved' && log.resolved) return false
return true
})
return (
<div className="space-y-6">
<div className="grid gap-4 md:grid-cols-3 lg:grid-cols-6">
<Card className="bg-black/40 border-white/10">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-gray-400">Total</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-white">{stats.total}</div>
</CardContent>
</Card>
<Card className="bg-black/40 border-white/10">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-gray-400">Errors</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-red-400">{stats.errors}</div>
</CardContent>
</Card>
<Card className="bg-black/40 border-white/10">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-gray-400">Warnings</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-yellow-400">{stats.warnings}</div>
</CardContent>
</Card>
<Card className="bg-black/40 border-white/10">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-gray-400">Info</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-blue-400">{stats.info}</div>
</CardContent>
</Card>
<Card className="bg-black/40 border-white/10">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-gray-400">Resolved</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-green-400">{stats.resolved}</div>
</CardContent>
</Card>
<Card className="bg-black/40 border-white/10">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-gray-400">Unresolved</CardTitle>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold text-orange-400">{stats.unresolved}</div>
</CardContent>
</Card>
</div>
<Card className="bg-black/40 border-white/10 text-white">
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>System Error Logs</CardTitle>
<CardDescription className="text-gray-400">
Track and manage system errors, warnings, and info messages
</CardDescription>
</div>
<div className="flex items-center gap-2">
<Button onClick={loadLogs} disabled={loading} size="sm" variant="outline" className="border-white/20 text-white hover:bg-white/10">
{loading ? 'Loading...' : 'Refresh'}
</Button>
<Button
onClick={() => {
setClearOnlyResolved(false)
setShowClearDialog(true)
}}
size="sm"
variant="outline"
className="border-red-500/50 text-red-400 hover:bg-red-500/20"
>
<Broom className="w-4 h-4 mr-2" />
Clear All
</Button>
<Button
onClick={() => {
setClearOnlyResolved(true)
setShowClearDialog(true)
}}
size="sm"
variant="outline"
className="border-green-500/50 text-green-400 hover:bg-green-500/20"
>
<Broom className="w-4 h-4 mr-2" />
Clear Resolved
</Button>
</div>
</div>
<div className="flex gap-2 mt-4">
<Select value={filterLevel} onValueChange={setFilterLevel}>
<SelectTrigger className="w-[180px] bg-white/5 border-white/10 text-white">
<SelectValue placeholder="Filter by level" />
</SelectTrigger>
<SelectContent className="bg-slate-900 border-white/10 text-white">
<SelectItem value="all">All Levels</SelectItem>
<SelectItem value="error">Errors</SelectItem>
<SelectItem value="warning">Warnings</SelectItem>
<SelectItem value="info">Info</SelectItem>
</SelectContent>
</Select>
<Select value={filterResolved} onValueChange={setFilterResolved}>
<SelectTrigger className="w-[180px] bg-white/5 border-white/10 text-white">
<SelectValue placeholder="Filter by status" />
</SelectTrigger>
<SelectContent className="bg-slate-900 border-white/10 text-white">
<SelectItem value="all">All Status</SelectItem>
<SelectItem value="resolved">Resolved</SelectItem>
<SelectItem value="unresolved">Unresolved</SelectItem>
</SelectContent>
</Select>
</div>
</CardHeader>
<CardContent>
<ScrollArea className="h-[600px] pr-4">
<div className="space-y-3">
{filteredLogs.length === 0 && !loading && (
<div className="py-12 text-center text-gray-400">
No error logs found
</div>
)}
{filteredLogs.map((log) => (
<Card
key={log.id}
className={`bg-white/5 border-white/10 ${log.resolved ? 'opacity-60' : ''}`}
>
<CardContent className="p-4">
<div className="flex items-start justify-between gap-4">
<div className="flex-1 space-y-2">
<div className="flex items-center gap-2">
<div className={`p-2 rounded ${getLevelColor(log.level)}`}>
{getLevelIcon(log.level)}
</div>
<Badge variant="outline" className={getLevelColor(log.level)}>
{log.level.toUpperCase()}
</Badge>
{log.resolved && (
<Badge variant="outline" className="bg-green-500/20 text-green-400 border-green-500/50">
<CheckCircle className="w-3 h-3 mr-1" />
Resolved
</Badge>
)}
<span className="text-xs text-gray-400">
{new Date(log.timestamp).toLocaleString()}
</span>
</div>
<div>
<p className="text-white font-medium">{log.message}</p>
{log.source && (
<p className="text-xs text-gray-400 mt-1">
Source: {log.source}
</p>
)}
{log.username && (
<p className="text-xs text-gray-400 mt-1">
User: {log.username} {log.userId && `(${log.userId})`}
</p>
)}
</div>
{log.stack && (
<details className="text-xs text-gray-400 bg-black/40 p-2 rounded">
<summary className="cursor-pointer hover:text-white">
Stack trace
</summary>
<pre className="mt-2 overflow-x-auto whitespace-pre-wrap">
{log.stack}
</pre>
</details>
)}
{log.context && (
<details className="text-xs text-gray-400 bg-black/40 p-2 rounded">
<summary className="cursor-pointer hover:text-white">
Context
</summary>
<pre className="mt-2 overflow-x-auto whitespace-pre-wrap">
{JSON.stringify(JSON.parse(log.context), null, 2)}
</pre>
</details>
)}
{log.resolved && log.resolvedAt && (
<p className="text-xs text-green-400">
Resolved on {new Date(log.resolvedAt).toLocaleString()}
{log.resolvedBy && ` by ${log.resolvedBy}`}
</p>
)}
</div>
<div className="flex flex-col gap-2">
{!log.resolved && (
<Button
onClick={() => handleMarkResolved(log.id)}
size="sm"
variant="outline"
className="border-green-500/50 text-green-400 hover:bg-green-500/20"
>
<CheckCircle className="w-4 h-4 mr-2" />
Resolve
</Button>
)}
<Button
onClick={() => handleDeleteLog(log.id)}
size="sm"
variant="outline"
className="border-red-500/50 text-red-400 hover:bg-red-500/20"
>
<Trash className="w-4 h-4 mr-2" />
Delete
</Button>
</div>
</div>
</CardContent>
</Card>
))}
</div>
</ScrollArea>
</CardContent>
</Card>
<AlertDialog open={showClearDialog} onOpenChange={setShowClearDialog}>
<AlertDialogContent className="bg-slate-900 border-white/10 text-white">
<AlertDialogHeader>
<AlertDialogTitle className="flex items-center gap-2 text-amber-300">
<Warning className="w-6 h-6" weight="fill" />
Confirm Clear Error Logs
</AlertDialogTitle>
<AlertDialogDescription className="text-gray-400">
{clearOnlyResolved
? 'This will permanently delete all resolved error logs. This action cannot be undone.'
: 'This will permanently delete ALL error logs. This action cannot be undone.'}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel className="border-white/20 text-white hover:bg-white/10">
Cancel
</AlertDialogCancel>
<AlertDialogAction
onClick={handleClearLogs}
className="bg-gradient-to-r from-red-600 to-orange-600 hover:from-red-700 hover:to-orange-700"
>
Clear Logs
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
)
}
@@ -1,6 +1,6 @@
import { describe, it, expect, vi, afterEach } from 'vitest'
import { render, screen, waitFor, fireEvent } from '@testing-library/react'
import { PowerTransferTab } from './PowerTransferTab'
import { PowerTransferTab } from '../PowerTransferTab'
import type { User } from '@/lib/level-types'
const superGodUser: User = {
@@ -0,0 +1,49 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from '@/components/ui'
import { Warning } from '@phosphor-icons/react'
interface ClearLogsDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
clearOnlyResolved: boolean
onConfirm: () => void
}
export function ClearLogsDialog({ open, onOpenChange, clearOnlyResolved, onConfirm }: ClearLogsDialogProps) {
return (
<AlertDialog open={open} onOpenChange={onOpenChange}>
<AlertDialogContent className="bg-slate-900 border-white/10 text-white">
<AlertDialogHeader>
<AlertDialogTitle className="flex items-center gap-2 text-amber-300">
<Warning className="w-6 h-6" weight="fill" />
Confirm Clear Error Logs
</AlertDialogTitle>
<AlertDialogDescription className="text-gray-400">
{clearOnlyResolved
? 'This will permanently delete all resolved error logs. This action cannot be undone.'
: 'This will permanently delete ALL error logs. This action cannot be undone.'}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel className="border-white/20 text-white hover:bg-white/10">
Cancel
</AlertDialogCancel>
<AlertDialogAction
onClick={onConfirm}
className="bg-gradient-to-r from-red-600 to-orange-600 hover:from-red-700 hover:to-orange-700"
>
Clear Logs
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}
@@ -0,0 +1,102 @@
import { Badge, Button, CardDescription, CardTitle, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui'
import type { ErrorLevelFilter, ResolutionFilter } from './useErrorLogFilters'
import type { User } from '@/lib/level-types'
import { Broom } from '@phosphor-icons/react'
interface ErrorLogControlsProps {
filterLevel: ErrorLevelFilter
filterResolution: ResolutionFilter
setFilterLevel: (value: ErrorLevelFilter) => void
setFilterResolution: (value: ResolutionFilter) => void
onRefresh: () => void
loading: boolean
user?: User
onRequestClear: (clearOnlyResolved: boolean) => void
}
export function ErrorLogControls({
filterLevel,
filterResolution,
setFilterLevel,
setFilterResolution,
onRefresh,
loading,
user,
onRequestClear,
}: ErrorLogControlsProps) {
const isSuperGod = user?.role === 'supergod'
const scopeDescription = isSuperGod
? 'All error logs across all tenants'
: 'Error logs for your tenant only'
return (
<div className="flex items-start justify-between">
<div>
<CardTitle>System Error Logs</CardTitle>
<CardDescription className="text-gray-400 flex items-center gap-2">
{scopeDescription}
{user?.tenantId && !isSuperGod && (
<Badge variant="outline" className="bg-purple-500/20 text-purple-400 border-purple-500/50">
Tenant: {user.tenantId}
</Badge>
)}
</CardDescription>
</div>
<div className="flex flex-col gap-2 items-end">
<div className="flex items-center gap-2">
<Button onClick={onRefresh} disabled={loading} size="sm" variant="outline" className="border-white/20 text-white hover:bg-white/10">
{loading ? 'Loading...' : 'Refresh'}
</Button>
{isSuperGod && (
<>
<Button
onClick={() => onRequestClear(false)}
size="sm"
variant="outline"
className="border-red-500/50 text-red-400 hover:bg-red-500/20"
>
<Broom className="w-4 h-4 mr-2" />
Clear All
</Button>
<Button
onClick={() => onRequestClear(true)}
size="sm"
variant="outline"
className="border-green-500/50 text-green-400 hover:bg-green-500/20"
>
<Broom className="w-4 h-4 mr-2" />
Clear Resolved
</Button>
</>
)}
</div>
<div className="flex gap-2">
<Select value={filterLevel} onValueChange={setFilterLevel}>
<SelectTrigger className="w-[180px] bg-white/5 border-white/10 text-white">
<SelectValue placeholder="Filter by level" />
</SelectTrigger>
<SelectContent className="bg-slate-900 border-white/10 text-white">
<SelectItem value="all">All Levels</SelectItem>
<SelectItem value="error">Errors</SelectItem>
<SelectItem value="warning">Warnings</SelectItem>
<SelectItem value="info">Info</SelectItem>
</SelectContent>
</Select>
<Select value={filterResolution} onValueChange={setFilterResolution}>
<SelectTrigger className="w-[180px] bg-white/5 border-white/10 text-white">
<SelectValue placeholder="Filter by status" />
</SelectTrigger>
<SelectContent className="bg-slate-900 border-white/10 text-white">
<SelectItem value="all">All Status</SelectItem>
<SelectItem value="resolved">Resolved</SelectItem>
<SelectItem value="unresolved">Unresolved</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
)
}
@@ -0,0 +1,149 @@
import { Badge, Button, Card, CardContent, ScrollArea } from '@/components/ui'
import { CheckCircle, Info, Trash, Warning } from '@phosphor-icons/react'
import type { ErrorLog } from '@/lib/db/error-logs'
import type { User } from '@/lib/level-types'
interface ErrorLogListProps {
logs: ErrorLog[]
onResolve: (id: string) => void
onDelete: (id: string) => void
loading: boolean
user?: User
}
const LEVEL_ICON = {
error: <Warning className="w-5 h-5" weight="fill" />,
warning: <Warning className="w-5 h-5" />,
info: <Info className="w-5 h-5" />,
default: <Info className="w-5 h-5" />,
}
const LEVEL_COLOR: Record<string, string> = {
error: 'bg-red-500/20 text-red-400 border-red-500/50',
warning: 'bg-yellow-500/20 text-yellow-400 border-yellow-500/50',
info: 'bg-blue-500/20 text-blue-400 border-blue-500/50',
default: 'bg-gray-500/20 text-gray-400 border-gray-500/50',
}
const getLevelColor = (level: string) => LEVEL_COLOR[level] ?? LEVEL_COLOR.default
const getLevelIcon = (level: string) => LEVEL_ICON[level as keyof typeof LEVEL_ICON] ?? LEVEL_ICON.default
export function ErrorLogList({ logs, onResolve, onDelete, loading, user }: ErrorLogListProps) {
const isSuperGod = user?.role === 'supergod'
return (
<ScrollArea className="h-[600px] pr-4">
<div className="space-y-3">
{logs.length === 0 && !loading && (
<div className="py-12 text-center text-gray-400">
No error logs found
</div>
)}
{logs.map((log) => (
<Card
key={log.id}
className={`bg-white/5 border-white/10 ${log.resolved ? 'opacity-60' : ''}`}
>
<CardContent className="p-4">
<div className="flex items-start justify-between gap-4">
<div className="flex-1 space-y-2">
<div className="flex items-center gap-2">
<div className={`p-2 rounded ${getLevelColor(log.level)}`}>
{getLevelIcon(log.level)}
</div>
<Badge variant="outline" className={getLevelColor(log.level)}>
{log.level.toUpperCase()}
</Badge>
{log.resolved && (
<Badge variant="outline" className="bg-green-500/20 text-green-400 border-green-500/50">
<CheckCircle className="w-3 h-3 mr-1" />
Resolved
</Badge>
)}
<span className="text-xs text-gray-400">
{new Date(log.timestamp).toLocaleString()}
</span>
{isSuperGod && log.tenantId && (
<Badge variant="outline" className="bg-purple-500/20 text-purple-400 border-purple-500/50">
Tenant: {log.tenantId}
</Badge>
)}
</div>
<div>
<p className="text-white font-medium">{log.message}</p>
{log.source && (
<p className="text-xs text-gray-400 mt-1">
Source: {log.source}
</p>
)}
{log.username && (
<p className="text-xs text-gray-400 mt-1">
User: {log.username} {log.userId && `(${log.userId})`}
</p>
)}
</div>
{log.stack && (
<details className="text-xs text-gray-400 bg-black/40 p-2 rounded">
<summary className="cursor-pointer hover:text-white">
Stack trace
</summary>
<pre className="mt-2 overflow-x-auto whitespace-pre-wrap">
{log.stack}
</pre>
</details>
)}
{log.context && (
<details className="text-xs text-gray-400 bg-black/40 p-2 rounded">
<summary className="cursor-pointer hover:text-white">
Context
</summary>
<pre className="mt-2 overflow-x-auto whitespace-pre-wrap">
{JSON.stringify(JSON.parse(log.context), null, 2)}
</pre>
</details>
)}
{log.resolved && log.resolvedAt && (
<p className="text-xs text-green-400">
Resolved on {new Date(log.resolvedAt).toLocaleString()}
{log.resolvedBy && ` by ${log.resolvedBy}`}
</p>
)}
</div>
<div className="flex flex-col gap-2">
{!log.resolved && (
<Button
onClick={() => onResolve(log.id)}
size="sm"
variant="outline"
className="border-green-500/50 text-green-400 hover:bg-green-500/20"
>
<CheckCircle className="w-4 h-4 mr-2" />
Resolve
</Button>
)}
{isSuperGod && (
<Button
onClick={() => onDelete(log.id)}
size="sm"
variant="outline"
className="border-red-500/50 text-red-400 hover:bg-red-500/20"
>
<Trash className="w-4 h-4 mr-2" />
Delete
</Button>
)}
</div>
</div>
</CardContent>
</Card>
))}
</div>
</ScrollArea>
)
}
@@ -0,0 +1,28 @@
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui'
import type { ErrorLogStats } from './useErrorLogs'
const STAT_CONFIG: Array<{ key: keyof ErrorLogStats; label: string; color: string }> = [
{ key: 'total', label: 'Total', color: 'text-white' },
{ key: 'errors', label: 'Errors', color: 'text-red-400' },
{ key: 'warnings', label: 'Warnings', color: 'text-yellow-400' },
{ key: 'info', label: 'Info', color: 'text-blue-400' },
{ key: 'resolved', label: 'Resolved', color: 'text-green-400' },
{ key: 'unresolved', label: 'Unresolved', color: 'text-orange-400' },
]
export function ErrorLogStats({ stats }: { stats: ErrorLogStats }) {
return (
<div className="grid gap-4 md:grid-cols-3 lg:grid-cols-6">
{STAT_CONFIG.map(({ key, label, color }) => (
<Card key={key} className="bg-black/40 border-white/10">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-gray-400">{label}</CardTitle>
</CardHeader>
<CardContent>
<div className={`text-2xl font-bold ${color}`}>{stats[key]}</div>
</CardContent>
</Card>
))}
</div>
)
}
@@ -0,0 +1,42 @@
import { Database } from '@/lib/database'
import type { User } from '@/lib/level-types'
import { toast } from 'sonner'
export async function markErrorResolved(id: string, reload: () => Promise<void>, user?: User) {
try {
await Database.updateErrorLog(id, {
resolved: true,
resolvedAt: Date.now(),
resolvedBy: user?.username || 'admin',
})
await reload()
toast.success('Error log marked as resolved')
} catch (error) {
toast.error('Failed to update error log')
}
}
export async function deleteErrorLog(id: string, reload: () => Promise<void>) {
try {
await Database.deleteErrorLog(id)
await reload()
toast.success('Error log deleted')
} catch (error) {
toast.error('Failed to delete error log')
}
}
export async function clearErrorLogs(
clearOnlyResolved: boolean,
reload: () => Promise<void>,
onCleared?: () => void
) {
try {
const count = await Database.clearErrorLogs(clearOnlyResolved)
await reload()
toast.success(`Cleared ${count} error log${count !== 1 ? 's' : ''}`)
onCleared?.()
} catch (error) {
toast.error('Failed to clear error logs')
}
}
@@ -0,0 +1,39 @@
import { useState } from 'react'
import type { ErrorLog } from '@/lib/db/error-logs'
export type ErrorLevelFilter = 'all' | 'error' | 'warning' | 'info'
export type ResolutionFilter = 'all' | 'resolved' | 'unresolved'
export interface ErrorLogFilters {
level: ErrorLevelFilter
resolution: ResolutionFilter
}
interface UseErrorLogFiltersReturn {
filters: ErrorLogFilters
setFilterLevel: (value: ErrorLevelFilter) => void
setFilterResolution: (value: ResolutionFilter) => void
}
export function useErrorLogFilters(): UseErrorLogFiltersReturn {
const [filterLevel, setFilterLevel] = useState<ErrorLevelFilter>('all')
const [filterResolved, setFilterResolved] = useState<ResolutionFilter>('all')
return {
filters: {
level: filterLevel,
resolution: filterResolved,
},
setFilterLevel,
setFilterResolution: setFilterResolved,
}
}
export function filterLogs(logs: ErrorLog[], filters: ErrorLogFilters): ErrorLog[] {
return logs.filter(log => {
if (filters.level !== 'all' && log.level !== filters.level) return false
if (filters.resolution === 'resolved' && !log.resolved) return false
if (filters.resolution === 'unresolved' && log.resolved) return false
return true
})
}
@@ -0,0 +1,76 @@
import { useCallback, useEffect, useState } from 'react'
import { Database } from '@/lib/database'
import type { ErrorLog } from '@/lib/db/error-logs'
import type { User } from '@/lib/level-types'
import { toast } from 'sonner'
export interface ErrorLogStats {
total: number
errors: number
warnings: number
info: number
resolved: number
unresolved: number
}
interface UseErrorLogsReturn {
logs: ErrorLog[]
loading: boolean
stats: ErrorLogStats
reload: () => Promise<void>
isSuperGod: boolean
}
export function useErrorLogs(user?: User): UseErrorLogsReturn {
const [logs, setLogs] = useState<ErrorLog[]>([])
const [loading, setLoading] = useState(false)
const [stats, setStats] = useState<ErrorLogStats>({
total: 0,
errors: 0,
warnings: 0,
info: 0,
resolved: 0,
unresolved: 0,
})
const isSuperGod = user?.role === 'supergod'
const tenantId = user?.tenantId
const calculateStats = useCallback((data: ErrorLog[]) => {
setStats({
total: data.length,
errors: data.filter(l => l.level === 'error').length,
warnings: data.filter(l => l.level === 'warning').length,
info: data.filter(l => l.level === 'info').length,
resolved: data.filter(l => l.resolved).length,
unresolved: data.filter(l => !l.resolved).length,
})
}, [])
const loadLogs = useCallback(async () => {
setLoading(true)
try {
const options = isSuperGod ? {} : { tenantId }
const data = await Database.getErrorLogs(options)
setLogs(data)
calculateStats(data)
} catch (error) {
toast.error('Failed to load error logs')
console.error('Error loading logs:', error)
} finally {
setLoading(false)
}
}, [calculateStats, isSuperGod, tenantId])
useEffect(() => {
loadLogs()
}, [loadLogs])
return {
logs,
loading,
stats,
reload: loadLogs,
isSuperGod,
}
}