Group B from PR #1735 round-1 review (must-fix #2).
Previously a failed async migration pinned the banner forever:
isSteadyState returned false as long as any migration was in 'failed'
status, with no path to clear. Operators lost trust in the banner;
real new failures got lost in the noise.
Fix:
- FAILED_AUTO_DISMISS_MS = 10 min from endedAt — past that window the
failed entry auto-clears from the banner. The failure is still
visible via /api/perf/async-migrations and /api/healthz; only the
banner stops blocking.
- Per-line × button: explicit user ack immediately removes the
failure from the banner.
- Fail closed: if endedAt is missing or unparseable, the failure
does NOT auto-dismiss (operator must see it).
- isSteadyState gets an optional nowMs param (defaults to Date.now)
for testability and to make the auto-dismiss math
re-render-deterministic.
CSS additions: .warmup-banner__item--failed coloring + .warmup-banner__dismiss
button styling using existing CSS variable patterns.
Tests added: test-warmup-banner-failed-dismiss-1735.js pins:
- within window: failure still blocks steady state + appears in messages
- past window: failure auto-clears from both
- explicit dismiss: immediate removal
- missing/malformed endedAt: fails closed (no auto-dismiss)