mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 18:52:56 +00:00
upload
This commit is contained in:
@@ -0,0 +1,254 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user