Skip to content

feat: add Export GIF link to finished game pages - #145

Draft
byte-the-bot wants to merge 1 commit into
mainfrom
implement/BS-c23cac20c43944fe
Draft

feat: add Export GIF link to finished game pages#145
byte-the-bot wants to merge 1 commit into
mainfrom
implement/BS-c23cac20c43944fe

Conversation

@byte-the-bot

Copy link
Copy Markdown
Collaborator

DO NOT MERGE UNTIL

Production BASE_URL is set on Cloud Run. It is currently unset, so
AppConfig::from_env() falls back to http://localhost:3000 and this link would
render as a dead engine_url=http://localhost:3000/api.

This is a pre-existing prod bug, not one introduced here. The board replay iframe
already on every prod game page renders the same dead origin today, as does the Share
input. Evidence (2026-08-15), curl -s https://arena.battlesnake.com/games/25d659a4-75b3-4c7a-8712-77d8c69aee15:

engine=http://localhost:3000/api
value="http://localhost:3000/games/25d659a4-..."

So the Export GIF link would be exactly as broken as the replay viewer sitting directly
above it — no worse. Merging this before BASE_URL is fixed is a defensible call; the
draft status is a safety default, not a veto. Corey decides.

Fix (needs write access to the Cloud Run service — Byte's gcloud identity is
byte-readonly@battlesnake-production.iam.gserviceaccount.com, read-only):

gcloud run services update <service> --region <region> \
  --update-env-vars BASE_URL=https://arena.battlesnake.com

Use --update-env-vars, NOT --set-env-vars: the latter wipes every other variable
on the service (DATABASE_URL, GCP_LOGGING, ENGINE_DATABASE_URL, the Eyes IDs, ...).
This creates a new revision, i.e. a rolling restart of production arena.

After it lands: reload a finished game page and confirm the source now shows
https://arena.battlesnake.com in both the board engine= URL and the Export GIF
href, then mark this PR ready for review.

Tracked follow-up for the deploy job: BS-583fb9517dc14ef9 (make ci.yml pass
--update-env-vars BASE_URL=... so this stops being untracked manual state).

Exporter gate

Ran 2026-08-15 against game 00c0ed77-51f4-4663-81b4-6d6f2e0df1cd: status=200 content_type=image/gif bytes=233989 time=4.90s, x-cache-hit: miss (fresh render,
not a CDN replay), magic bytes GIF89a. (A first attempt against game
3bcc20ac-86bc-4f5d-bca5-ac79036ca2f9 came back x-cache-hit: hit with age: 31883
— inconclusive per protocol, retried with a different game.)

Whoever flips this PR out of draft must rerun the full gate — fresh game discovery
from a snake profile, header capture, x-cache-hit: miss required — as the last action
before merging, and paste the result as a PR comment. Reusing the game ID above is not
acceptable: within 7 days it is a guaranteed cache hit.

What this does

Adds an Export GIF link to the game page's action row (GET /games/{id}), rendered
only when the game's status is finished, linking to
https://exporter.battlesnake.com/games/{id}/gif?engine_url={BASE_URL}/api. The
exporter fetches frame history from the engine-compatible {BASE_URL}/api/games/{id}/frames
endpoint shipped in #127, so the /api suffix in the URL is load-bearing. Plain outbound
anchor (target="_blank" rel="noopener", bare .btn like its siblings) — no proxying,
no request-time health checks, no JavaScript; exporter downtime is the exporter's error
page. Renders for anonymous and authenticated viewers alike (not nested under the
auth branch). og:image/meta tags are out of scope by spec.

Deliberate limitations (decisions, not misses)

  • Archived/legacy games are finished with no local frames, so they get a link that
    renders nothing. Accepted: they have no game_battlesnakes rows, so every user-facing
    listing excludes them — reachable only by typing a UUID; the page already shows its
    degenerate "0 snakes" state, and Game/get_game_by_id don't carry archived_at.
    If arena ever surfaces archived games in a listing, gate the link on
    archived_at IS NULL at that time.
  • Failed games never get the link, even though they're terminal: gated strictly on
    == GameStatus::Finished, not an "is the game over?" heuristic.
  • Exporter output is CDN-cached per game path for 7 days; re-exports return identical
    bytes. Harmless — finished games are immutable.

Tests

  • Unit: export_gif_url exact-string test (full URL incl. /gif, ?engine_url=,
    trailing /api — a regression can't silently fall back to the exporter's default engine).
  • E2E presence: finished page, exact href/target/rel via getByRole + toHaveCount(1).
  • E2E absence: table-driven over waiting/running/failed, toHaveCount(0).
  • Full Rust suite (612 tests) and full Playwright suite green locally; the one local
    failure is the known Lima-VM-only game-list.spec.ts job-timing flake (fails on
    main too, passes on CI).

Links to exporter.battlesnake.com with engine_url={BASE_URL}/api so the
exporter pulls frames from the engine-compatible endpoint shipped in
PR #127. Rendered only for finished games, as the last action in the
theater row, for anonymous and authenticated viewers alike.
@byte-the-bot
byte-the-bot force-pushed the implement/BS-c23cac20c43944fe branch from 505c3be to 1e0d4ba Compare August 16, 2026 08:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant