Refactor case pages into two distinct routes

This commit separates the case detail page into two distinct routes:
`/web/cases/{case_id}` for the main case information and document, and
`/web/cases/{case_id}/recordings` for a dedicated view of audio files
and their transcripts.

This change improves the organization and clarity of the case viewing
experience by segmenting the related but distinct information into their
own UI sections.
This commit is contained in:
2026-04-19 17:02:16 +02:00
parent 76e8ee18e9
commit 3bb2d23adb
5 changed files with 329 additions and 129 deletions
-55
View File
@@ -557,39 +557,6 @@ async fn recovery_skips_completed_analysis() {
);
}
// ---------------------------------------------------------------------
// Document view
// ---------------------------------------------------------------------
#[tokio::test]
async fn document_view_reads_document() {
let config = config_with_llm(unique_tmp("dv"), "http://unused".into());
let case_id = "11111111-1111-1111-1111-111111111111";
let case_dir = seed_case(&config.data_path, "dr_a", case_id);
std::fs::write(case_dir.join("document.md"), "der Inhalt").unwrap();
let app = doctate_server::create_router(config);
let cookie = login(app.clone(), "dr_a").await;
let resp = app
.oneshot(
Request::builder()
.uri(format!("/web/cases/{case_id}/document"))
.header(header::COOKIE, &cookie)
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
.await
.unwrap();
let body_str = String::from_utf8(body.to_vec()).unwrap();
assert!(body_str.contains("der Inhalt"), "expected content in body");
assert!(body_str.contains("Dokument"));
}
// ---------------------------------------------------------------------
// Re-analysis path: same handler, version derived from existing documents
// ---------------------------------------------------------------------
@@ -827,28 +794,6 @@ async fn recovery_skips_deleted_cases() {
assert!(rx.try_recv().is_err(), "deleted case must not be enqueued");
}
#[tokio::test]
async fn document_view_without_document_returns_404() {
let config = config_with_llm(unique_tmp("dv2"), "http://unused".into());
let case_id = "11111111-1111-1111-1111-111111111111";
seed_case(&config.data_path, "dr_a", case_id);
let app = doctate_server::create_router(config);
let cookie = login(app.clone(), "dr_a").await;
let resp = app
.oneshot(
Request::builder()
.uri(format!("/web/cases/{case_id}/document"))
.header(header::COOKIE, &cookie)
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}
// ---------------------------------------------------------------------
// Admin-only reset
// ---------------------------------------------------------------------
-74
View File
@@ -27,80 +27,6 @@ fn test_config() -> Arc<Config> {
})
}
async fn body_to_string(response: axum::response::Response) -> String {
let bytes = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
String::from_utf8(bytes.to_vec()).unwrap()
}
#[tokio::test]
async fn web_index_empty() {
let config = test_config();
let app = doctate_server::create_router(config);
let response = app
.oneshot(Request::builder().uri("/web/").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = body_to_string(response).await;
assert!(body.contains("No cases found"));
}
#[tokio::test]
async fn web_index_shows_cases() {
let config = test_config();
let data_path = config.data_path.clone();
// Pre-create cases on disk
let case1 = "550e8400-e29b-41d4-a716-446655440000";
let case2 = "660e8400-e29b-41d4-a716-446655440000";
let open_case = data_path.join("dr_test").join(case1);
std::fs::create_dir_all(&open_case).unwrap();
std::fs::write(open_case.join("2026-04-13T10-30-00Z.m4a"), b"fake audio 1").unwrap();
let done_case = data_path.join("dr_test").join(case2);
std::fs::create_dir_all(&done_case).unwrap();
std::fs::write(done_case.join("2026-04-12T09-00-00Z.m4a"), b"fake audio 2").unwrap();
std::fs::write(
done_case.join("2026-04-12T09-00-00Z.transcript.txt"),
"Herzkatheter ohne Befund.",
)
.unwrap();
std::fs::write(done_case.join("oneliner.txt"), "Herzkatheter unauffällig").unwrap();
let app = doctate_server::create_router(config);
let response = app
.oneshot(Request::builder().uri("/web/").body(Body::empty()).unwrap())
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = body_to_string(response).await;
assert!(body.contains(case1));
assert!(body.contains(case2));
assert!(body.contains("dr_test"));
assert!(body.contains("<audio"));
assert!(body.contains("2026-04-13T10-30-00Z.m4a"));
// case1 has no transcript → pending placeholder
assert!(body.contains("Transcription pending"));
// case2 has a transcript → rendered
assert!(body.contains("Herzkatheter ohne Befund."));
// case2 has an oneliner → rendered exactly once (case1 has none)
assert!(body.contains("Herzkatheter unauffällig"));
assert_eq!(
body.matches("class=\"oneliner\"").count(),
1,
"expected exactly one oneliner div"
);
let _ = std::fs::remove_dir_all(&data_path);
}
#[tokio::test]
async fn web_audio_serves_file() {
let config = test_config();