Add pending upload indicator to UI
This commit introduces a visual indicator in the UI to show when a case has pending uploads. The `WorkerSnapshot` struct has been updated to include a `pending_case_ids` field, which is an `Arc<HashSet<Uuid>>`. This set stores the IDs of cases that currently have files in the pending directory. The `run_loop` function now populates this set by scanning the pending directory and collecting the `case_id`s of any files found. This snapshot is then sent to the UI via the `worker_rx` channel. In the `DoctateApp::ui` method, the `pending_ids` set is cloned from the worker's snapshot. If a case's ID is present in this set, a distinct "⬆" prefix and a yellow label are used to highlight it, signifying an ongoing or pending upload. Otherwise, the standard label is used.
This commit is contained in:
@@ -483,6 +483,14 @@ impl DoctateApp {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Pending-upload lookup for the badge. Cheap clone (Arc) from the
|
||||
// worker's last published snapshot; empty set if no worker yet.
|
||||
let pending_ids = self
|
||||
.worker_rx
|
||||
.as_ref()
|
||||
.map(|rx| rx.borrow().pending_case_ids.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut action = None;
|
||||
// Ready-for-new means the user-intent state is Idle AND no ffmpeg
|
||||
// instance is still flushing. Upload-queue depth does NOT gate.
|
||||
@@ -499,7 +507,17 @@ impl DoctateApp {
|
||||
.as_deref()
|
||||
.map(str::to_owned)
|
||||
.unwrap_or_else(|| "⏳".to_owned());
|
||||
ui.label(format!("{time_str} · {oneliner}"));
|
||||
let has_pending = pending_ids.contains(&marker.case_id);
|
||||
let line = if has_pending {
|
||||
format!("⬆ {time_str} · {oneliner}")
|
||||
} else {
|
||||
format!("{time_str} · {oneliner}")
|
||||
};
|
||||
if has_pending {
|
||||
ui.colored_label(egui::Color32::from_rgb(200, 140, 0), line);
|
||||
} else {
|
||||
ui.label(line);
|
||||
}
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
if ui
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
//! never succeed; keeping the audio around would violate the
|
||||
//! data-minimization rule.
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, Mutex as StdMutex};
|
||||
use std::time::{Duration, Instant};
|
||||
@@ -31,6 +32,7 @@ use reqwest::StatusCode;
|
||||
use tokio::sync::{mpsc, watch};
|
||||
use tokio::task::AbortHandle;
|
||||
use tracing::{debug, info, warn};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::case_store::CaseStore;
|
||||
use crate::snapshot_cache::{CachedSnapshot, SnapshotCache};
|
||||
@@ -64,6 +66,10 @@ pub struct WorkerSnapshot {
|
||||
/// iteration. `0` during `Polling` / `Idle`, positive during
|
||||
/// `Uploading`.
|
||||
pub queue_len: usize,
|
||||
/// Set of `case_id`s that currently have at least one file sitting
|
||||
/// in the pending directory. Shared as an `Arc` so the UI can clone
|
||||
/// the borrow cheaply and look up per-case without scanning disk.
|
||||
pub pending_case_ids: Arc<HashSet<Uuid>>,
|
||||
/// Set to `Some(now)` after any successful upload or poll (200/304).
|
||||
/// `None` before the first success. Drives Online/Offline heuristics
|
||||
/// together with `last_failure`.
|
||||
@@ -79,6 +85,7 @@ impl Default for WorkerSnapshot {
|
||||
Self {
|
||||
state: WorkerState::Idle,
|
||||
queue_len: 0,
|
||||
pending_case_ids: Arc::new(HashSet::new()),
|
||||
last_success: None,
|
||||
last_failure: None,
|
||||
}
|
||||
@@ -207,11 +214,13 @@ async fn run_loop(
|
||||
|
||||
let publish = |state: WorkerState,
|
||||
queue_len: usize,
|
||||
pending_ids: Arc<HashSet<Uuid>>,
|
||||
last_ok: Option<Instant>,
|
||||
last_fail: Option<Instant>| {
|
||||
let snap = WorkerSnapshot {
|
||||
state,
|
||||
queue_len,
|
||||
pending_case_ids: pending_ids,
|
||||
last_success: last_ok,
|
||||
last_failure: last_fail,
|
||||
};
|
||||
@@ -224,10 +233,18 @@ async fn run_loop(
|
||||
let mut files = scan_pending_dir(&pending_dir);
|
||||
files.sort_by(|a, b| a.recorded_at.cmp(&b.recorded_at)); // FIFO
|
||||
let queue_len = files.len();
|
||||
let pending_ids: Arc<HashSet<Uuid>> =
|
||||
Arc::new(files.iter().map(|u| u.case_id).collect());
|
||||
let next = files.into_iter().next();
|
||||
|
||||
if let Some(upload) = next {
|
||||
publish(WorkerState::Uploading, queue_len, last_success, last_failure);
|
||||
publish(
|
||||
WorkerState::Uploading,
|
||||
queue_len,
|
||||
pending_ids.clone(),
|
||||
last_success,
|
||||
last_failure,
|
||||
);
|
||||
let _ = events_tx.send(UploadEvent::Started(upload.case_id));
|
||||
match post_upload(&http, &config, &upload).await {
|
||||
UploadOutcome::Succeeded(status) => {
|
||||
@@ -256,7 +273,13 @@ async fn run_loop(
|
||||
// doesn't misreport "Uploading" during a passive wait.
|
||||
// The footer derives "Offline" from `last_failure` being
|
||||
// younger than `last_success`.
|
||||
publish(WorkerState::Idle, queue_len, last_success, last_failure);
|
||||
publish(
|
||||
WorkerState::Idle,
|
||||
queue_len,
|
||||
pending_ids.clone(),
|
||||
last_success,
|
||||
last_failure,
|
||||
);
|
||||
tokio::time::sleep(backoff).await;
|
||||
backoff = (backoff * 2).min(MAX_BACKOFF);
|
||||
}
|
||||
@@ -273,7 +296,13 @@ async fn run_loop(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
publish(WorkerState::Polling, 0, last_success, last_failure);
|
||||
publish(
|
||||
WorkerState::Polling,
|
||||
0,
|
||||
pending_ids.clone(),
|
||||
last_success,
|
||||
last_failure,
|
||||
);
|
||||
match poll_once(&http, &config, &store, &cache, &mut etag).await {
|
||||
Ok(PollOutcome::Success) | Ok(PollOutcome::NotModified) => {
|
||||
let now = Instant::now();
|
||||
@@ -290,7 +319,13 @@ async fn run_loop(
|
||||
}
|
||||
}
|
||||
|
||||
publish(WorkerState::Idle, 0, last_success, last_failure);
|
||||
publish(
|
||||
WorkerState::Idle,
|
||||
0,
|
||||
pending_ids.clone(),
|
||||
last_success,
|
||||
last_failure,
|
||||
);
|
||||
|
||||
// Wait for next tick OR kick. Cancel-safe on both arms.
|
||||
tokio::select! {
|
||||
|
||||
Reference in New Issue
Block a user