From 857e6f007d9274b18a65ebeda468b2a27e55f437 Mon Sep 17 00:00:00 2001 From: Tomas Dvorak Date: Sat, 25 Oct 2025 14:57:59 +0200 Subject: [PATCH] finally please --- .env | 3 + docker-compose.yml | 1 + internal/controllers/base_controller.go | 161 +++++++++++------------- internal/services/prefetch_service.go | 102 ++++++++++++++- 4 files changed, 175 insertions(+), 92 deletions(-) diff --git a/.env b/.env index d52bec4..b1141ba 100644 --- a/.env +++ b/.env @@ -95,3 +95,6 @@ UMAMI_URL=https://umami.tdvorak.dev UMAMI_USERNAME=admin UMAMI_PASSWORD=eevRQ6h3G@!c#y4A1T UMAMI_WEBSITE_ID= + + +PREFETCH_TARGET=http://127.0.0.1:8080/api/v1 \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 21e2fa0..28a3826 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -18,6 +18,7 @@ services: - PORT=8080 - RUN_MIGRATIONS=true - SEED_DATABASE=false + - PREFETCH_TARGET=http://localhost:8080/api/v1 depends_on: db: condition: service_healthy diff --git a/internal/controllers/base_controller.go b/internal/controllers/base_controller.go index 6b63153..91e9721 100644 --- a/internal/controllers/base_controller.go +++ b/internal/controllers/base_controller.go @@ -1913,78 +1913,69 @@ func (bc *BaseController) SetupInitialize(c *gin.Context) { } } // Trigger background prefetch and YouTube cache refresh when settings are updated post-setup - scheme := "http" - if c.Request.TLS != nil { - scheme = "https" - } - host := c.Request.Host - if host != "" { - baseURL := scheme + "://" + host + "/api/v1" - // Best-effort: immediately write public settings cache from current Settings - go func(snap models.Settings) { - defer func() { _ = recover() }() - snap.LoadCustomNav() - var pubVids []string - if snap.VideosJSON != "" { _ = json.Unmarshal([]byte(snap.VideosJSON), &pubVids) } - var pubVidsItems any - if snap.VideosItemsJSON != "" { _ = json.Unmarshal([]byte(snap.VideosItemsJSON), &pubVidsItems) } - var pubMerchItems any - if snap.MerchItemsJSON != "" { _ = json.Unmarshal([]byte(snap.MerchItemsJSON), &pubMerchItems) } - resp := map[string]any{ - "club_id": snap.ClubID, - "club_type": snap.ClubType, - "club_name": snap.ClubName, - "club_logo_url": snap.ClubLogoURL, - "club_url": snap.ClubURL, - "primary_color": snap.PrimaryColor, - "secondary_color": snap.SecondaryColor, - "accent_color": snap.AccentColor, - "background_color": snap.BackgroundColor, - "text_color": snap.TextColor, - "font_heading": snap.FontHeading, - "font_body": snap.FontBody, - "sponsors_layout": snap.SponsorsLayout, - "sponsors_theme": snap.SponsorsTheme, - "facebook_url": snap.FacebookURL, - "instagram_url": snap.InstagramURL, - "youtube_url": snap.YoutubeURL, - "gallery_url": snap.GalleryURL, - "gallery_label": snap.GalleryLabel, - "videos_module_enabled": snap.VideosModuleEnabled, - "videos_style": snap.VideosStyle, - "videos_source": snap.VideosSource, - "videos_limit": snap.VideosLimit, - "videos": pubVids, - "videos_items": pubVidsItems, - "merch_module_enabled": snap.MerchModuleEnabled, - "merch_style": snap.MerchStyle, - "merch_source": snap.MerchSource, - "merch_limit": snap.MerchLimit, - "merch_items": pubMerchItems, - "about_html": snap.AboutHTML, - "show_about_in_nav": snap.ShowAboutInNav, - "custom_nav": snap.CustomNav, - "contact_address": snap.ContactAddress, - "contact_city": snap.ContactCity, - "contact_zip": snap.ContactZip, - "contact_country": snap.ContactCountry, - "contact_phone": snap.ContactPhone, - "contact_email": snap.ContactEmail, - "location_latitude": snap.LocationLatitude, - "location_longitude": snap.LocationLongitude, - "map_zoom_level": snap.MapZoomLevel, - "map_style": snap.MapStyle, - "show_map_on_homepage": snap.ShowMapOnHomepage, - } - b, _ := json.MarshalIndent(resp, "", " ") - outPath := filepath.Join("cache", "prefetch", "settings.json") - _ = os.MkdirAll(filepath.Dir(outPath), 0o755) - tmp := outPath + ".tmp" - _ = os.WriteFile(tmp, b, 0o644) - _ = os.Rename(tmp, outPath) - }(s) - go services.PrefetchOnce(baseURL) - } + go func(snap models.Settings) { + defer func() { _ = recover() }() + snap.LoadCustomNav() + var pubVids []string + if snap.VideosJSON != "" { _ = json.Unmarshal([]byte(snap.VideosJSON), &pubVids) } + var pubVidsItems any + if snap.VideosItemsJSON != "" { _ = json.Unmarshal([]byte(snap.VideosItemsJSON), &pubVidsItems) } + var pubMerchItems any + if snap.MerchItemsJSON != "" { _ = json.Unmarshal([]byte(snap.MerchItemsJSON), &pubMerchItems) } + resp := map[string]any{ + "club_id": snap.ClubID, + "club_type": snap.ClubType, + "club_name": snap.ClubName, + "club_logo_url": snap.ClubLogoURL, + "club_url": snap.ClubURL, + "primary_color": snap.PrimaryColor, + "secondary_color": snap.SecondaryColor, + "accent_color": snap.AccentColor, + "background_color": snap.BackgroundColor, + "text_color": snap.TextColor, + "font_heading": snap.FontHeading, + "font_body": snap.FontBody, + "sponsors_layout": snap.SponsorsLayout, + "sponsors_theme": snap.SponsorsTheme, + "facebook_url": snap.FacebookURL, + "instagram_url": snap.InstagramURL, + "youtube_url": snap.YoutubeURL, + "gallery_url": snap.GalleryURL, + "gallery_label": snap.GalleryLabel, + "videos_module_enabled": snap.VideosModuleEnabled, + "videos_style": snap.VideosStyle, + "videos_source": snap.VideosSource, + "videos_limit": snap.VideosLimit, + "videos": pubVids, + "videos_items": pubVidsItems, + "merch_module_enabled": snap.MerchModuleEnabled, + "merch_style": snap.MerchStyle, + "merch_source": snap.MerchSource, + "merch_limit": snap.MerchLimit, + "merch_items": pubMerchItems, + "about_html": snap.AboutHTML, + "show_about_in_nav": snap.ShowAboutInNav, + "custom_nav": snap.CustomNav, + "contact_address": snap.ContactAddress, + "contact_city": snap.ContactCity, + "contact_zip": snap.ContactZip, + "contact_country": snap.ContactCountry, + "contact_phone": snap.ContactPhone, + "contact_email": snap.ContactEmail, + "location_latitude": snap.LocationLatitude, + "location_longitude": snap.LocationLongitude, + "map_zoom_level": snap.MapZoomLevel, + "map_style": snap.MapStyle, + "show_map_on_homepage": snap.ShowMapOnHomepage, + } + b, _ := json.MarshalIndent(resp, "", " ") + outPath := filepath.Join("cache", "prefetch", "settings.json") + _ = os.MkdirAll(filepath.Dir(outPath), 0o755) + tmp := outPath + ".tmp" + _ = os.WriteFile(tmp, b, 0o644) + _ = os.Rename(tmp, outPath) + }(s) + go services.PrefetchOnce(getPrefetchBaseURL()) if strings.TrimSpace(s.YoutubeURL) != "" { go func(u string) { _ = services.RefreshYouTubeChannelNow(u) }(s.YoutubeURL) } @@ -2245,7 +2236,7 @@ func (bc *BaseController) SetupInitialize(c *gin.Context) { logger.Info("Initial settings saved: club_id=%s club_name=%s gallery_url=%s gallery_label=%s", s.ClubID, s.ClubName, s.GalleryURL, s.GalleryLabel) // Immediately write public settings cache from current Settings snapshot - func() { + go func() { defer func() { _ = recover() }() s.LoadCustomNav() var pubVids []string @@ -2313,28 +2304,20 @@ func (bc *BaseController) SetupInitialize(c *gin.Context) { logger.Info("Default homepage page elements seeded") // Run all setup operations asynchronously in background to provide immediate response - scheme := "http" - if c.Request.TLS != nil { - scheme = "https" - } - host := c.Request.Host - logger.Info("Starting initial data prefetch and setup operations in background...") // Run all setup operations in a single background goroutine - go func(settingsID uint, youtubeURL, galleryURL, adminEmail, baseHost string) { + go func(settingsID uint, youtubeURL, galleryURL, adminEmail string) { defer func() { _ = recover() }() // 1. Trigger prefetch (matches, standings, etc.) - if baseHost != "" { - baseURL := scheme + "://" + baseHost + "/api/v1" - services.PrefetchOnce(baseURL) - logger.Info("Background prefetch completed") + baseURL := getPrefetchBaseURL() + services.PrefetchOnce(baseURL) + logger.Info("Background prefetch completed") - // Auto-populate competition aliases from FACR data - bc.autoPopulateCompetitionAliases() - logger.Info("Background competition aliases populated") - } + // Auto-populate competition aliases from FACR data + bc.autoPopulateCompetitionAliases() + logger.Info("Background competition aliases populated") // 2. If YouTube channel is configured, refresh its cache if strings.TrimSpace(youtubeURL) != "" { @@ -2368,7 +2351,7 @@ func (bc *BaseController) SetupInitialize(c *gin.Context) { } logger.Info("All background setup operations completed") - }(s.ID, s.YoutubeURL, s.GalleryURL, admin.Email, host) + }(s.ID, s.YoutubeURL, s.GalleryURL, admin.Email) logger.Info("SetupInitialize finished successfully - background operations running") c.JSON(http.StatusOK, gin.H{"message": "Setup completed successfully"}) diff --git a/internal/services/prefetch_service.go b/internal/services/prefetch_service.go index fbc6d51..7ed3425 100644 --- a/internal/services/prefetch_service.go +++ b/internal/services/prefetch_service.go @@ -542,9 +542,81 @@ func doPrefetchCycle(client *http.Client, baseURL string) { } } - // Alias: keep legacy/frontend callers happy expecting matches.json - if b, err := os.ReadFile(filepath.Join(cacheDir, "events_upcoming.json")); err == nil { - _ = os.WriteFile(filepath.Join(cacheDir, "matches.json"), b, 0o644) + // Prepare matches.json from FACR data if available; fallback to events_upcoming.json + createdMatches := false + buildMatchesFromFACR := func(data []byte) bool { + type facrMatch struct { + DateTime string `json:"date_time"` + Home string `json:"home"` + Away string `json:"away"` + Venue string `json:"venue"` + HomeLogoURL string `json:"home_logo_url"` + AwayLogoURL string `json:"away_logo_url"` + MatchID string `json:"match_id"` + } + var facr struct { + Competitions []struct { + Name string `json:"name"` + Code string `json:"code"` + Matches []facrMatch `json:"matches"` + } `json:"competitions"` + } + if err := json.Unmarshal(data, &facr); err != nil { + return false + } + // Collect upcoming matches (next 30 days) in simplified format + now := time.Now() + max := now.Add(30 * 24 * time.Hour) + var out []map[string]any + for _, c := range facr.Competitions { + compName := strings.TrimSpace(c.Name) + if compName == "" { + compName = strings.TrimSpace(c.Code) + } + for _, m := range c.Matches { + dt := strings.TrimSpace(m.DateTime) + if dt == "" { + continue + } + // dt like "12.08.2023 18:00" + parts := strings.SplitN(dt, " ", 2) + d := parts[0] + t := "" + if len(parts) > 1 { t = parts[1] } + // parse date dd.mm.yyyy + dd := strings.Split(d, ".") + if len(dd) < 3 { continue } + day := dd[0] + month := dd[1] + year := dd[2] + if len(month) == 1 { month = "0" + month } + if len(day) == 1 { day = "0" + day } + isoDate := year + "-" + month + "-" + day + if len(t) >= 5 { + t = t[:5] + } else { + t = "18:00" + } + // Build time.Time for filtering + ts, err := time.ParseInLocation("2006-01-02 15:04", isoDate+" "+t, time.Local) + if err != nil { continue } + if ts.Before(now) || ts.After(max) { continue } + out = append(out, map[string]any{ + "id": m.MatchID, + "home": m.Home, + "away": m.Away, + "competition": compName, + "date": isoDate, + "time": t, + "venue": m.Venue, + "home_logo_url": m.HomeLogoURL, + "away_logo_url": m.AwayLogoURL, + }) + } + } + if len(out) == 0 { return false } + _ = writeJSONAtomic(filepath.Join(cacheDir, "matches.json"), out) + return true } // 2) Dynamic FACR endpoints based on saved settings @@ -582,10 +654,34 @@ func doPrefetchCycle(client *http.Client, baseURL string) { statuses = append(statuses, epStatus{Path: path, File: file, Ok: true}) } } + // Try to build matches.json from freshly fetched FACR prefetch file + if b, err := os.ReadFile(filepath.Join(cacheDir, "facr_club_info.json")); err == nil { + if buildMatchesFromFACR(b) { createdMatches = true } + } } else { log.Printf("[prefetch] WARNING: FACR skipped: missing club_id=%q or club_type=%q in settings", clubID, clubType) } + // If FACR prefetch not available, try internal FACR cache as fallback + if !createdMatches && clubID != "" && clubType != "" { + facrCachePath := filepath.Join("cache", "facr", fmt.Sprintf("%s_%s_info.json", clubType, clubID)) + if b, err := os.ReadFile(facrCachePath); err == nil { + var cached struct{ Data []byte `json:"data"` } + if jsonErr := json.Unmarshal(b, &cached); jsonErr == nil && len(cached.Data) > 0 { + if buildMatchesFromFACR(cached.Data) { createdMatches = true } + } + } + } + + // Final fallback: copy events_upcoming.json or write empty list + if !createdMatches { + if b, err := os.ReadFile(filepath.Join(cacheDir, "events_upcoming.json")); err == nil { + _ = os.WriteFile(filepath.Join(cacheDir, "matches.json"), b, 0o644) + } else { + _ = os.WriteFile(filepath.Join(cacheDir, "matches.json"), []byte("[]"), 0o644) + } + } + // Meta timestamp _ = os.WriteFile(filepath.Join(cacheDir, "meta.json"), []byte(fmt.Sprintf(`{"lastUpdated":"%s"}`, time.Now().Format(time.RFC3339))), 0o644) // Detailed status for admin/debugging