use crate::db::models::{DashboardSummary, QueueStatusCount, ReviewQueueSummary}; use crate::db::{Db, db_error}; use crate::error::AppResult; pub async fn summary(db: &Db) -> AppResult { let client = db.client().await?; let row = client .query_one( "SELECT (SELECT count(*)::bigint FROM podcast_channels WHERE deleted_at IS NULL AND status IN ('selected', 'active')) AS active_channels, (SELECT count(*)::bigint FROM videos WHERE deleted_at IS NULL) AS videos, (SELECT count(*)::bigint FROM interviewees WHERE deleted_at IS NULL AND status = 'active') AS active_interviewees, (SELECT count(*)::bigint FROM contacts WHERE deleted_at IS NULL AND status = 'active') AS active_contacts, (SELECT count(*)::bigint FROM pipeline_runs WHERE status IN ('running', 'cancelling')) AS running_runs, (SELECT count(*)::bigint FROM jobs WHERE status IN ('queued', 'retry_scheduled')) AS queued_jobs, (SELECT count(*)::bigint FROM jobs WHERE status = 'failed') AS failed_jobs, (SELECT count(*)::bigint FROM interviewee_candidates WHERE status = 'pending') AS pending_interviewee_reviews, (SELECT count(*)::bigint FROM contact_candidates WHERE status IN ('pending', 'needs_review')) AS pending_contact_reviews, (SELECT COALESCE(sum(cost_micros), 0)::bigint FROM ai_calls) AS ai_cost_micros", &[], ) .await .map_err(db_error)?; Ok(DashboardSummary { active_channels: row.get("active_channels"), videos: row.get("videos"), active_interviewees: row.get("active_interviewees"), active_contacts: row.get("active_contacts"), running_runs: row.get("running_runs"), queued_jobs: row.get("queued_jobs"), failed_jobs: row.get("failed_jobs"), pending_interviewee_reviews: row.get("pending_interviewee_reviews"), pending_contact_reviews: row.get("pending_contact_reviews"), ai_cost_micros: row.get("ai_cost_micros"), }) } pub async fn review_queue(db: &Db) -> AppResult { let client = db.client().await?; let row = client .query_one( "SELECT (SELECT count(*)::bigint FROM interviewee_candidates WHERE status = 'pending') AS interviewee_candidates_pending, (SELECT count(*)::bigint FROM interviewees WHERE deleted_at IS NULL AND status = 'active' AND dedup_review_status = 'needs_review') AS interviewees_needing_dedup_review, (SELECT count(*)::bigint FROM contact_candidates WHERE status = 'pending') AS contacts_pending, (SELECT count(*)::bigint FROM contact_candidates WHERE status = 'needs_review') AS contacts_needing_review", &[], ) .await .map_err(db_error)?; Ok(ReviewQueueSummary { interviewee_candidates_pending: row.get("interviewee_candidates_pending"), interviewees_needing_dedup_review: row.get("interviewees_needing_dedup_review"), contacts_pending: row.get("contacts_pending"), contacts_needing_review: row.get("contacts_needing_review"), }) } pub async fn job_status_counts(db: &Db) -> AppResult> { let client = db.client().await?; let rows = client .query( "SELECT status, count(*)::bigint AS count FROM jobs GROUP BY status ORDER BY status", &[], ) .await .map_err(db_error)?; Ok(rows .into_iter() .map(|row| QueueStatusCount { status: row.get("status"), count: row.get("count"), }) .collect()) }