Add event bus for live UI updates

Introduces a system for broadcasting real-time events to the web UI.
This enables features like automatic page reloads when case data changes
in the background, improving the user experience by keeping the UI
synchronized with the backend.

Key components:
- `events` module: Contains the `CaseEventKind` enum, `CaseEvent`
  struct, and `EventSender` type for managing the broadcast channel.
- SSE endpoint (`/web/events`): Streams events to connected browsers.
- Client-side JavaScript: Listens for events and triggers debounced page
  reloads.
- Integration points: Workers and route handlers now emit events when
  relevant state changes occur.
This commit is contained in:
2026-04-19 23:33:53 +02:00
parent 17aa5d7200
commit 1d702e2d85
20 changed files with 628 additions and 22 deletions
+10 -1
View File
@@ -3,6 +3,7 @@ pub mod auth;
pub mod case_id;
pub mod config;
pub mod error;
pub mod events;
pub mod gazetteer;
pub mod magic_link;
pub mod models;
@@ -68,7 +69,7 @@ pub struct PipelineState {
}
/// Shared application state. Clonable — all fields are cheap to clone
/// (`Arc` and `mpsc::Sender`).
/// (`Arc` and `mpsc::Sender` / `broadcast::Sender`).
#[derive(Clone)]
pub struct AppState {
pub config: Arc<Config>,
@@ -78,6 +79,7 @@ pub struct AppState {
pub magic_link_store: MagicLinkStore,
pub analyze_busy: AnalyzeBusy,
pub transcribe_busy: TranscribeBusy,
pub events_tx: events::EventSender,
}
impl FromRef<AppState> for Arc<Config> {
@@ -133,6 +135,12 @@ impl FromRef<AppState> for PipelineState {
}
}
impl FromRef<AppState> for events::EventSender {
fn from_ref(state: &AppState) -> Self {
state.events_tx.clone()
}
}
/// Test/simple entrypoint: jobs pushed into either channel are dropped
/// because the receivers are not retained. Use [`create_router_with_state`]
/// from `main.rs` where real workers own the receivers.
@@ -147,6 +155,7 @@ pub fn create_router(config: Arc<Config>) -> Router {
magic_link_store: magic_link::new_store(),
analyze_busy: AnalyzeBusy(Arc::new(AtomicBool::new(false))),
transcribe_busy: TranscribeBusy(Arc::new(AtomicBool::new(false))),
events_tx: events::channel(),
})
}