fix: preserve show_closed view + ungrey closed-case controls

Three UX polish items for the show-closed listing, grouped because
they are the same root problem in three surfaces:

* handle_reopen_case now redirects via resolve_return_path(&headers)
  instead of hard-coding /web/cases, so a reopen from the
  ?show_closed=1 listing stays in that view. Matches the pattern
  already used by analyze/reset/delete-recording.

* handle_close_case now redirects via the new helper
  resolve_list_return_path(&headers), which wraps resolve_return_path
  and additionally collapses a /web/cases/{uuid} detail path to
  /web/cases while keeping the query string. A close from the detail
  page previously would have tried to redirect back to the now-404
  detail, or when triggered from ?show_closed=1 would have dropped
  the query.

* Closed cases in my_cases.html now render the checkbox,
  Analysieren/Neu analysieren and Reset controls in disabled state
  instead of being hidden. Layout stays consistent with mixed-state
  listings, and the user can see what actions would be available
  after reopen. The handlers remain the authoritative guard via
  locate_case_or_404 -> 404 for closed cases, so the HTML disabled
  is a hint only.

Adds four unit tests for resolve_list_return_path and two integration
tests for the new redirect behaviour (close from listing with query,
close from detail with query).
This commit is contained in:
2026-04-21 11:20:17 +02:00
parent 23e828df45
commit 8b1f58ba23
3 changed files with 191 additions and 7 deletions
+118
View File
@@ -851,6 +851,124 @@ async fn closed_case_detail_with_show_closed_returns_200() {
);
}
#[tokio::test]
async fn close_redirects_back_to_referer_query() {
// Close from the show-closed listing must keep ?show_closed=1 so
// the user stays in the view they were in.
let config = config_with_llm(unique_tmp("close-ref-q"), "http://unused".into());
let case_id = "11111111-1111-1111-1111-111111111111";
let case_dir = seed_case(&config.data_path, "dr_a", case_id);
seed_recording(&case_dir, "10-00-00", Some("ok"));
let app = doctate_server::create_router(config);
let cookie = login(app.clone(), "dr_a").await;
let resp = app
.oneshot(
Request::builder()
.method("POST")
.uri(format!("/web/cases/{case_id}/close"))
.header(header::COOKIE, &cookie)
.header(header::REFERER, "/web/cases?show_closed=1")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::SEE_OTHER);
assert_eq!(
resp.headers()
.get(header::LOCATION)
.unwrap()
.to_str()
.unwrap(),
"/web/cases?show_closed=1"
);
}
#[tokio::test]
async fn close_from_detail_redirects_to_list_preserving_query() {
// Close from /web/cases/{uuid}?show_closed=1 must NOT redirect back
// to the detail page (it 404s after the close). The uuid segment
// is stripped; the query survives so the user lands in show-closed.
let config = config_with_llm(unique_tmp("close-ref-det"), "http://unused".into());
let case_id = "11111111-1111-1111-1111-111111111111";
let case_dir = seed_case(&config.data_path, "dr_a", case_id);
seed_recording(&case_dir, "10-00-00", Some("ok"));
let app = doctate_server::create_router(config);
let cookie = login(app.clone(), "dr_a").await;
let resp = app
.oneshot(
Request::builder()
.method("POST")
.uri(format!("/web/cases/{case_id}/close"))
.header(header::COOKIE, &cookie)
.header(
header::REFERER,
format!("/web/cases/{case_id}?show_closed=1"),
)
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::SEE_OTHER);
assert_eq!(
resp.headers()
.get(header::LOCATION)
.unwrap()
.to_str()
.unwrap(),
"/web/cases?show_closed=1",
"close from detail must redirect to the LIST with the query intact"
);
}
#[tokio::test]
async fn reopen_redirects_back_to_referer() {
// User reopens a case from the show-closed listing. The redirect
// must preserve `?show_closed=1` so the user stays in that view
// instead of being ejected back to the default open-only listing.
let config = config_with_llm(unique_tmp("reopen-ref"), "http://unused".into());
let case_id = "11111111-1111-1111-1111-111111111111";
let case_dir = seed_case(&config.data_path, "dr_a", case_id);
seed_recording(&case_dir, "10-00-00", Some("ok"));
let app = doctate_server::create_router(config);
let cookie = login(app.clone(), "dr_a").await;
let _ = app
.clone()
.oneshot(close_request(case_id, &cookie))
.await
.unwrap();
let resp = app
.oneshot(
Request::builder()
.method("POST")
.uri(format!("/web/cases/{case_id}/reopen"))
.header(header::COOKIE, &cookie)
.header(header::REFERER, "/web/cases?show_closed=1")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::SEE_OTHER);
assert_eq!(
resp.headers()
.get(header::LOCATION)
.unwrap()
.to_str()
.unwrap(),
"/web/cases?show_closed=1",
"reopen must redirect back to the referer path (preserving the query)"
);
}
#[tokio::test]
async fn reopen_is_noop_on_open_case() {
let config = config_with_llm(unique_tmp("reopen-2"), "http://unused".into());