//! SSE endpoint streaming case events to the web UI. //! //! One long-lived connection per browser tab. Non-admins only see events //! for their own user_slug; admins see events across all users //! (useful for support/monitoring). A 15s keep-alive comment keeps idle //! connections alive through reverse proxies / NAT. use std::convert::Infallible; use std::time::Duration; use axum::extract::State; use axum::response::sse::{Event, KeepAlive, Sse}; use futures_util::stream::Stream; use tokio_stream::StreamExt; use tokio_stream::wrappers::BroadcastStream; use tokio_stream::wrappers::errors::BroadcastStreamRecvError; use crate::auth::AuthenticatedWebUser; use crate::events::EventSender; /// `GET /events` — `text/event-stream`, one connection per tab. pub async fn handle_events( user: AuthenticatedWebUser, State(events_tx): State, ) -> Sse>> { let rx = events_tx.subscribe(); let is_admin = user.is_admin(); let slug = user.slug.clone(); let stream = BroadcastStream::new(rx).filter_map(move |res| match res { Ok(evt) => { // Non-admins only see events for their own user_slug. if !is_admin && evt.user_slug != slug { return None; } // Serialization of `CaseEvent` is infallible in practice (plain // struct with String + enum fields); on the unexpected error // path we drop the event rather than tear down the stream. let json = serde_json::to_string(&evt).ok()?; Some(Ok(Event::default().event("case").data(json))) } // Lagged subscribers: skip silently. The next real event or the // user's next navigation will resync state from the filesystem. Err(BroadcastStreamRecvError::Lagged(n)) => { tracing::debug!(skipped = n, "sse subscriber lagged"); None } }); Sse::new(stream).keep_alive( KeepAlive::new() .interval(Duration::from_secs(15)) .text("keepalive"), ) }