fix(backend): improve video resilience and add blog ordering validation

Implement fallback mechanisms for YouTube video data and add startup
validation for blog post ordering.

- Add `loadVideosJSON` to persist and reload video data from disk.
- Update `refreshVideos` to retain existing local videos if the YouTube
  API returns an empty response.
- Add `validateBlogOrdering` to perform a health check on blog post
  sequence during startup.
- Call video loading and blog validation in `main`.
This commit is contained in:
Tomas Dvorak
2026-05-11 15:30:24 +02:00
parent 76c447a395
commit 4c904a1546
+60
View File
@@ -330,6 +330,14 @@ func refreshVideos(ctx context.Context) error {
return fmt.Errorf("yt get: %w", err) return fmt.Errorf("yt get: %w", err)
} }
items := resp.Videos items := resp.Videos
if len(items) == 0 {
// API returned empty data; keep existing local videos as fallback
vc.mu.RLock()
existingCount := len(vc.data.Items)
vc.mu.RUnlock()
log.Printf("warn: yt api returned 0 videos; keeping %d existing videos", existingCount)
return nil
}
if len(items) > 5 { if len(items) > 5 {
items = items[:5] items = items[:5]
} }
@@ -397,6 +405,28 @@ func writeVideosJSON() error {
return nil return nil
} }
func loadVideosJSON() error {
path := videosPath()
b, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("read videos json: %w", err)
}
var payload struct {
FetchedAt time.Time `json:"fetched_at"`
Channel string `json:"channel"`
Items []YTVideo `json:"items"`
}
if err := json.Unmarshal(b, &payload); err != nil {
return fmt.Errorf("unmarshal videos json: %w", err)
}
vc.mu.Lock()
vc.data.FetchedAt = payload.FetchedAt
vc.data.Channel = payload.Channel
vc.data.Items = payload.Items
vc.mu.Unlock()
return nil
}
func videosPath() string { func videosPath() string {
if p := os.Getenv("VIDEOS_PATH"); p != "" { if p := os.Getenv("VIDEOS_PATH"); p != "" {
return p return p
@@ -689,6 +719,26 @@ func listLatestBlogs(siteRoot string, limit int) ([]BlogItem, error) {
return items, nil return items, nil
} }
// validateBlogOrdering performs a startup health check to catch ordering regressions early.
func validateBlogOrdering(siteRoot string) error {
items, err := listLatestBlogs(siteRoot, 12)
if err != nil {
return err
}
if len(items) < 2 {
return nil
}
for i := 0; i < len(items)-1; i++ {
ii, e1 := strconv.Atoi(items[i].ID)
jj, e2 := strconv.Atoi(items[i+1].ID)
if e1 == nil && e2 == nil && ii <= jj {
return fmt.Errorf("blog ordering suspect: %s (%d) should be newer than %s (%d)", items[i].ID, ii, items[i+1].ID, jj)
}
}
log.Printf("blog ordering validated: newest=%s total=%d", items[0].ID, len(items))
return nil
}
// extractTitle finds the first <h1>...</h1> and returns its inner text (very simple, best-effort) // extractTitle finds the first <h1>...</h1> and returns its inner text (very simple, best-effort)
func extractTitle(path string) string { func extractTitle(path string) string {
b, err := os.ReadFile(path) b, err := os.ReadFile(path)
@@ -824,11 +874,21 @@ func main() {
go scheduler(ctx) go scheduler(ctx)
go videosScheduler(ctx) go videosScheduler(ctx)
// Load previously persisted videos so we have a fallback if yt api fails
if err := loadVideosJSON(); err != nil {
log.Printf("no persisted videos to load: %v", err)
}
// Initial videos fetch on startup to warm cache // Initial videos fetch on startup to warm cache
if err := refreshVideos(ctx); err != nil { if err := refreshVideos(ctx); err != nil {
log.Printf("initial videos refresh error: %v", err) log.Printf("initial videos refresh error: %v", err)
} }
// Startup validation: warn if blog ordering looks wrong
if err := validateBlogOrdering(staticPath()); err != nil {
log.Printf("blog ordering validation: %v", err)
}
mux := http.NewServeMux() mux := http.NewServeMux()
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
okCORS(w) okCORS(w)