diff --git a/surfsense_web/app/dashboard/[search_space_id]/documents/upload/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/documents/upload/page.tsx index 5f9f76f..e6f3ec9 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/documents/upload/page.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/documents/upload/page.tsx @@ -170,9 +170,9 @@ export default function FileUploader() { formData.append('search_space_id', search_space_id) try { - toast("File Upload", { - description: "Files Uploading Initiated", - }) + // toast("File Upload", { + // description: "Files Uploading Initiated", + // }) const response = await fetch(`${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL!}/api/v1/documents/fileupload`, { method: "POST", @@ -188,8 +188,8 @@ export default function FileUploader() { await response.json() - toast("Upload Successful", { - description: "Files Uploaded Successfully", + toast("Upload Task Initiated", { + description: "Files Uploading Initiated", }) router.push(`/dashboard/${search_space_id}/documents`); diff --git a/surfsense_web/app/dashboard/[search_space_id]/layout.tsx b/surfsense_web/app/dashboard/[search_space_id]/layout.tsx index 93e9f2d..ff077f5 100644 --- a/surfsense_web/app/dashboard/[search_space_id]/layout.tsx +++ b/surfsense_web/app/dashboard/[search_space_id]/layout.tsx @@ -43,10 +43,10 @@ export default function DashboardLayout({ title: "Upload Documents", url: `/dashboard/${search_space_id}/documents/upload`, }, - { - title: "Add Webpages", - url: `/dashboard/${search_space_id}/documents/webpage`, - }, + // { TODO: FIX THIS AND ADD IT BACK + // title: "Add Webpages", + // url: `/dashboard/${search_space_id}/documents/webpage`, + // }, { title: "Add Youtube Videos", url: `/dashboard/${search_space_id}/documents/youtube`, @@ -78,6 +78,13 @@ export default function DashboardLayout({ icon: "Podcast", items: [ ], + }, + { + title: "Logs", + url: `/dashboard/${search_space_id}/logs`, + icon: "FileText", + items: [ + ], } ] diff --git a/surfsense_web/app/dashboard/[search_space_id]/logs/(manage)/page.tsx b/surfsense_web/app/dashboard/[search_space_id]/logs/(manage)/page.tsx new file mode 100644 index 0000000..e43e03b --- /dev/null +++ b/surfsense_web/app/dashboard/[search_space_id]/logs/(manage)/page.tsx @@ -0,0 +1,1085 @@ +"use client"; + +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Checkbox } from "@/components/ui/checkbox"; +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Pagination, PaginationContent, PaginationItem } from "@/components/ui/pagination"; +import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { JsonMetadataViewer } from "@/components/json-metadata-viewer"; +import { useLogs, useLogsSummary, Log, LogLevel, LogStatus } from "@/hooks/use-logs"; +import { cn } from "@/lib/utils"; +import { + ColumnDef, + ColumnFiltersState, + PaginationState, + Row, + SortingState, + VisibilityState, + flexRender, + getCoreRowModel, + getFacetedUniqueValues, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from "@tanstack/react-table"; +import { AnimatePresence, motion } from "framer-motion"; +import { + Activity, + AlertCircle, + AlertTriangle, + Bug, + CheckCircle2, + ChevronDown, + ChevronFirst, + ChevronLast, + ChevronLeft, + ChevronRight, + ChevronUp, + CircleAlert, + CircleX, + Clock, + Columns3, + Filter, + Info, + ListFilter, + MoreHorizontal, + RefreshCw, + Terminal, + Trash, + X, + Zap, +} from "lucide-react"; +import { useParams } from "next/navigation"; +import React, { useContext, useEffect, useId, useMemo, useRef, useState } from "react"; +import { toast } from "sonner"; + +// Define animation variants for reuse +const fadeInScale = { + hidden: { opacity: 0, scale: 0.95 }, + visible: { + opacity: 1, + scale: 1, + transition: { type: "spring", stiffness: 300, damping: 30 } + }, + exit: { + opacity: 0, + scale: 0.95, + transition: { duration: 0.15 } + } +}; + +// Log level icons and colors +const logLevelConfig = { + DEBUG: { icon: Bug, color: "text-muted-foreground", bgColor: "bg-muted/50" }, + INFO: { icon: Info, color: "text-blue-600", bgColor: "bg-blue-50" }, + WARNING: { icon: AlertTriangle, color: "text-yellow-600", bgColor: "bg-yellow-50" }, + ERROR: { icon: AlertCircle, color: "text-red-600", bgColor: "bg-red-50" }, + CRITICAL: { icon: Zap, color: "text-purple-600", bgColor: "bg-purple-50" }, +} as const; + +// Log status icons and colors +const logStatusConfig = { + IN_PROGRESS: { icon: Clock, color: "text-blue-600", bgColor: "bg-blue-50" }, + SUCCESS: { icon: CheckCircle2, color: "text-green-600", bgColor: "bg-green-50" }, + FAILED: { icon: X, color: "text-red-600", bgColor: "bg-red-50" }, +} as const; + +const columns: ColumnDef[] = [ + { + id: "select", + header: ({ table }) => ( + table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + /> + ), + cell: ({ row }) => ( + row.toggleSelected(!!value)} + aria-label="Select row" + /> + ), + size: 28, + enableSorting: false, + enableHiding: false, + }, + { + header: "Level", + accessorKey: "level", + cell: ({ row }) => { + const level = row.getValue("level") as LogLevel; + const config = logLevelConfig[level]; + const Icon = config.icon; + return ( + +
+ +
+ + {level} + +
+ ); + }, + size: 120, + }, + { + header: "Status", + accessorKey: "status", + cell: ({ row }) => { + const status = row.getValue("status") as LogStatus; + const config = logStatusConfig[status]; + const Icon = config.icon; + return ( + +
+ +
+ + {status.replace('_', ' ')} + +
+ ); + }, + size: 140, + }, + { + header: "Source", + accessorKey: "source", + cell: ({ row }) => { + const source = row.getValue("source") as string; + return ( + + + {source || "System"} + + ); + }, + size: 150, + }, + { + header: "Message", + accessorKey: "message", + cell: ({ row }) => { + const message = row.getValue("message") as string; + const taskName = row.original.log_metadata?.task_name; + + return ( +
+ {taskName && ( +
+ {taskName} +
+ )} +
+ {message.length > 100 ? `${message.substring(0, 100)}...` : message} +
+
+ ); + }, + size: 400, + }, + { + header: "Created At", + accessorKey: "created_at", + cell: ({ row }) => { + const date = new Date(row.getValue("created_at")); + return ( +
+
{date.toLocaleDateString()}
+
{date.toLocaleTimeString()}
+
+ ); + }, + size: 120, + }, + { + id: "actions", + header: () => Actions, + cell: ({ row }) => , + size: 60, + enableHiding: false, + }, +]; + +// Create a context to share functions +const LogsContext = React.createContext<{ + deleteLog: (id: number) => Promise; + refreshLogs: () => Promise; +} | null>(null); + +export default function LogsManagePage() { + const id = useId(); + const params = useParams(); + const searchSpaceId = Number(params.search_space_id); + + const { logs, loading: logsLoading, error: logsError, refreshLogs, deleteLog } = useLogs(searchSpaceId); + const { summary, loading: summaryLoading, error: summaryError, refreshSummary } = useLogsSummary(searchSpaceId, 24); + + const [columnFilters, setColumnFilters] = useState([]); + const [columnVisibility, setColumnVisibility] = useState({}); + const [pagination, setPagination] = useState({ + pageIndex: 0, + pageSize: 20, + }); + const [sorting, setSorting] = useState([ + { + id: "created_at", + desc: true, + }, + ]); + + const inputRef = useRef(null); + + const table = useReactTable({ + data: logs, + columns, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + onSortingChange: setSorting, + enableSortingRemoval: false, + getPaginationRowModel: getPaginationRowModel(), + onPaginationChange: setPagination, + onColumnFiltersChange: setColumnFilters, + onColumnVisibilityChange: setColumnVisibility, + getFilteredRowModel: getFilteredRowModel(), + getFacetedUniqueValues: getFacetedUniqueValues(), + state: { + sorting, + pagination, + columnFilters, + columnVisibility, + }, + }); + + // Get unique values for filters + const uniqueLevels = useMemo(() => { + const levelColumn = table.getColumn("level"); + if (!levelColumn) return []; + return Array.from(levelColumn.getFacetedUniqueValues().keys()).sort(); + }, [table.getColumn("level")?.getFacetedUniqueValues()]); + + const uniqueStatuses = useMemo(() => { + const statusColumn = table.getColumn("status"); + if (!statusColumn) return []; + return Array.from(statusColumn.getFacetedUniqueValues().keys()).sort(); + }, [table.getColumn("status")?.getFacetedUniqueValues()]); + + const handleDeleteRows = async () => { + const selectedRows = table.getSelectedRowModel().rows; + + if (selectedRows.length === 0) { + toast.error("No rows selected"); + return; + } + + const deletePromises = selectedRows.map(row => deleteLog(row.original.id)); + + try { + const results = await Promise.all(deletePromises); + const allSuccessful = results.every(result => result === true); + + if (allSuccessful) { + toast.success(`Successfully deleted ${selectedRows.length} log(s)`); + } else { + toast.error("Some logs could not be deleted"); + } + + await refreshLogs(); + table.resetRowSelection(); + } catch (error: any) { + console.error("Error deleting logs:", error); + toast.error("Error deleting logs"); + } + }; + + const handleRefresh = async () => { + await Promise.all([refreshLogs(), refreshSummary()]); + toast.success("Logs refreshed"); + }; + + return ( + Promise.resolve(false)), + refreshLogs: refreshLogs || (() => Promise.resolve()) + }}> + + {/* Summary Dashboard */} + + + {/* Logs Table Header */} + +
+

