Files
MyClub/frontend/src/pages/admin/PrefetchAdminPage.tsx
T
Tomáš Dvořák 12cba639b9 upload
2025-10-16 13:32:05 +02:00

255 lines
9.6 KiB
TypeScript

import React from 'react';
import {
Box,
Heading,
Text,
Stack,
Button,
Stat,
StatLabel,
StatNumber,
StatHelpText,
SimpleGrid,
useToast,
Card,
CardHeader,
CardBody,
Badge,
Divider,
Modal,
ModalOverlay,
ModalContent,
ModalHeader,
ModalCloseButton,
ModalBody,
ModalFooter,
Textarea,
HStack,
VStack,
} from '@chakra-ui/react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { getPrefetchStatus, triggerPrefetch, PrefetchStatus } from '../../services/admin/prefetch';
import AdminLayout from '../../layouts/AdminLayout';
import api, { API_URL } from '../../services/api';
const PrefetchAdminPage: React.FC = () => {
const toast = useToast();
const qc = useQueryClient();
// RAW cache viewer state
const [rawOpen, setRawOpen] = React.useState<boolean>(false);
const [rawSel, setRawSel] = React.useState<string>('');
const [rawLoading, setRawLoading] = React.useState<boolean>(false);
const [rawError, setRawError] = React.useState<string | null>(null);
const [rawText, setRawText] = React.useState<string>('');
// Load list of available cache files (admin)
const { data: rawList, isError: rawListError, error: rawListErrorMsg } = useQuery<{ files: Array<{ label: string; path: string; size_bytes?: number; mod_time?: string }>}>({
queryKey: ['admin', 'cache', 'list'],
queryFn: async () => {
const res = await api.get('/admin/cache/list');
return res.data;
},
staleTime: 30_000,
retry: 1,
});
const fetchRaw = async (path: string) => {
setRawLoading(true);
setRawError(null);
setRawText('');
try {
const res = await api.get(`/admin/cache/file?path=${encodeURIComponent(path)}`, {
transformResponse: [(data) => data], // Get raw text response
});
const txt = res.data;
try {
const obj = JSON.parse(txt);
setRawText(JSON.stringify(obj, null, 2));
} catch {
setRawText(txt);
}
} catch (e: any) {
setRawError(e?.message || 'Nelze načíst data');
} finally {
setRawLoading(false);
}
};
const { data: status, isLoading, isFetching } = useQuery<PrefetchStatus>({
queryKey: ['admin', 'prefetch', 'status'],
queryFn: getPrefetchStatus,
refetchInterval: 30_000, // keep page live
});
const trigger = useMutation({
mutationFn: triggerPrefetch,
onSuccess: async () => {
toast({ title: 'Prefetch spuštěn', status: 'success' });
await qc.invalidateQueries({ queryKey: ['admin', 'prefetch', 'status'] });
},
onError: (err: any) => {
toast({ title: 'Spuštění prefetch selhalo', description: String(err?.message || err), status: 'error' });
},
});
const last = status?.lastUpdated ? new Date(status.lastUpdated) : null;
const next = status?.nextApproximate ? new Date(status.nextApproximate) : null;
return (
<AdminLayout>
<Box>
<Heading size="lg" mb={4}>Prefetch & Cache</Heading>
<Text color="gray.600" mb={6}>
Na pozadí běží úloha, která pravidelně stahuje JSON snapshoty z veřejných API pro rychlejší načítání stránek. Zde uvidíte aktuální plán a můžete spustit ruční stažení.
</Text>
<SimpleGrid columns={{ base: 1, md: 3 }} spacing={4} mb={6}>
<Card>
<CardHeader><Text fontWeight="bold">Režim</Text></CardHeader>
<CardBody>
<Stat>
<StatLabel>Aktuální režim</StatLabel>
<StatNumber>
{status?.fastMode ? (
<Badge colorScheme="green">Rychlý (během zápasu)</Badge>
) : (
<Badge>Normální</Badge>
)}
</StatNumber>
<StatHelpText>V době konání zápasů se automaticky přepne do rychlého režimu.</StatHelpText>
</Stat>
</CardBody>
</Card>
<Card>
<CardHeader><Text fontWeight="bold">Poslední aktualizace</Text></CardHeader>
<CardBody>
<Stat>
<StatLabel>Poslední prefetch</StatLabel>
<StatNumber fontSize="lg">
{last ? last.toLocaleString() : 'Unknown'}
</StatNumber>
<StatHelpText>{isFetching ? 'Obnovuji…' : 'Aktuální'}</StatHelpText>
</Stat>
</CardBody>
</Card>
<Card>
<CardHeader><Text fontWeight="bold">Další spuštění</Text></CardHeader>
<CardBody>
<Stat>
<StatLabel>Přibližně</StatLabel>
<StatNumber fontSize="lg">
{next ? next.toLocaleString() : '—'}
</StatNumber>
<StatHelpText>
Interval: {status?.intervalMinutes ?? 30} min
</StatHelpText>
</Stat>
</CardBody>
</Card>
</SimpleGrid>
<Card>
<CardHeader>
<Stack direction={{ base: 'column', sm: 'row' }} align="center" justify="space-between">
<Text fontWeight="bold">Ovládání</Text>
<Stack direction="row" spacing={3}>
<Button
colorScheme="blue"
isLoading={trigger.isLoading}
onClick={() => trigger.mutate()}
>
Spustit stažení
</Button>
<Button variant="outline" onClick={() => qc.invalidateQueries({ queryKey: ['admin', 'prefetch', 'status'] })}>
Obnovit stav
</Button>
<Button
variant="outline"
onClick={() => {
setRawOpen(true);
const first = rawList?.files?.[0];
if (first) {
setRawSel(first.path);
fetchRaw(first.path);
} else if (rawListError) {
setRawError('Nelze načíst seznam souborů');
} else if (!rawList?.files || rawList.files.length === 0) {
setRawError('Žádné cache soubory nebyly nalezeny');
}
}}
isDisabled={rawListError && !rawList}
>
Zobrazit RAW data
</Button>
</Stack>
</Stack>
</CardHeader>
<Divider />
<CardBody>
<Text color="gray.600">
Ruční spuštění zahájí na pozadí obnovu všech veřejných endpointů a zdrojů FAČR. Nezablokuje uživatelské rozhraní.
</Text>
</CardBody>
</Card>
{/* RAW viewer modal */}
<Modal isOpen={rawOpen} onClose={() => setRawOpen(false)} size="6xl" scrollBehavior="inside">
<ModalOverlay />
<ModalContent>
<ModalHeader>RAW data (prefetch & cache)</ModalHeader>
<ModalCloseButton />
<ModalBody>
{rawListError ? (
<Box p={4} textAlign="center">
<Text color="red.500" mb={2}>Chyba při načítání seznamu souborů</Text>
<Text color="gray.500" fontSize="sm">{String(rawListErrorMsg)}</Text>
</Box>
) : !rawList?.files || rawList.files.length === 0 ? (
<Box p={4} textAlign="center">
<Text color="gray.500">Žádné cache soubory nebyly nalezeny</Text>
<Text fontSize="sm" color="gray.400" mt={2}>Zkuste spustit prefetch stažení nejprve</Text>
</Box>
) : (
<SimpleGrid columns={{ base: 1, md: 4 }} spacing={4}>
<VStack align="stretch" spacing={2} gridColumn={{ base: '1', md: 'span 1' }}>
{rawList.files.map((f) => (
<Button
key={f.path}
variant={rawSel === f.path ? 'solid' : 'outline'}
onClick={() => { setRawSel(f.path); fetchRaw(f.path); }}
justifyContent="flex-start"
size="sm"
>
<Text noOfLines={1} fontSize="xs" textAlign="left" w="full">
{f.label}
</Text>
</Button>
))}
</VStack>
<Box gridColumn={{ base: '1', md: 'span 3' }}>
<HStack justify="space-between" mb={2}>
<Text fontWeight="semibold" fontSize="sm" noOfLines={1}>{rawSel || 'Vyberte soubor'}</Text>
<HStack>
<Button size="sm" variant="ghost" onClick={() => fetchRaw(rawSel)} isLoading={rawLoading} isDisabled={!rawSel}>Obnovit</Button>
<Button size="sm" as="a" href={`${API_URL}/admin/cache/file?path=${encodeURIComponent(rawSel)}`} target="_blank" rel="noreferrer" isDisabled={!rawSel}>Otevřít v nové záložce</Button>
</HStack>
</HStack>
{rawError && <Box color="red.500" mb={2} p={2} bg="red.50" borderRadius="md">{rawError}</Box>}
<Textarea value={rawText} onChange={() => {}} readOnly fontFamily="mono" rows={24} fontSize="xs" />
</Box>
</SimpleGrid>
)}
</ModalBody>
<ModalFooter>
<Button onClick={() => setRawOpen(false)}>Zavřít</Button>
</ModalFooter>
</ModalContent>
</Modal>
</Box>
</AdminLayout>
);
};
export default PrefetchAdminPage;