feat(admin): observability — job history, live now-analyzing, durations & metrics (0.84.0) (#6)
Some checks failed
deploy / test-backend (push) Failing after 19m59s
deploy / test-frontend (push) Successful in 9m54s
deploy / build-and-push (push) Has been skipped
deploy / deploy (push) Has been skipped

This commit was merged in pull request #6.
This commit is contained in:
2026-06-16 12:21:13 +00:00
parent 790549636f
commit d51ab2a049
41 changed files with 3655 additions and 21 deletions

View File

@@ -216,16 +216,23 @@ impl WorkerContext {
}
}
// Resolve the page breadcrumb once so live events carry the
// manga/chapter/number the dashboard keys on. A missing breadcrumb
// (page deleted) just suppresses events — the dispatch still runs
// and acks normally.
let breadcrumb = repo::page::locate(&self.pool, page_id).await.ok().flatten();
if let Some((manga_id, chapter_id, page_number)) = breadcrumb {
// Resolve the labeled page breadcrumb once so live events carry the
// manga/chapter/number AND human labels the dashboard's "now
// analyzing" banner names. A missing breadcrumb (page deleted) just
// suppresses events — the dispatch still runs and acks normally.
let breadcrumb = repo::page::locate_labeled(&self.pool, page_id)
.await
.ok()
.flatten();
if let Some((manga_id, manga_title, chapter_id, chapter_number, page_number)) =
breadcrumb.clone()
{
self.events.publish(AnalysisEvent::Started {
page_id,
manga_id,
manga_title,
chapter_id,
chapter_number,
page_number,
});
}
@@ -246,8 +253,10 @@ impl WorkerContext {
})
};
let started = std::time::Instant::now();
let dispatch = AssertUnwindSafe(self.dispatcher.dispatch(page_id)).catch_unwind();
let outcome = tokio::time::timeout(self.job_timeout, dispatch).await;
let elapsed_ms = started.elapsed().as_millis() as i64;
heartbeat.abort();
// Flatten timeout / panic / dispatch error into one Result + message.
@@ -258,11 +267,27 @@ impl WorkerContext {
Ok(Ok(Ok(()))) => Ok(()),
};
if let Some((manga_id, chapter_id, page_number)) = breadcrumb {
if let Some((manga_id, manga_title, chapter_id, chapter_number, page_number)) =
breadcrumb
{
let event = if result.is_ok() {
AnalysisEvent::Completed { page_id, manga_id, chapter_id, page_number }
AnalysisEvent::Completed {
page_id,
manga_id,
manga_title,
chapter_id,
chapter_number,
page_number,
}
} else {
AnalysisEvent::Failed { page_id, manga_id, chapter_id, page_number }
AnalysisEvent::Failed {
page_id,
manga_id,
manga_title,
chapter_id,
chapter_number,
page_number,
}
};
self.events.publish(event);
}
@@ -294,6 +319,11 @@ impl WorkerContext {
}
}
}
// Stamp how long the vision dispatch took. Best-effort and ordered
// after the row is written (persist on success / mark_failed on
// terminal failure); a transient failure with no row updates nothing.
let _ = repo::page_analysis::record_duration(&self.pool, page_id, elapsed_ms).await;
}
}

View File

@@ -25,25 +25,33 @@ pub enum AnalysisEvent {
manga_id: Option<Uuid>,
chapter_id: Option<Uuid>,
},
/// The worker began analyzing a page.
/// The worker began analyzing a page. Carries the manga title and
/// chapter number (alongside the ids) so the dashboard's "now
/// analyzing" banner can name the target without a lookup.
Started {
page_id: Uuid,
manga_id: Uuid,
manga_title: String,
chapter_id: Uuid,
chapter_number: i32,
page_number: i32,
},
/// The worker finished a page successfully.
Completed {
page_id: Uuid,
manga_id: Uuid,
manga_title: String,
chapter_id: Uuid,
chapter_number: i32,
page_number: i32,
},
/// The worker failed a page (this attempt).
Failed {
page_id: Uuid,
manga_id: Uuid,
manga_title: String,
chapter_id: Uuid,
chapter_number: i32,
page_number: i32,
},
}
@@ -76,3 +84,25 @@ impl Default for AnalysisEvents {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn started_serializes_kind_and_labels() {
let ev = AnalysisEvent::Started {
page_id: Uuid::nil(),
manga_id: Uuid::nil(),
manga_title: "Berserk".into(),
chapter_id: Uuid::nil(),
chapter_number: 12,
page_number: 7,
};
let v = serde_json::to_value(&ev).unwrap();
assert_eq!(v["kind"], "started");
assert_eq!(v["manga_title"], "Berserk");
assert_eq!(v["chapter_number"], 12);
assert_eq!(v["page_number"], 7);
}
}