Feat: Add bulk actions for case analysis and deletion

Introduces a new `/web/cases/bulk` endpoint to handle multiple case
actions simultaneously.

This change adds the following:
- A new `bulk` module for handling bulk operations.
- The `handle_bulk_action` function in `bulk.rs` to dispatch to specific
  actions.
- `bulk_analyze` and `bulk_delete` functions to process the actions.
- Updates `Cargo.toml` and `Cargo.lock` to include necessary
  dependencies: `form_urlencoded`, `serde_core`, `serde_html_form`, and
  `serde_path_to_error`.
- Modified `axum-extra` features to include `form`.
- Adds checks for deleted cases in `collect_pending` for both analysis
  and transcription recovery.
- Renames and modifies `handle_close_case` to `handle_analyze_case` in
  `case_actions.rs` to better reflect its functionality.
- Adds a new `handle_delete_case` and `handle_undo_delete` to
  `case_actions.rs`.
- Updates `my_cases.html` and `case_detail.html` to support bulk
  actions, including checkboxes, a bulk action bar, and an "Undo last
  delete" feature.
- Modifies `handle_audio` and `scan_cases` in `web.rs` to respect delete
  markers.
- Updates `analyze_test.rs` with new request builders for analyze and
  delete actions.
This commit is contained in:
2026-04-15 23:06:31 +02:00
parent 36b3e5f6c1
commit 0b63d8ff56
12 changed files with 662 additions and 288 deletions
+8 -1
View File
@@ -56,7 +56,11 @@ pub async fn handle_audio(
.map_err(|_| AppError::BadRequest("Invalid case_id (not a UUID)".into()))?;
validate_filename(&filename)?;
let file_path = crate::paths::case_dir(&config.data_path, &user, &case_id).join(&filename);
let case_path = crate::paths::case_dir(&config.data_path, &user, &case_id);
if crate::paths::is_deleted(&case_path).await {
return Err(AppError::NotFound("Audio file not found".into()));
}
let file_path = case_path.join(&filename);
if !tokio::fs::try_exists(&file_path).await.unwrap_or(false) {
return Err(AppError::NotFound("Audio file not found".into()));
}
@@ -129,6 +133,9 @@ async fn scan_cases(data_path: &Path) -> Vec<CaseView> {
}
let case_path = case_entry.path();
if crate::paths::is_deleted(&case_path).await {
continue;
}
let recordings = scan_recordings(&case_path).await;
if recordings.is_empty() {
continue;