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;
}
}