diff --git a/cmd/acb-worker/b2.go b/cmd/acb-worker/b2.go
index 30a95a7..138761b 100644
--- a/cmd/acb-worker/b2.go
+++ b/cmd/acb-worker/b2.go
@@ -49,15 +49,19 @@ func NewB2Client(cfg *Config) *B2Client {
}
}
-// Upload uploads data to B2.
-func (c *B2Client) Upload(ctx context.Context, key string, data []byte, contentType string) error {
- _, err := c.client.PutObject(ctx, &s3.PutObjectInput{
+// Upload uploads data to B2. Pass contentEncoding="" for uncompressed objects.
+func (c *B2Client) Upload(ctx context.Context, key string, data []byte, contentType string, contentEncoding string) error {
+ input := &s3.PutObjectInput{
Bucket: aws.String(c.bucket),
Key: aws.String(key),
Body: bytes.NewReader(data),
ContentType: aws.String(contentType),
CacheControl: aws.String("public, max-age=31536000, immutable"),
- })
+ }
+ if contentEncoding != "" {
+ input.ContentEncoding = aws.String(contentEncoding)
+ }
+ _, err := c.client.PutObject(ctx, input)
return err
}
diff --git a/cmd/acb-worker/main.go b/cmd/acb-worker/main.go
index d965958..240ef6f 100644
--- a/cmd/acb-worker/main.go
+++ b/cmd/acb-worker/main.go
@@ -6,6 +6,8 @@
package main
import (
+ "bytes"
+ "compress/gzip"
"context"
"encoding/json"
"flag"
@@ -224,14 +226,14 @@ func (w *Worker) pollAndExecute(ctx context.Context) error {
replayURL := ""
if w.b2 != nil {
uploadStart := time.Now()
- replayData, _ := json.Marshal(replay)
replayURL, err = w.uploadReplay(ctx, claimData.Match.ID, replay)
if err != nil {
w.metrics.RecordReplayUploadError()
w.logger.Printf("Failed to upload replay: %v", err)
// Continue without replay URL - match result is more important
} else {
- w.metrics.RecordReplayUpload(time.Since(uploadStart), len(replayData))
+ replaySize, _ := json.Marshal(replay)
+ w.metrics.RecordReplayUpload(time.Since(uploadStart), len(replaySize))
w.logger.Printf("Uploaded replay to %s", replayURL)
}
}
@@ -318,10 +320,11 @@ func (w *Worker) executeMatch(ctx context.Context, claimData *JobClaimData) (*Ma
// Convert result
result := &MatchResult{
- WinnerID: "",
- Turns: engineResult.Turns,
- EndReason: engineResult.Reason,
- Scores: make(map[string]int),
+ WinnerID: "",
+ Turns: engineResult.Turns,
+ EndReason: engineResult.Reason,
+ Scores: make(map[string]int),
+ CrashedBots: make(map[string]bool),
}
// Set winner ID from result (Winner is int, -1 for draw)
@@ -341,6 +344,13 @@ func (w *Worker) executeMatch(ctx context.Context, claimData *JobClaimData) (*Ma
}
}
+ // Propagate crash status from engine
+ for _, p := range claimData.Participants {
+ if p.PlayerSlot < len(engineResult.Crashed) {
+ result.CrashedBots[p.BotID] = engineResult.Crashed[p.PlayerSlot]
+ }
+ }
+
return result, replay, nil
}
@@ -364,7 +374,7 @@ func (w *Worker) sendHeartbeats(ctx context.Context, jobID string) {
}
}
-// uploadReplay uploads the replay to B2 and returns the URL.
+// uploadReplay uploads the gzipped replay to B2 and returns the URL.
func (w *Worker) uploadReplay(ctx context.Context, matchID string, replay *engine.Replay) (string, error) {
if w.b2 == nil {
return "", fmt.Errorf("B2 client not configured")
@@ -376,9 +386,19 @@ func (w *Worker) uploadReplay(ctx context.Context, matchID string, replay *engin
return "", fmt.Errorf("failed to serialize replay: %w", err)
}
+ // Gzip compress
+ var buf bytes.Buffer
+ gw := gzip.NewWriter(&buf)
+ if _, err := gw.Write(data); err != nil {
+ return "", fmt.Errorf("failed to gzip replay: %w", err)
+ }
+ if err := gw.Close(); err != nil {
+ return "", fmt.Errorf("failed to close gzip writer: %w", err)
+ }
+
// Upload to B2
- key := fmt.Sprintf("replays/%s.json", matchID)
- if err := w.b2.Upload(ctx, key, data, "application/json"); err != nil {
+ key := fmt.Sprintf("replays/%s.json.gz", matchID)
+ if err := w.b2.Upload(ctx, key, buf.Bytes(), "application/json", "gzip"); err != nil {
return "", fmt.Errorf("failed to upload replay to B2: %w", err)
}
diff --git a/web/src/pages/bot-profile.ts b/web/src/pages/bot-profile.ts
index a05e11d..b492324 100644
--- a/web/src/pages/bot-profile.ts
+++ b/web/src/pages/bot-profile.ts
@@ -184,7 +184,7 @@ function renderMatchItem(match: BotProfile['recent_matches'][number]): string {
${won ? 'W' : 'L'}
${opponent ? escapeHtml(opponent.name) : 'Unknown'}
${match.participants.map(p => p.score).join(' - ')}
- Watch
+ Watch
`;
}
diff --git a/web/src/pages/matches.ts b/web/src/pages/matches.ts
index 1150ac6..dc792ea 100644
--- a/web/src/pages/matches.ts
+++ b/web/src/pages/matches.ts
@@ -311,7 +311,7 @@ function renderMatchCard(match: MatchSummary): string {
${match.turns ?? '-'} turns
${match.end_reason ?? '-'}
- Watch Replay
+ Watch Replay
`;
diff --git a/web/src/pages/playlists.ts b/web/src/pages/playlists.ts
index fc51a3d..f9aa3d2 100644
--- a/web/src/pages/playlists.ts
+++ b/web/src/pages/playlists.ts
@@ -531,7 +531,7 @@ function addMatchShowMore(container: HTMLElement, remaining: PlaylistMatch[]): v
}
function watchMatch(matchId: string): void {
- window.location.hash = `/watch/replay?url=/replays/${matchId}.json`;
+ window.location.hash = `/watch/replay?url=/replays/${matchId}.json.gz`;
}
function copyEmbedCode(matchId: string): void {