Add Server-Sent Events for realtime dashboard updates by RustyBower · Pull Request #1191 · scoringengine/scoringengine · GitHub
Skip to content

Add Server-Sent Events for realtime dashboard updates#1191

Merged
RustyBower merged 7 commits into
masterfrom
feature/websocket-realtime
Apr 1, 2026
Merged

Add Server-Sent Events for realtime dashboard updates#1191
RustyBower merged 7 commits into
masterfrom
feature/websocket-realtime

Conversation

@RustyBower

Copy link
Copy Markdown
Collaborator

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:

  • Engine publishes round_complete events to Redis pub/sub after update_all_cache()
  • Lightweight gevent SSE server (port 8001) runs alongside uWSGI in the web container
  • Nginx routes /api/events to gevent with proxy_buffering off, everything else to uWSGI
  • Auth via short-lived Redis-backed tokens (no shared SECRET_KEY needed between processes)
  • Per-client event filtering by role/team visibility

Frontend:

  • sse.js — EventSource client with exponential backoff reconnection
  • Falls back to 30s polling automatically after 5 failed reconnects
  • Scoreboard, overview, and services pages converted from setInterval to SSE event handlers

Pages converted:

  • Scoreboard (/scoreboard) — bar/line charts refresh on round_complete
  • Overview (/overview) — status matrix + round data refresh on round_complete
  • Services (/services) — team stats + service table refresh on round_complete

No breaking changes — if the SSE server is unavailable, clients fall back to polling.

Test plan

  • SSE event delivery verified: Redis PUBLISH → gevent → nginx → curl
  • Open scoreboard in browser, verify EventSource connection in DevTools Network tab
  • Run engine with example data, verify dashboards update within 1-2s of round completion
  • Open scoreboard in two tabs — both update simultaneously
  • Kill SSE server process — client reconnects with exponential backoff, then falls back to 30s polling
  • Test as anonymous user — only public events received
  • Test as blue team — verify team-specific filtering works

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
@RustyBower RustyBower force-pushed the feature/websocket-realtime branch from 42bbf9d to 4b65070 Compare April 1, 2026 01:25
@socket-security

socket-security Bot commented Apr 1, 2026

Copy link
Copy Markdown

Comment thread pyproject.toml Outdated

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is nowhere close to the latest version of gevent: https://pypi.org/project/gevent/

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here's the socket report for the most recent gevent version: https://socket.dev/pypi/package/gevent/overview/25.9.1

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

…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.
@RustyBower RustyBower merged commit 1cfcaab into master Apr 1, 2026
9 checks passed
@RustyBower RustyBower mentioned this pull request Apr 1, 2026
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.

2 participants