feat(analysis): live SSE event stream for the admin dashboard
Broadcasts analysis progress so the dashboard updates live: - analysis::events: AnalysisEvents broadcaster + AnalysisEvent (Enqueued / Started / Completed / Failed), carrying the manga/chapter/page breadcrumb. - The worker daemon resolves each page's breadcrumb (repo::page::locate) and publishes Started before dispatch and Completed/Failed after. - The admin reenqueue publishes Enqueued (scoped by manga/chapter). - GET /v1/admin/analysis/status/stream — SSE (RequireAdmin) forwarding each event as a named `analysis` frame; broadcast lag emits a `lagged` frame. AppState carries the always-present events bus. Tests: worker publishes started+completed (with breadcrumb) and failed; SSE route is admin-gated (403 non-admin) and returns text/event-stream. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -61,6 +61,10 @@ pub struct AppState {
|
||||
/// Mirrors `config.analysis.enabled`; defaults to `false` so the
|
||||
/// feature is opt-in and the test harness inherits a no-op gate.
|
||||
pub analysis_enabled: bool,
|
||||
/// Live analysis-event broadcaster. Always present (the worker and the
|
||||
/// admin enqueue path publish here; the SSE endpoint subscribes). When
|
||||
/// the worker is disabled the channel simply stays quiet.
|
||||
pub analysis_events: Arc<crate::analysis::events::AnalysisEvents>,
|
||||
}
|
||||
|
||||
/// Shared handle the admin crawler endpoints use to observe and control
|
||||
@@ -137,6 +141,11 @@ pub async fn build(config: Config) -> anyhow::Result<AppHandle> {
|
||||
(None, None, None)
|
||||
};
|
||||
|
||||
// Live-event bus shared by the worker (progress) and the admin enqueue
|
||||
// path; the SSE endpoint subscribes. Created unconditionally so the
|
||||
// stream exists even with the worker disabled.
|
||||
let analysis_events = Arc::new(crate::analysis::events::AnalysisEvents::new());
|
||||
|
||||
// AI content-analysis worker. Independent of the crawler daemon (works
|
||||
// for uploads with the crawler off), gated on ANALYSIS_ENABLED. Uses a
|
||||
// plain reqwest client — no cookie jar / proxy, unlike the crawler's.
|
||||
@@ -160,6 +169,7 @@ pub async fn build(config: Config) -> anyhow::Result<AppHandle> {
|
||||
dispatcher,
|
||||
workers: config.analysis.workers,
|
||||
job_timeout: config.analysis.job_timeout,
|
||||
events: Arc::clone(&analysis_events),
|
||||
},
|
||||
);
|
||||
tracing::info!(
|
||||
@@ -183,6 +193,7 @@ pub async fn build(config: Config) -> anyhow::Result<AppHandle> {
|
||||
crawler,
|
||||
admin_allowed_origins: Arc::new(config.admin_allowed_origins.clone()),
|
||||
analysis_enabled: config.analysis.enabled,
|
||||
analysis_events,
|
||||
};
|
||||
let router = router(state).layer(cors_layer(&config.cors_allowed_origins));
|
||||
Ok(AppHandle { router, daemon, analysis_daemon })
|
||||
|
||||
Reference in New Issue
Block a user