{{ message }}
Add Server-Sent Events for realtime dashboard updates#1191
Merged
Conversation
Replace 30-second polling with SSE push notifications on the scoreboard,
overview, and services pages. Events are published to Redis pub/sub when
rounds complete (via cache_helper) and streamed to browsers through a
lightweight gevent SSE server running alongside uWSGI.
Architecture:
- scoring_engine/events.py: Redis pub/sub publisher (publish_event())
- scoring_engine/sse_server.py: Gevent SSE server on port 8001
- /api/events/token: Short-lived Redis-backed auth tokens for SSE
- /api/events: SSE stream endpoint, filtered by user role/team
- Nginx routes /api/events to gevent, everything else to uWSGI
Frontend:
- sse.js: EventSource client with exponential backoff reconnection
and automatic fallback to 30s polling after 5 failed retries
- Scoreboard, overview, and services pages converted from setInterval
to ScoringEngineSSE.on('round_complete', ...) handlers
Infrastructure:
- gevent added to pyproject.toml
- Web Dockerfile starts both SSE server and uWSGI
- Nginx config adds SSE proxy with buffering disabled
SSE server: Switch from per-connection Redis subscriptions to a single persistent background greenlet that stays subscribed and broadcasts to all connected clients. The previous approach lost events when no client happened to be connected at the exact moment of publish. Engine: Skip services with no environments instead of crashing with IndexError. AgentCheck services from example data have no environments. Also convert announcement badge polling in base.html to SSE events.
Pages converted from setInterval to SSE: - /service/<id> — check history refreshes on round_complete - /injects — inject list refreshes on inject_update - /inject/<id> — detail, sidebar, comments, files refresh on inject_update - /admin/injects — grading table refreshes on inject_update - /admin/inject/<id> — comments/files refresh on inject_update - Announcement badge in base.html — refreshes on announcement/round_complete Event publishing added to mutation endpoints: - Admin: grade, request revision, reopen inject - Blue team: submit, resubmit, add comment Also: - Extract should_send_event() to events.py (testable without gevent) - Fix SSE client to let browser handle native EventSource reconnection - Skip services with no environments in engine (fixes crash on example data) - Add unit tests for publish_event and should_send_event visibility filtering
- Admin status page: competition summary and engine stats now refresh on round_complete instead of polling every 5s (progress bar stays at 3s) - Admin sidebar: engine pause/resume status refreshes on settings_changed - Publish settings_changed events from engine toggle and inject scores toggle
42bbf9d to
4b65070
Compare
ION28
reviewed
Apr 1, 2026
Collaborator
There was a problem hiding this comment.
this is nowhere close to the latest version of gevent: https://pypi.org/project/gevent/
Collaborator
There was a problem hiding this comment.
here's the socket report for the most recent gevent version: https://socket.dev/pypi/package/gevent/overview/25.9.1
Collaborator
There was a problem hiding this comment.
note that it's "contains native code or compiled binaries" is an FP: https://socket.dev/pypi/package/gevent/files/25.9.1/tar-gz/gevent-25.9.1/deps/libuv/test/fixtures/load_error.node
…E.md gevent was pinned to 24.11.1 but 25.9.1 is the latest stable release. Also add a Dependencies section to CLAUDE.md noting that new deps should always use the latest stable version with exact pinning.
- Token endpoint tests: anonymous, blue, white, red roles + uniqueness - Inject API tests: verify publish_event calls on submit, comment, grade - Cache helper test: verify update_all_cache publishes round_complete - 22 new SSE-related tests total
sse_server.py requires gevent runtime (monkey-patching at import time) and can't be unit tested in the standard pytest environment. Its logic is tested via the extracted should_send_event() in events.py and via integration tests. Add coverage omit to prevent false coverage drops.
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Summary
Replaces 30-second polling with Server-Sent Events (SSE) for near-instant dashboard updates when rounds complete. This is the biggest UX improvement for competitions with 200+ students watching the scoreboard.
Architecture:
round_completeevents to Redis pub/sub afterupdate_all_cache()/api/eventsto gevent withproxy_buffering off, everything else to uWSGIFrontend:
sse.js— EventSource client with exponential backoff reconnectionsetIntervalto SSE event handlersPages converted:
/scoreboard) — bar/line charts refresh onround_complete/overview) — status matrix + round data refresh onround_complete/services) — team stats + service table refresh onround_completeNo breaking changes — if the SSE server is unavailable, clients fall back to polling.
Test plan