feat(analysis): worker daemon + real dispatcher + app wiring
The background analysis worker that drains analyze_page jobs: - analysis::daemon: a lean sibling of the crawler daemon — leases only KIND_ANALYZE_PAGE, skip-if-done-unless-force, lease heartbeat, panic + timeout isolation, ack done/failed, and a failed page_analysis row on terminal (dead-lettered) failure. AnalyzeDispatcher trait seam. - RealAnalyzeDispatcher: load page → storage.get → VisionClient.analyze → persist_analysis (skips a deleted page; caps image bytes). - app::build spawns the daemon (own plain reqwest client) when ANALYSIS_ENABLED; AppHandle/main shut it down alongside the crawler. - repo::page::find_by_id. Tests: dispatch+ack-done, skip-when-done, force re-dispatch, terminal failure writes failed row, panic isolation, ignores non-analyze jobs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -29,6 +29,19 @@ pub async fn create<'e, E: PgExecutor<'e>>(
|
||||
Ok(row)
|
||||
}
|
||||
|
||||
/// Fetch a single page by id. `None` when it doesn't exist (e.g. deleted
|
||||
/// between an analysis job's enqueue and its dispatch).
|
||||
pub async fn find_by_id(pool: &PgPool, id: Uuid) -> AppResult<Option<Page>> {
|
||||
let row = sqlx::query_as::<_, Page>(
|
||||
"SELECT id, chapter_id, page_number, storage_key, content_type \
|
||||
FROM pages WHERE id = $1",
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_optional(pool)
|
||||
.await?;
|
||||
Ok(row)
|
||||
}
|
||||
|
||||
pub async fn list_for_chapter(pool: &PgPool, chapter_id: Uuid) -> AppResult<Vec<Page>> {
|
||||
let rows = sqlx::query_as::<_, Page>(
|
||||
r#"
|
||||
|
||||
Reference in New Issue
Block a user