refactor: replace delete/undo flow with close/reopen endpoints

- POST /web/cases/:id/delete renamed to /close; emits CaseClosed.
- New POST /web/cases/:id/reopen removes the marker; emits CaseReopened.
- Bulk action "delete" renamed to "close"; the shared closed_at timestamp
  still groups the selection for future UI features.
- POST /web/cases/undo-delete removed along with summarize_latest_close_group,
  latest_close_timestamp and restore_close_group. Per-case reopen makes
  the group-undo banner obsolete.
- CaseEventKind::CaseDeleted -> CaseClosed, CaseRestored -> CaseReopened.
- Templates updated to Lucide trash-2 icon, German label "Fall schliessen".
- Tests migrated; delete_watermark_test.rs moved to close_watermark_test.rs.
This commit is contained in:
2026-04-21 10:43:39 +02:00
parent 9410d6daaa
commit 396565a571
11 changed files with 176 additions and 241 deletions
+10 -12
View File
@@ -27,9 +27,9 @@ pub struct BulkForm {
case_ids: Vec<String>,
}
/// POST /web/cases/bulk — apply `action` (analyze | delete) to every
/// selected case. Errors per case are logged but do not abort the batch:
/// a single bad/missing case must not block the rest.
/// POST /web/cases/bulk — apply `action` (analyze | close | reset) to
/// every selected case. Errors per case are logged but do not abort the
/// batch: a single bad/missing case must not block the rest.
pub async fn handle_bulk_action(
user: AuthenticatedWebUser,
State(config): State<Arc<Config>>,
@@ -51,7 +51,7 @@ pub async fn handle_bulk_action(
)
.await
}
"delete" => bulk_delete(&events_tx, &user.slug, &user_root, &form.case_ids).await,
"close" => bulk_close(&events_tx, &user.slug, &user_root, &form.case_ids).await,
"reset" => {
if !user.is_admin() {
warn!(slug = %user.slug, "bulk-reset: not admin");
@@ -133,25 +133,23 @@ async fn bulk_analyze(
info!(slug = %slug, ok, skipped, "bulk-analyze completed");
}
async fn bulk_delete(
async fn bulk_close(
events_tx: &EventSender,
slug: &str,
user_root: &std::path::Path,
case_ids: &[String],
) {
// Shared `closed_at` across the whole bulk action: identical timestamp
// groups the cases so a subsequent undo can restore exactly this click.
let closed_at = now_rfc3339();
let mut ok = 0usize;
let mut skipped = 0usize;
for case_id in case_ids {
if uuid::Uuid::parse_str(case_id).is_err() {
warn!(slug = %slug, case_id = %case_id, "bulk-delete: invalid case_id");
warn!(slug = %slug, case_id = %case_id, "bulk-close: invalid case_id");
skipped += 1;
continue;
}
let Some(case_dir) = locate_case(user_root, case_id).await else {
warn!(slug = %slug, case_id = %case_id, "bulk-delete: case not found");
warn!(slug = %slug, case_id = %case_id, "bulk-close: case not found");
skipped += 1;
continue;
};
@@ -159,14 +157,14 @@ async fn bulk_delete(
closed_at: closed_at.clone(),
};
if let Err(e) = write_close_marker(&case_dir, &marker).await {
warn!(slug = %slug, case_id = %case_id, error = %e, "bulk-delete: write marker failed");
warn!(slug = %slug, case_id = %case_id, error = %e, "bulk-close: write marker failed");
skipped += 1;
continue;
}
events::emit(events_tx, slug, case_id, CaseEventKind::CaseDeleted);
events::emit(events_tx, slug, case_id, CaseEventKind::CaseClosed);
ok += 1;
}
info!(slug = %slug, closed_at = %closed_at, ok, skipped, "bulk-delete completed");
info!(slug = %slug, closed_at = %closed_at, ok, skipped, "bulk-close completed");
}
async fn bulk_reset(