Task Logs

+

+ Monitor and analyze all task execution logs +

+
+ +
+ + {/* Filters */} + + + {/* Delete Button */} + {table.getSelectedRowModel().rows.length > 0 && ( + + + + + + +
+
+ +
+ + Are you absolutely sure? + + This action cannot be undone. This will permanently delete{" "} + {table.getSelectedRowModel().rows.length} selected log(s). + + +
+ + Cancel + Delete + +
+
+
+ )} + + {/* Logs Table */} + +
+
+ ); +} + +// Summary Dashboard Component +function LogsSummaryDashboard({ + summary, + loading, + error, + onRefresh +}: { + summary: any; + loading: boolean; + error: string | null; + onRefresh: () => void; +}) { + if (loading) { + return ( + + {[...Array(4)].map((_, i) => ( + + +
+ + +
+ + + ))} + + ); + } + + if (error || !summary) { + return ( + + +
+ +

Failed to load summary

+ +
+
+
+ ); + } + + return ( + + {/* Total Logs */} + + + + Total Logs + + + +
{summary.total_logs}
+

+ Last {summary.time_window_hours} hours +

+
+
+
+ + {/* Active Tasks */} + + + + Active Tasks + + + +
+ {summary.active_tasks?.length || 0} +
+

+ Currently running +

+
+
+
+ + {/* Success Rate */} + + + + Success Rate + + + +
+ {summary.total_logs > 0 + ? Math.round(((summary.by_status?.SUCCESS || 0) / summary.total_logs) * 100) + : 0 + }% +
+

+ {summary.by_status?.SUCCESS || 0} successful +

+
+
+
+ + {/* Recent Failures */} + + + + Recent Failures + + + +
+ {summary.recent_failures?.length || 0} +
+

+ Need attention +

+
+
+
+
+ ); +} + +// Filters Component +function LogsFilters({ + table, + uniqueLevels, + uniqueStatuses, + inputRef, + id +}: { + table: any; + uniqueLevels: string[]; + uniqueStatuses: string[]; + inputRef: React.RefObject; + id: string; +}) { + return ( + +
+ {/* Search Input */} + + table.getColumn("message")?.setFilterValue(e.target.value)} + placeholder="Filter by message..." + type="text" + /> +
+ +
+ {Boolean(table.getColumn("message")?.getFilterValue()) && ( + + )} +
+ + {/* Level Filter */} + + + {/* Status Filter */} + + + {/* Column Visibility */} + + + + + + Toggle columns + {table + .getAllColumns() + .filter((column: any) => column.getCanHide()) + .map((column: any) => ( + column.toggleVisibility(!!value)} + onSelect={(event) => event.preventDefault()} + > + {column.id} + + ))} + + +
+
+ ); +} + +// Filter Dropdown Component +function FilterDropdown({ + title, + column, + options, + id +}: { + title: string; + column: any; + options: string[]; + id: string; +}) { + const selectedValues = useMemo(() => { + const filterValue = column?.getFilterValue() as string[]; + return filterValue ?? []; + }, [column?.getFilterValue()]); + + const handleValueChange = (checked: boolean, value: string) => { + const filterValue = column?.getFilterValue() as string[]; + const newFilterValue = filterValue ? [...filterValue] : []; + + if (checked) { + newFilterValue.push(value); + } else { + const index = newFilterValue.indexOf(value); + if (index > -1) { + newFilterValue.splice(index, 1); + } + } + + column?.setFilterValue(newFilterValue.length ? newFilterValue : undefined); + }; + + return ( + + + + + +
+
Filter by {title}
+
+ {options.map((value, i) => ( +
+ handleValueChange(checked, value)} + /> + +
+ ))} +
+
+
+
+ ); +} + +// Logs Table Component +function LogsTable({ + table, + logs, + loading, + error, + onRefresh, + id +}: { + table: any; + logs: Log[]; + loading: boolean; + error: string | null; + onRefresh: () => void; + id: string; +}) { + if (loading) { + return ( + +
+
+
+

