feat(admin): overview dashboard sections + system temperature/load sensors (0.85.0) (#11)
This commit was merged in pull request #11.
This commit is contained in:
@@ -83,6 +83,77 @@ const MANGA_SYNC_STATE_CASE: &str = r#"
|
||||
END
|
||||
"#;
|
||||
|
||||
/// Library shape for the admin overview: total mangas split by derived
|
||||
/// sync state, plus library-wide chapter/page totals and the newest manga.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct MangaStats {
|
||||
pub total: i64,
|
||||
pub synced: i64,
|
||||
pub in_progress: i64,
|
||||
pub dropped: i64,
|
||||
pub total_chapters: i64,
|
||||
pub total_pages: i64,
|
||||
pub newest_title: Option<String>,
|
||||
pub newest_seen_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
/// Aggregate the manga library for the overview dashboard. Sync-state
|
||||
/// counts reuse `MANGA_SYNC_STATE_CASE` so they can't drift from the
|
||||
/// per-row classification on the Mangas tab.
|
||||
pub async fn manga_stats(pool: &PgPool) -> AppResult<MangaStats> {
|
||||
let counts_sql = format!(
|
||||
r#"
|
||||
SELECT
|
||||
COUNT(*)::bigint AS total,
|
||||
COUNT(*) FILTER (WHERE s = 'synced')::bigint AS synced,
|
||||
COUNT(*) FILTER (WHERE s = 'in_progress')::bigint AS in_progress,
|
||||
COUNT(*) FILTER (WHERE s = 'dropped')::bigint AS dropped
|
||||
FROM (SELECT {case} AS s FROM mangas m) q
|
||||
"#,
|
||||
case = MANGA_SYNC_STATE_CASE
|
||||
);
|
||||
let (total, synced, in_progress, dropped): (i64, i64, i64, i64) =
|
||||
sqlx::query_as(&counts_sql).fetch_one(pool).await?;
|
||||
|
||||
let (total_chapters,): (i64,) =
|
||||
sqlx::query_as("SELECT COUNT(*)::bigint FROM chapters")
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
let (total_pages,): (i64,) = sqlx::query_as("SELECT COUNT(*)::bigint FROM pages")
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
|
||||
// Newest by creation, with its own latest non-dropped crawl sighting
|
||||
// (NULL for user uploads with no source rows).
|
||||
let newest: Option<(String, Option<DateTime<Utc>>)> = sqlx::query_as(
|
||||
r#"
|
||||
SELECT m.title,
|
||||
(SELECT MAX(ms.last_seen_at) FROM manga_sources ms
|
||||
WHERE ms.manga_id = m.id AND ms.dropped_at IS NULL)
|
||||
FROM mangas m
|
||||
ORDER BY m.created_at DESC
|
||||
LIMIT 1
|
||||
"#,
|
||||
)
|
||||
.fetch_optional(pool)
|
||||
.await?;
|
||||
let (newest_title, newest_seen_at) = match newest {
|
||||
Some((title, seen)) => (Some(title), seen),
|
||||
None => (None, None),
|
||||
};
|
||||
|
||||
Ok(MangaStats {
|
||||
total,
|
||||
synced,
|
||||
in_progress,
|
||||
dropped,
|
||||
total_chapters,
|
||||
total_pages,
|
||||
newest_title,
|
||||
newest_seen_at,
|
||||
})
|
||||
}
|
||||
|
||||
/// Paginated admin manga list with derived sync state and total count.
|
||||
/// Filters by `search` (substring on title, case-insensitive) and
|
||||
/// `sync_state` (post-derivation). The CTE keeps the case expression
|
||||
|
||||
Reference in New Issue
Block a user