Two bugs that could cause blank PDFs to re-trigger indefinitely:
1. webdriver.py: tiled fallback used `if img is None:` which let b""
(returned by combine_screenshot_tiles on an empty tile list) pass
through silently. Changed to `if not img:` to catch both cases.
2. screenshots.py: compute_and_cache had no else branch after
`if image: cache_payload.update(image)`. When get_screenshot returned
None or b"" without raising, status stayed at COMPUTING instead of
ERROR, causing is_computing_stale() to re-trigger the task
indefinitely after THUMBNAIL_ERROR_CACHE_TTL. Added
`else: cache_payload.error()` so failed screenshots always transition
to ERROR and use the controlled TTL back-off.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ECharts animations play after chart data loads (after the loading spinner
clears). The Playwright screenshot path was applying the global
SCREENSHOT_SELENIUM_ANIMATION_WAIT *before* waiting for spinners to clear,
meaning the animation buffer was consumed before data had even loaded.
Move the animation wait to after `wait_for_function` (the global spinner
check) in both non-tiled code paths — when SCREENSHOT_TILED_ENABLED is
False, and when it is True but the dashboard is below the tiling threshold.
The tiled path already handles this correctly per-tile inside
`take_tiled_screenshot`, which was fixed by PRs #39895 and #40694.
This brings the thumbnail API / Celery screenshot path to parity with the
report-email path on small-to-medium dashboards.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>