Loading logs...

+
+
+
+ ); + } + + if (error) { + return ( + +
+
+ +

Error loading logs

+ +
+
+
+ ); + } + + if (logs.length === 0) { + return ( + +
+
+ +

No logs found

+
+
+
+ ); + } + + return ( + <> + + + + {table.getHeaderGroups().map((headerGroup: any) => ( + + {headerGroup.headers.map((header: any) => ( + + {header.isPlaceholder ? null : header.column.getCanSort() ? ( +
+ {flexRender(header.column.columnDef.header, header.getContext())} + {{ + asc: , + desc: , + }[header.column.getIsSorted() as string] ?? null} +
+ ) : ( + flexRender(header.column.columnDef.header, header.getContext()) + )} +
+ ))} +
+ ))} +
+ + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row: any, index: number) => ( + + {row.getVisibleCells().map((cell: any) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + + No logs found. + + + )} + + +
+
+ + {/* Pagination */} + + + ); +} + +// Pagination Component +function LogsPagination({ table, id }: { table: any; id: string }) { + return ( +
+ + + + + + +

+ + {table.getState().pagination.pageIndex * table.getState().pagination.pageSize + 1}- + {Math.min( + table.getState().pagination.pageIndex * table.getState().pagination.pageSize + + table.getState().pagination.pageSize, + table.getRowCount(), + )} + {" "} + of {table.getRowCount()} +

