Refactor busy flags and worker guards

The `WorkerBusy` type was previously a single `Arc<AtomicBool>` shared
between the analyze and transcribe workers. This commit refactors this
to:

- Introduce `AnalyzeBusy` and `TranscribeBusy` newtype wrappers around
  `WorkerBusy` to distinguish between the two flags. This allows
  `axum::extract::State` to target them individually.
- Move the `BusyGuard` RAII guard into `src/lib.rs` and make it generic
  to work with any `WorkerBusy` instance.
- Update the `main.rs`, `worker.rs`, and `routes/user_web.rs` files to
  use the new types and guards, ensuring correct state management for
  both pipelines.
- Enhance the transcription recovery scan
  (`transcribe::recovery::scan_and_enqueue`) to iterate over user
  directories and call `enqueue_pending_for_user` for each, making it
  more robust and aligned with the per-user self-healing mechanism.
- Add a check for `transcribe_busy` in the `case_detail.html` template
  to correctly display "Transkription läuft…" only when the transcribe
  worker is actually active.
This commit is contained in:
2026-04-16 00:32:45 +02:00
parent 9072d7a833
commit 2f62f11563
8 changed files with 175 additions and 118 deletions
+47 -15
View File
@@ -12,7 +12,8 @@ use crate::auth::AuthenticatedWebUser;
use crate::config::Config;
use crate::error::AppError;
use crate::routes::web::{scan_recordings, RecordingView};
use crate::WorkerBusy;
use crate::transcribe::{recovery as transcribe_recovery, TranscribeSender};
use crate::{AnalyzeBusy, TranscribeBusy};
struct UserCaseView {
case_id: String,
@@ -67,6 +68,10 @@ struct CaseDetailTemplate {
llm_missing: bool,
analyzing: bool,
has_document: bool,
/// True iff the transcribe worker is currently running. Gate for the
/// per-recording "Transkription läuft…" label — suppresses the lie when
/// a transcript is missing but no worker is active.
transcribe_busy: bool,
}
/// Flags derived from filesystem state + config.
@@ -142,30 +147,45 @@ pub(crate) async fn any_document_exists(case_dir: &Path) -> bool {
false
}
/// Self-heal: if the analyze worker is idle but orphan inputs (an input file
/// without its document) exist for this user, re-enqueue them. Page-load is
/// the trigger; no cron, no periodic task.
/// Self-heal: if a worker is idle but orphans exist on disk for this user,
/// re-enqueue them. Page-load is the trigger; no cron, no periodic task.
/// Runs for both pipelines so a page-load on either view cleans both.
async fn heal_orphans_if_idle(
user_root: &Path,
worker_busy: &WorkerBusy,
slug: &str,
analyze_busy: &AnalyzeBusy,
analyze_tx: &AnalyzeSender,
transcribe_busy: &TranscribeBusy,
transcribe_tx: &TranscribeSender,
) {
if worker_busy.load(Ordering::Acquire) {
return;
if !analyze_busy.0.load(Ordering::Acquire) {
analyze_recovery::enqueue_pending_for_user(user_root, analyze_tx).await;
}
if !transcribe_busy.0.load(Ordering::Acquire) {
transcribe_recovery::enqueue_pending_for_user(user_root, slug, transcribe_tx).await;
}
analyze_recovery::enqueue_pending_for_user(user_root, analyze_tx).await;
}
pub async fn handle_my_cases(
user: AuthenticatedWebUser,
State(config): State<Arc<Config>>,
State(worker_busy): State<WorkerBusy>,
State(analyze_busy): State<AnalyzeBusy>,
State(analyze_tx): State<AnalyzeSender>,
State(transcribe_busy): State<TranscribeBusy>,
State(transcribe_tx): State<TranscribeSender>,
) -> Result<Html<String>, AppError> {
let user_root = config.data_path.join(&user.slug);
heal_orphans_if_idle(&user_root, &worker_busy, &analyze_tx).await;
heal_orphans_if_idle(
&user_root,
&user.slug,
&analyze_busy,
&analyze_tx,
&transcribe_busy,
&transcribe_tx,
)
.await;
let busy = worker_busy.load(Ordering::Acquire);
let busy = analyze_busy.0.load(Ordering::Acquire);
let cases = scan_user_cases(&config, &user.slug, busy).await;
let undo_count = crate::routes::case_actions::summarize_latest_batch(&user_root)
.await
@@ -186,8 +206,10 @@ pub async fn handle_my_cases(
pub async fn handle_case_detail(
user: AuthenticatedWebUser,
State(config): State<Arc<Config>>,
State(worker_busy): State<WorkerBusy>,
State(analyze_busy): State<AnalyzeBusy>,
State(analyze_tx): State<AnalyzeSender>,
State(transcribe_busy): State<TranscribeBusy>,
State(transcribe_tx): State<TranscribeSender>,
AxumPath(case_id): AxumPath<String>,
) -> Result<Html<String>, AppError> {
uuid::Uuid::parse_str(&case_id)
@@ -195,7 +217,15 @@ pub async fn handle_case_detail(
// IDOR guard: case_dir must live under the session's user_slug.
let user_root = config.data_path.join(&user.slug);
heal_orphans_if_idle(&user_root, &worker_busy, &analyze_tx).await;
heal_orphans_if_idle(
&user_root,
&user.slug,
&analyze_busy,
&analyze_tx,
&transcribe_busy,
&transcribe_tx,
)
.await;
let case_dir = match locate_case(&user_root, &case_id).await {
Some(p) => p,
@@ -213,8 +243,9 @@ pub async fn handle_case_detail(
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty());
let busy = worker_busy.load(Ordering::Acquire);
let flags = compute_flags(&case_dir, &recordings, config.llm_configured(), busy).await;
let a_busy = analyze_busy.0.load(Ordering::Acquire);
let t_busy = transcribe_busy.0.load(Ordering::Acquire);
let flags = compute_flags(&case_dir, &recordings, config.llm_configured(), a_busy).await;
let case_id_short = case_id.chars().take(8).collect();
CaseDetailTemplate {
@@ -227,6 +258,7 @@ pub async fn handle_case_detail(
llm_missing: flags.llm_missing,
analyzing: flags.analyzing,
has_document: flags.has_document,
transcribe_busy: t_busy,
}
.render()
.map(Html)