mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-04 02:32:57 +00:00
fix update
This commit is contained in:
@@ -214,7 +214,7 @@ const MobileMenu = ({ isOpen, onClose, isAdmin, isAuthenticated, menuBg, divider
|
|||||||
<Button as={RouterLink} to="/hraci" variant="ghost" justifyContent="flex-start">{t('nav.players')}</Button>
|
<Button as={RouterLink} to="/hraci" variant="ghost" justifyContent="flex-start">{t('nav.players')}</Button>
|
||||||
)}
|
)}
|
||||||
{hasTables && (
|
{hasTables && (
|
||||||
<Button as={RouterLink} to="/tabulky" variant="ghost" justifyContent="flex-start">{t('nav.tables')}</Button>
|
<Button as={RouterLink} to="/tabulky" variant="ghost" justifyContent="flex-start">{t('nav.table')}</Button>
|
||||||
)}
|
)}
|
||||||
{Array.isArray(settings?.custom_nav) && settings.custom_nav.length > 0 && settings.custom_nav.map((item: any, idx: number) => {
|
{Array.isArray(settings?.custom_nav) && settings.custom_nav.length > 0 && settings.custom_nav.map((item: any, idx: number) => {
|
||||||
const customLinkIsExternal = typeof item?.url === 'string' && /^https?:\/\//i.test(item.url);
|
const customLinkIsExternal = typeof item?.url === 'string' && /^https?:\/\//i.test(item.url);
|
||||||
@@ -595,7 +595,7 @@ const Navbar: React.FC<{ fullWidth?: boolean; variant?: string }> = ({ fullWidth
|
|||||||
baseLinks.push({ label: t('nav.players'), to: '/hraci' });
|
baseLinks.push({ label: t('nav.players'), to: '/hraci' });
|
||||||
}
|
}
|
||||||
if (navbarData.hasTables) {
|
if (navbarData.hasTables) {
|
||||||
baseLinks.push({ label: t('nav.tables'), to: '/tabulky' });
|
baseLinks.push({ label: t('nav.table'), to: '/tabulky' });
|
||||||
}
|
}
|
||||||
if (navbarData.hasArticles) {
|
if (navbarData.hasArticles) {
|
||||||
baseLinks.push(
|
baseLinks.push(
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ const SetupPage: React.FC = () => {
|
|||||||
const [selectedClubSearchLabel, setSelectedClubSearchLabel] = useState('');
|
const [selectedClubSearchLabel, setSelectedClubSearchLabel] = useState('');
|
||||||
const { data: publicSettings } = usePublicSettings();
|
const { data: publicSettings } = usePublicSettings();
|
||||||
const isManualClubDataMode = (publicSettings?.club_data_mode || '').toLowerCase() === 'manual';
|
const isManualClubDataMode = (publicSettings?.club_data_mode || '').toLowerCase() === 'manual';
|
||||||
const { searchClubs, searchResults, searchLoading, clearSearchResults } = useFacrApi();
|
const { searchClubs, searchResults, searchLoading, clearSearchResults, getClub, getClubTable } = useFacrApi();
|
||||||
const suppressNextClubSearchRef = useRef(false);
|
const suppressNextClubSearchRef = useRef(false);
|
||||||
|
|
||||||
const resolveLogoUrl = (u?: string | null) => {
|
const resolveLogoUrl = (u?: string | null) => {
|
||||||
@@ -138,6 +138,8 @@ const SetupPage: React.FC = () => {
|
|||||||
const [processingLogos, setProcessingLogos] = useState(false);
|
const [processingLogos, setProcessingLogos] = useState(false);
|
||||||
const [rembgTotal, setRembgTotal] = useState(0);
|
const [rembgTotal, setRembgTotal] = useState(0);
|
||||||
const [rembgDone, setRembgDone] = useState(0);
|
const [rembgDone, setRembgDone] = useState(0);
|
||||||
|
const [fetchingFacrData, setFetchingFacrData] = useState(false);
|
||||||
|
const [facrDataProgress, setFacrDataProgress] = useState('');
|
||||||
|
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -498,6 +500,29 @@ const SetupPage: React.FC = () => {
|
|||||||
});
|
});
|
||||||
} catch {}
|
} catch {}
|
||||||
toast({ title: 'Nastavení dokončeno', status: 'success', duration: 3000, isClosable: true });
|
toast({ title: 'Nastavení dokončeno', status: 'success', duration: 3000, isClosable: true });
|
||||||
|
|
||||||
|
// Prefetch FACR data (club info and tables) before proceeding
|
||||||
|
// This ensures data is cached and ready when user enters admin
|
||||||
|
if (clubId && clubType && !isManualClubDataMode) {
|
||||||
|
setFetchingFacrData(true);
|
||||||
|
setFacrDataProgress('Získáváme data z FAČR...');
|
||||||
|
try {
|
||||||
|
// Fetch club info with retries (backend handles retries)
|
||||||
|
setFacrDataProgress('Získáváme data z FAČR... (informace o klubu)');
|
||||||
|
await getClub(clubId, clubType as 'football' | 'futsal').catch(() => null);
|
||||||
|
|
||||||
|
// Fetch club tables
|
||||||
|
setFacrDataProgress('Získáváme data z FAČR... (tabulky a zápasy)');
|
||||||
|
await getClubTable(clubId, clubType as 'football' | 'futsal').catch(() => null);
|
||||||
|
|
||||||
|
setFacrDataProgress('Data z FAČR načtena');
|
||||||
|
} catch {
|
||||||
|
// Continue even if FACR fetch fails - user can retry later
|
||||||
|
} finally {
|
||||||
|
setFetchingFacrData(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Start background removal only if backend allows it; otherwise skip waiting UI
|
// Start background removal only if backend allows it; otherwise skip waiting UI
|
||||||
let allowRembg = false;
|
let allowRembg = false;
|
||||||
try {
|
try {
|
||||||
@@ -1324,6 +1349,16 @@ const SetupPage: React.FC = () => {
|
|||||||
</VStack>
|
</VStack>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
{fetchingFacrData && (
|
||||||
|
<Box position="fixed" top={0} left={0} right={0} bottom={0} bg="rgba(0,0,0,0.6)" zIndex={9999} display="flex" alignItems="center" justifyContent="center">
|
||||||
|
<VStack spacing={3} bg={bg} p={8} borderRadius="xl" boxShadow="xl">
|
||||||
|
<Spinner size="xl" />
|
||||||
|
<Heading size="md">Získáváme data z FAČR</Heading>
|
||||||
|
<Text>{facrDataProgress || 'Načítám data…'}</Text>
|
||||||
|
<Text fontSize="sm" color="gray.500">Prosím vyčkejte, může to chvíli trvat…</Text>
|
||||||
|
</VStack>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -33,6 +33,70 @@ var (
|
|||||||
cacheTTL = 30 * time.Minute
|
cacheTTL = 30 * time.Minute
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// facrRetryConfig defines retry behavior for FACR API calls
|
||||||
|
var facrRetryConfig = struct {
|
||||||
|
MaxAttempts int
|
||||||
|
BaseDelay time.Duration
|
||||||
|
MaxDelay time.Duration
|
||||||
|
}{
|
||||||
|
MaxAttempts: 10,
|
||||||
|
BaseDelay: 5 * time.Second,
|
||||||
|
MaxDelay: 60 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchWithRetry performs HTTP GET with retry logic and exponential backoff
|
||||||
|
func fetchWithRetry(url string, timeout time.Duration) (*http.Response, error) {
|
||||||
|
var lastErr error
|
||||||
|
client := &http.Client{Timeout: timeout}
|
||||||
|
|
||||||
|
for attempt := 1; attempt <= facrRetryConfig.MaxAttempts; attempt++ {
|
||||||
|
resp, err := client.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
lastErr = err
|
||||||
|
if attempt < facrRetryConfig.MaxAttempts {
|
||||||
|
multiplier := 1 << uint(attempt-1) // 2^(attempt-1)
|
||||||
|
delay := facrRetryConfig.BaseDelay * time.Duration(multiplier)
|
||||||
|
if delay > facrRetryConfig.MaxDelay {
|
||||||
|
delay = facrRetryConfig.MaxDelay
|
||||||
|
}
|
||||||
|
time.Sleep(delay)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if response indicates an error from the FACR API
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
body, _ := io.ReadAll(io.LimitReader(resp.Body, 2048))
|
||||||
|
resp.Body.Close()
|
||||||
|
|
||||||
|
// Check if body contains scraping error (temporary failure)
|
||||||
|
bodyStr := string(body)
|
||||||
|
if strings.Contains(bodyStr, "scraping failed") ||
|
||||||
|
strings.Contains(bodyStr, "Cloudflare") ||
|
||||||
|
strings.Contains(bodyStr, "failed to fetch") ||
|
||||||
|
resp.StatusCode >= 500 {
|
||||||
|
lastErr = fmt.Errorf("FACR API temporary error (attempt %d): %s", attempt, bodyStr)
|
||||||
|
if attempt < facrRetryConfig.MaxAttempts {
|
||||||
|
multiplier := 1 << uint(attempt-1) // 2^(attempt-1)
|
||||||
|
delay := facrRetryConfig.BaseDelay * time.Duration(multiplier)
|
||||||
|
if delay > facrRetryConfig.MaxDelay {
|
||||||
|
delay = facrRetryConfig.MaxDelay
|
||||||
|
}
|
||||||
|
time.Sleep(delay)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// For other errors, return immediately with a reconstructed response
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("FACR API failed after %d attempts: %v", facrRetryConfig.MaxAttempts, lastErr)
|
||||||
|
}
|
||||||
|
|
||||||
func cacheDir() string {
|
func cacheDir() string {
|
||||||
return filepath.Join("cache", "facr")
|
return filepath.Join("cache", "facr")
|
||||||
}
|
}
|
||||||
@@ -229,8 +293,7 @@ func (fc *FACRController) SearchClubs(c *gin.Context) {
|
|||||||
vals.Set("q", q)
|
vals.Set("q", q)
|
||||||
searchURL := "https://facr.tdvorak.dev/club/search?" + vals.Encode()
|
searchURL := "https://facr.tdvorak.dev/club/search?" + vals.Encode()
|
||||||
|
|
||||||
httpClient := &http.Client{Timeout: 60 * time.Second}
|
resp, err := fetchWithRetry(searchURL, 10*time.Minute)
|
||||||
resp, err := httpClient.Get(searchURL)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("Error fetching from FACR API: %v", err)})
|
c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("Error fetching from FACR API: %v", err)})
|
||||||
return
|
return
|
||||||
@@ -371,8 +434,7 @@ func (fc *FACRController) GetClubInfo(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
external := fmt.Sprintf("https://facr.tdvorak.dev/club/%s/%s", clubType, clubID)
|
external := fmt.Sprintf("https://facr.tdvorak.dev/club/%s/%s", clubType, clubID)
|
||||||
httpClient := &http.Client{Timeout: 60 * time.Second}
|
resp, err := fetchWithRetry(external, 10*time.Minute)
|
||||||
resp, err := httpClient.Get(external)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("proxy error: %v", err)})
|
c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("proxy error: %v", err)})
|
||||||
return
|
return
|
||||||
@@ -639,8 +701,7 @@ func (fc *FACRController) GetClubTables(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
external := fmt.Sprintf("https://facr.tdvorak.dev/club/%s/%s/table", clubType, clubID)
|
external := fmt.Sprintf("https://facr.tdvorak.dev/club/%s/%s/table", clubType, clubID)
|
||||||
httpClient := &http.Client{Timeout: 60 * time.Second}
|
resp, err := fetchWithRetry(external, 10*time.Minute)
|
||||||
resp, err := httpClient.Get(external)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("proxy error: %v", err)})
|
c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("proxy error: %v", err)})
|
||||||
return
|
return
|
||||||
|
|||||||
Reference in New Issue
Block a user