'use client'; import { Suspense, useCallback, useEffect, useState } from 'react'; import { useRouter, useSearchParams } from 'next/navigation'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { RefreshCw, ScrollText, Trash2 } from 'lucide-react'; import { useTranslations } from 'next-intl'; import { toast } from 'sonner'; import { LoadingStateWithBorder } from '@/components/layout/loading'; import { Button } from '@/components/ui/button'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { AccessLogService } from '@/lib/services/openflare'; import { AccessLogFilters } from './components/access-log-filters'; import { type AccessLogTab, type OverviewRangeHours, parseSortValue, type SearchDraft, } from './components/access-log-utils'; import { CleanupDialog } from './components/cleanup-dialog'; import { DetailTab } from './components/detail-tab'; import { buildIpSummaryTimeParams, defaultLocalRangeForHours, IpTab, type IpTabTimeMode, } from './components/ip-tab'; import { OverviewTab } from './components/overview-tab'; const emptyDraft: SearchDraft = { nodeId: '', remoteAddr: '', host: '', path: '', statusCode: '', since: '', until: '', }; function resolveTab(value: string | null): AccessLogTab { if (value === 'list') return 'list'; if (value === 'ips') return 'ips'; return 'overview'; } function AccessLogsPageContent() { const t = useTranslations('accessLogs'); const router = useRouter(); const searchParams = useSearchParams(); const queryClient = useQueryClient(); const tab = resolveTab(searchParams.get('tab')); const [draft, setDraft] = useState(emptyDraft); const [filters, setFilters] = useState(emptyDraft); const [pageSize, setPageSize] = useState(20); const [page, setPage] = useState(0); const [detailSort, setDetailSort] = useState('logged_at:desc'); const [ipSort, setIpSort] = useState('total_requests:desc'); const [ipPageSize, setIpPageSize] = useState(20); const [ipPage, setIpPage] = useState(0); const [ipHours, setIpHours] = useState(168); const [ipTimeMode, setIpTimeMode] = useState('preset'); const [ipCustomSince, setIpCustomSince] = useState(''); const [ipCustomUntil, setIpCustomUntil] = useState(''); const [overviewHours, setOverviewHours] = useState(24); const [overviewHosts, setOverviewHosts] = useState([]); const [cleanupOpen, setCleanupOpen] = useState(false); const detailSortState = parseSortValue(detailSort); const ipSortState = parseSortValue(ipSort); const ipTimeParams = buildIpSummaryTimeParams({ timeMode: ipTimeMode, hours: ipHours, customSince: ipCustomSince, customUntil: ipCustomUntil, }); const ipQueryEnabled = tab === 'ips' && ipTimeParams != null; const handleTabChange = (value: string) => { const next = resolveTab(value); router.replace( next === 'overview' ? '/access-logs' : `/access-logs?tab=${next}`, ); }; const overviewQuery = useQuery({ queryKey: [ 'openflare', 'access-logs', 'overview', overviewHours, overviewHosts, ], queryFn: () => AccessLogService.getOverview({ hours: overviewHours, hosts: overviewHosts.length > 0 ? overviewHosts : undefined, }), enabled: tab === 'overview', }); const listQuery = useQuery({ queryKey: [ 'openflare', 'access-logs', 'list', filters, page, pageSize, detailSort, ], queryFn: () => AccessLogService.list({ node_id: filters.nodeId || undefined, remote_addr: filters.remoteAddr || undefined, host: filters.host || undefined, path: filters.path || undefined, status_code: filters.statusCode ? Number.parseInt(filters.statusCode, 10) : undefined, since: filters.since || undefined, until: filters.until || undefined, p: page, page_size: pageSize, sort_by: detailSortState.sortBy, sort_order: detailSortState.sortOrder, }), enabled: tab === 'list', }); const ipQuery = useQuery({ queryKey: [ 'openflare', 'access-logs', 'ip-summary', ipTimeParams, ipPage, ipPageSize, ipSort, ], queryFn: () => AccessLogService.listIPSummaries({ ...(ipTimeParams as NonNullable), p: ipPage, page_size: ipPageSize, sort_by: ipSortState.sortBy, sort_order: ipSortState.sortOrder, }), enabled: ipQueryEnabled, }); const cleanupMutation = useMutation({ mutationFn: (retentionDays: number) => AccessLogService.cleanup({ retention_days: retentionDays }), onSuccess: async (result) => { toast.success(t('cleaned', { count: result.deleted_count })); setCleanupOpen(false); await queryClient.invalidateQueries({ queryKey: ['openflare', 'access-logs'], }); }, onError: (error) => { toast.error(error instanceof Error ? error.message : t('cleanupFailed')); }, }); const handleSearch = useCallback(() => { setFilters({ nodeId: draft.nodeId.trim(), remoteAddr: draft.remoteAddr.trim(), host: draft.host.trim(), path: draft.path.trim(), statusCode: draft.statusCode.trim(), since: draft.since.trim(), until: draft.until.trim(), }); setPage(0); }, [draft]); const handleReset = () => { setDraft(emptyDraft); setFilters(emptyDraft); setPage(0); }; useEffect(() => { setPage(0); }, [tab, pageSize]); useEffect(() => { setIpPage(0); }, [ tab, ipPageSize, ipHours, ipTimeMode, ipCustomSince, ipCustomUntil, ipSort, ]); const refreshActive = () => { if (tab === 'overview') void overviewQuery.refetch(); if (tab === 'list') void listQuery.refetch(); if (tab === 'ips') void ipQuery.refetch(); }; const isFetching = overviewQuery.isFetching || listQuery.isFetching || ipQuery.isFetching; return (

{t('title')}

{t('subtitle')}

{t('tabOverview')} {t('tabIps')} {t('tabList')} void overviewQuery.refetch()} /> { setIpTimeMode('preset'); setIpHours(next); }} onTimeModeChange={(mode) => { setIpTimeMode(mode); if ( mode === 'custom' && (!ipCustomSince.trim() || !ipCustomUntil.trim()) ) { const range = defaultLocalRangeForHours(ipHours); setIpCustomSince(range.since); setIpCustomUntil(range.until); } }} onApplyCustomRange={(since, until) => { setIpCustomSince(since); setIpCustomUntil(until); setIpTimeMode('custom'); setIpPage(0); }} onPageSizeChange={setIpPageSize} onSortChange={setIpSort} onRetry={() => void ipQuery.refetch()} onPrevPage={() => setIpPage((p) => Math.max(0, p - 1))} onNextPage={() => setIpPage((p) => p + 1)} isFetching={ipQuery.isFetching} />
void listQuery.refetch()} onPrevPage={() => setPage((p) => Math.max(0, p - 1))} onNextPage={() => setPage((p) => p + 1)} isFetching={listQuery.isFetching} />
cleanupMutation.mutate(days)} loading={cleanupMutation.isPending} />
); } export default function AccessLogsPage() { const t = useTranslations('accessLogs'); return ( } > ); }