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:
@@ -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