+
+ +
+ + + + + + + + + + + + + + + + +
+
+ ); +} + +// Row Actions Component +function LogRowActions({ row }: { row: Row }) { + const [isOpen, setIsOpen] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); + const { deleteLog, refreshLogs } = useContext(LogsContext)!; + const log = row.original; + + const handleDelete = async () => { + setIsDeleting(true); + try { + await deleteLog(log.id); + toast.success("Log deleted successfully"); + await refreshLogs(); + } catch (error) { + console.error("Error deleting log:", error); + toast.error("Failed to delete log"); + } finally { + setIsDeleting(false); + setIsOpen(false); + } + }; + + return ( +
+ + + + + + e.preventDefault()}> + View Metadata + + } + /> + + + + { + e.preventDefault(); + setIsOpen(true); + }} + > + Delete + + + + + Are you sure? + + This action cannot be undone. This will permanently delete the log entry. + + + + Cancel + + {isDeleting ? "Deleting..." : "Delete"} + + + + + + +
+ ); +} \ No newline at end of file diff --git a/surfsense_web/components/sidebar/app-sidebar.tsx b/surfsense_web/components/sidebar/app-sidebar.tsx index 62f8cf6..cd437f3 100644 --- a/surfsense_web/components/sidebar/app-sidebar.tsx +++ b/surfsense_web/components/sidebar/app-sidebar.tsx @@ -16,6 +16,7 @@ import { Trash2, Podcast, type LucideIcon, + FileText, } from "lucide-react" import { Logo } from "@/components/Logo"; @@ -47,7 +48,8 @@ export const iconMap: Record = { Info, ExternalLink, Trash2, - Podcast + Podcast, + FileText } const defaultData = { diff --git a/surfsense_web/hooks/index.ts b/surfsense_web/hooks/index.ts index e12a868..9ed3909 100644 --- a/surfsense_web/hooks/index.ts +++ b/surfsense_web/hooks/index.ts @@ -1 +1,2 @@ -export * from './useSearchSourceConnectors'; \ No newline at end of file +export * from './useSearchSourceConnectors'; +export * from './use-logs'; \ No newline at end of file diff --git a/surfsense_web/hooks/use-logs.ts b/surfsense_web/hooks/use-logs.ts new file mode 100644 index 0000000..17bcc32 --- /dev/null +++ b/surfsense_web/hooks/use-logs.ts @@ -0,0 +1,313 @@ +"use client" +import { useState, useEffect, useCallback, useMemo } from 'react'; +import { toast } from 'sonner'; + +export type LogLevel = "DEBUG" | "INFO" | "WARNING" | "ERROR" | "CRITICAL"; +export type LogStatus = "IN_PROGRESS" | "SUCCESS" | "FAILED"; + +export interface Log { + id: number; + level: LogLevel; + status: LogStatus; + message: string; + source?: string; + log_metadata?: Record; + created_at: string; + search_space_id: number; +} + +export interface LogFilters { + search_space_id?: number; + level?: LogLevel; + status?: LogStatus; + source?: string; + start_date?: string; + end_date?: string; +} + +export interface LogSummary { + total_logs: number; + time_window_hours: number; + by_status: Record; + by_level: Record; + by_source: Record; + active_tasks: Array<{ + id: number; + task_name: string; + message: string; + started_at: string; + source?: string; + }>; + recent_failures: Array<{ + id: number; + task_name: string; + message: string; + failed_at: string; + source?: string; + error_details?: string; + }>; +} + +export function useLogs(searchSpaceId?: number, filters: LogFilters = {}) { + const [logs, setLogs] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // Memoize filters to prevent infinite re-renders + const memoizedFilters = useMemo(() => filters, [JSON.stringify(filters)]); + + const buildQueryParams = useCallback((customFilters: LogFilters = {}) => { + const params = new URLSearchParams(); + + const allFilters = { ...memoizedFilters, ...customFilters }; + + if (allFilters.search_space_id) { + params.append('search_space_id', allFilters.search_space_id.toString()); + } + if (allFilters.level) { + params.append('level', allFilters.level); + } + if (allFilters.status) { + params.append('status', allFilters.status); + } + if (allFilters.source) { + params.append('source', allFilters.source); + } + if (allFilters.start_date) { + params.append('start_date', allFilters.start_date); + } + if (allFilters.end_date) { + params.append('end_date', allFilters.end_date); + } + + return params.toString(); + }, [memoizedFilters]); + + const fetchLogs = useCallback(async (customFilters: LogFilters = {}, options: { skip?: number; limit?: number } = {}) => { + try { + setLoading(true); + + const params = new URLSearchParams(buildQueryParams(customFilters)); + if (options.skip !== undefined) params.append('skip', options.skip.toString()); + if (options.limit !== undefined) params.append('limit', options.limit.toString()); + + const response = await fetch( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/logs/?${params}`, + { + headers: { + Authorization: `Bearer ${localStorage.getItem('surfsense_bearer_token')}`, + }, + method: "GET", + } + ); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.detail || "Failed to fetch logs"); + } + + const data = await response.json(); + setLogs(data); + setError(null); + return data; + } catch (err: any) { + setError(err.message || 'Failed to fetch logs'); + console.error('Error fetching logs:', err); + throw err; + } finally { + setLoading(false); + } + }, [buildQueryParams]); + + // Initial fetch + useEffect(() => { + const initialFilters = searchSpaceId ? { ...memoizedFilters, search_space_id: searchSpaceId } : memoizedFilters; + fetchLogs(initialFilters); + }, [searchSpaceId, fetchLogs, memoizedFilters]); + + // Function to refresh the logs list + const refreshLogs = useCallback(async (customFilters: LogFilters = {}) => { + const finalFilters = searchSpaceId ? { ...customFilters, search_space_id: searchSpaceId } : customFilters; + return await fetchLogs(finalFilters); + }, [searchSpaceId, fetchLogs]); + + // Function to create a new log + const createLog = useCallback(async (logData: Omit) => { + try { + const response = await fetch( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/logs/`, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${localStorage.getItem('surfsense_bearer_token')}`, + }, + method: "POST", + body: JSON.stringify(logData), + } + ); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.detail || "Failed to create log"); + } + + const newLog = await response.json(); + setLogs(prevLogs => [newLog, ...prevLogs]); + toast.success("Log created successfully"); + return newLog; + } catch (err: any) { + toast.error(err.message || 'Failed to create log'); + console.error('Error creating log:', err); + throw err; + } + }, []); + + // Function to update a log + const updateLog = useCallback(async (logId: number, updateData: Partial>) => { + try { + const response = await fetch( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/logs/${logId}`, + { + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${localStorage.getItem('surfsense_bearer_token')}`, + }, + method: "PUT", + body: JSON.stringify(updateData), + } + ); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.detail || "Failed to update log"); + } + + const updatedLog = await response.json(); + setLogs(prevLogs => + prevLogs.map(log => log.id === logId ? updatedLog : log) + ); + toast.success("Log updated successfully"); + return updatedLog; + } catch (err: any) { + toast.error(err.message || 'Failed to update log'); + console.error('Error updating log:', err); + throw err; + } + }, []); + + // Function to delete a log + const deleteLog = useCallback(async (logId: number) => { + try { + const response = await fetch( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/logs/${logId}`, + { + headers: { + Authorization: `Bearer ${localStorage.getItem('surfsense_bearer_token')}`, + }, + method: "DELETE", + } + ); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.detail || "Failed to delete log"); + } + + setLogs(prevLogs => prevLogs.filter(log => log.id !== logId)); + toast.success("Log deleted successfully"); + return true; + } catch (err: any) { + toast.error(err.message || 'Failed to delete log'); + console.error('Error deleting log:', err); + return false; + } + }, []); + + // Function to get a single log + const getLog = useCallback(async (logId: number) => { + try { + const response = await fetch( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/logs/${logId}`, + { + headers: { + Authorization: `Bearer ${localStorage.getItem('surfsense_bearer_token')}`, + }, + method: "GET", + } + ); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.detail || "Failed to fetch log"); + } + + return await response.json(); + } catch (err: any) { + toast.error(err.message || 'Failed to fetch log'); + console.error('Error fetching log:', err); + throw err; + } + }, []); + + return { + logs, + loading, + error, + refreshLogs, + createLog, + updateLog, + deleteLog, + getLog, + fetchLogs + }; +} + +// Separate hook for log summary +export function useLogsSummary(searchSpaceId: number, hours: number = 24) { + const [summary, setSummary] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchSummary = useCallback(async () => { + if (!searchSpaceId) return; + + try { + setLoading(true); + const response = await fetch( + `${process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL}/api/v1/logs/search-space/${searchSpaceId}/summary?hours=${hours}`, + { + headers: { + Authorization: `Bearer ${localStorage.getItem('surfsense_bearer_token')}`, + }, + method: "GET", + } + ); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + throw new Error(errorData.detail || "Failed to fetch logs summary"); + } + + const data = await response.json(); + setSummary(data); + setError(null); + return data; + } catch (err: any) { + setError(err.message || 'Failed to fetch logs summary'); + console.error('Error fetching logs summary:', err); + throw err; + } finally { + setLoading(false); + } + }, [searchSpaceId, hours]); + + useEffect(() => { + fetchSummary(); + }, [fetchSummary]); + + const refreshSummary = useCallback(() => { + return fetchSummary(); + }, [fetchSummary]); + + return { summary, loading, error, refreshSummary }; +} \ No newline at end of file