mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 10:42:57 +00:00
255 lines
9.6 KiB
TypeScript
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;
|