feat(admin): observability — job history, live now-analyzing, durations & metrics (0.84.0) (#6)
This commit was merged in pull request #6.
This commit is contained in:
@@ -26,7 +26,8 @@ use crate::api::pagination::PagedResponse;
|
||||
use crate::app::AppState;
|
||||
use crate::auth::extractor::RequireAdmin;
|
||||
use crate::domain::page_analysis::{
|
||||
ChapterCoverage, MangaCoverage, PageAnalysisDetail, PageStatusItem,
|
||||
AnalysisHistoryRow, AnalysisMetrics, ChapterCoverage, MangaCoverage, PageAnalysisDetail,
|
||||
PageStatusItem,
|
||||
};
|
||||
use crate::error::{AppError, AppResult};
|
||||
use crate::repo;
|
||||
@@ -41,6 +42,8 @@ pub fn routes() -> Router<AppState> {
|
||||
.route("/admin/analysis/mangas/:id/chapters", get(coverage_chapters))
|
||||
.route("/admin/analysis/chapters/:id/pages", get(chapter_pages))
|
||||
.route("/admin/analysis/pages/:id", get(page_detail))
|
||||
.route("/admin/analysis/history", get(list_history))
|
||||
.route("/admin/analysis/metrics", get(metrics))
|
||||
.route("/admin/analysis/status/stream", get(stream_status))
|
||||
}
|
||||
|
||||
@@ -149,6 +152,79 @@ struct ItemsResponse<T> {
|
||||
items: Vec<T>,
|
||||
}
|
||||
|
||||
/// Status/NSFW filters + pagination/search for the analysis history list.
|
||||
#[derive(Debug, Deserialize, Default)]
|
||||
pub struct HistoryParams {
|
||||
#[serde(default)]
|
||||
pub status: Option<String>,
|
||||
#[serde(default)]
|
||||
pub nsfw: bool,
|
||||
#[serde(default)]
|
||||
pub search: Option<String>,
|
||||
#[serde(default = "default_coverage_limit")]
|
||||
pub limit: i64,
|
||||
#[serde(default)]
|
||||
pub offset: i64,
|
||||
}
|
||||
|
||||
async fn list_history(
|
||||
State(state): State<AppState>,
|
||||
_admin: RequireAdmin,
|
||||
Query(params): Query<HistoryParams>,
|
||||
) -> AppResult<Json<PagedResponse<AnalysisHistoryRow>>> {
|
||||
let limit = params.limit.clamp(1, 100);
|
||||
let offset = params.offset.max(0);
|
||||
let status = params
|
||||
.status
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|s| !s.is_empty());
|
||||
let search = params
|
||||
.search
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|s| !s.is_empty());
|
||||
let (items, total) = repo::page_analysis::list_history(
|
||||
&state.db,
|
||||
repo::page_analysis::AnalysisHistoryFilter {
|
||||
status,
|
||||
nsfw_only: params.nsfw,
|
||||
search,
|
||||
},
|
||||
limit,
|
||||
offset,
|
||||
)
|
||||
.await?;
|
||||
Ok(Json(PagedResponse::with_total(items, limit, offset, total)))
|
||||
}
|
||||
|
||||
/// `?days=` window for the metrics endpoints. `0` (or absent) = all time.
|
||||
#[derive(Debug, Deserialize, Default)]
|
||||
pub struct MetricsParams {
|
||||
#[serde(default)]
|
||||
pub days: i64,
|
||||
}
|
||||
|
||||
/// Convert a `days` window param into a lower-bound timestamp. `<= 0` → all
|
||||
/// time (`None`); otherwise `now - days` (capped at a year).
|
||||
pub(super) fn window_since(days: i64) -> Option<chrono::DateTime<chrono::Utc>> {
|
||||
if days <= 0 {
|
||||
None
|
||||
} else {
|
||||
Some(chrono::Utc::now() - chrono::Duration::days(days.min(365)))
|
||||
}
|
||||
}
|
||||
|
||||
async fn metrics(
|
||||
State(state): State<AppState>,
|
||||
_admin: RequireAdmin,
|
||||
Query(params): Query<MetricsParams>,
|
||||
) -> AppResult<Json<AnalysisMetrics>> {
|
||||
let since = window_since(params.days);
|
||||
let m = repo::page_analysis::analysis_metrics(&state.db, since).await?;
|
||||
Ok(Json(m))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ReenqueueBody {
|
||||
/// Skip pages that already have a `done` analysis row. Defaults to
|
||||
|
||||
Reference in New Issue
Block a user