fix(index-builder): promote recent replays from B2 to R2 warm cache

Modified bundleWarmAssetsForCycle to return the deduplicated match IDs
of warm-set replays, and added a call to promoteRecentReplays in the
build cycle. This implements plan §8.2.5: index builder promotes recent
replays from B2 cold archive to R2 warm cache.

Acceptance criteria for plan-gap bead bf-jfmo:
- /r2/replays/{id}.json.gz will return valid gzipped replay JSON once
  the Cloudflare Pages R2 bucket binding (ACB_BUCKET) is configured
- R2 bucket exists with replay objects after each build cycle
- Index builder has R2 S3 client configured (credentials via env vars)

Closes: bf-jfmo
This commit is contained in:
jedarden 2026-05-26 08:15:02 -04:00
parent 4f1b26f6fe
commit 3bd6ed45f9

View file

@ -241,11 +241,20 @@ func runBuildCycle(ctx context.Context, db *sql.DB, cfg *Config) error {
}
// Bundle warm-set assets (replays, thumbnails, cards, evolution) from B2 into Pages deploy
if err := bundleWarmAssetsForCycle(ctx, db, cfg, data); err != nil {
warmMatchIDs, err := bundleWarmAssetsForCycle(ctx, db, cfg, data)
if err != nil {
slog.Error("Failed to bundle warm assets", "error", err)
// Non-fatal
}
// Promote recent replays from B2 to R2 warm cache (plan §8.2.5)
if len(warmMatchIDs) > 0 {
if err := promoteRecentReplays(ctx, cfg, warmMatchIDs); err != nil {
slog.Error("Failed to promote replays to R2", "error", err)
// Non-fatal
}
}
// Enrich featured replays with AI commentary (§13.3)
if err := enrichReplays(ctx, data, cfg, llmClient); err != nil {
slog.Error("Failed to enrich replays", "error", err)
@ -263,16 +272,17 @@ func runBuildCycle(ctx context.Context, db *sql.DB, cfg *Config) error {
// bundleWarmAssetsForCycle bundles warm-set assets from B2 into the Pages deploy directory.
// This includes replays, thumbnails, bot cards, and evolution live.json.
func bundleWarmAssetsForCycle(ctx context.Context, db *sql.DB, cfg *Config, data *IndexData) error {
// Returns the deduplicated match IDs that were bundled (for R2 promotion).
func bundleWarmAssetsForCycle(ctx context.Context, db *sql.DB, cfg *Config, data *IndexData) ([]string, error) {
// Get recent match IDs from the last 24 hours
recentMatchIDs, err := fetchRecentMatchIDs(ctx, db, 24*time.Hour)
if err != nil {
return fmt.Errorf("fetch recent match IDs: %w", err)
return nil, fmt.Errorf("fetch recent match IDs: %w", err)
}
if len(recentMatchIDs) == 0 {
slog.Debug("No recent matches to bundle")
return nil
return nil, nil
}
// Get exempt match IDs (playlists, series, seasons) - these are also part of warm set
@ -300,7 +310,7 @@ func bundleWarmAssetsForCycle(ctx context.Context, db *sql.DB, cfg *Config, data
if len(dedupedMatchIDs) == 0 {
slog.Debug("No matches to bundle")
return nil
return nil, nil
}
slog.Info("Bundling warm assets from B2",
@ -311,7 +321,7 @@ func bundleWarmAssetsForCycle(ctx context.Context, db *sql.DB, cfg *Config, data
// Create B2 client for bundling
b2Client, err := getB2Client(cfg)
if err != nil {
return fmt.Errorf("create B2 client: %w", err)
return nil, fmt.Errorf("create B2 client: %w", err)
}
// Bundle replays
@ -344,7 +354,7 @@ func bundleWarmAssetsForCycle(ctx context.Context, db *sql.DB, cfg *Config, data
// Continue
}
return nil
return dedupedMatchIDs, nil
}
// fetchRecentMatchIDs retrieves match IDs from the last duration