Refactor case detail into a dedicated page

This commit restructures the web interface for case details. The
previous `/web/cases/{case_id}` route, which previously showed both the
case summary and its recordings, has been split into two distinct pages:

- `/web/cases/{case_id}`: This page now displays the overall case
  information, including the document if available.
- `/web/cases/{case_id}/recordings`: This new page is dedicated to
  listing and displaying individual recordings within a case.

This change improves the organization and clarity of the web UI,
allowing for more focused views of case data. Additionally, the
`case_detail.html` and `document.html` templates have been removed as
their functionality is now handled by the new `case_page.html` and the
upcoming document rendering logic. The `cases.html` template has also
been removed, indicating a shift towards more granular page views.
This commit is contained in:
2026-04-19 17:11:29 +02:00
parent 3bb2d23adb
commit b7e54db54c
10 changed files with 326 additions and 445 deletions
-77
View File
@@ -125,31 +125,9 @@ struct MyCasesTemplate {
is_admin: bool,
}
#[derive(Template)]
#[template(path = "case_detail.html")]
struct CaseDetailTemplate {
slug: String,
case_id: String,
case_id_short: String,
oneliner: Option<String>,
recordings: Vec<RecordingView>,
can_analyze: bool,
llm_missing: bool,
analyzing: bool,
has_document: bool,
/// True iff the transcribe worker is currently running. Gate for the
/// per-recording "Transkription läuft…" label — suppresses the lie when
/// a transcript is missing but no worker is active.
transcribe_busy: bool,
/// True iff the session user is an admin — toggles admin-only controls
/// (currently the Reset button).
is_admin: bool,
}
#[derive(Template)]
#[template(path = "case_page.html")]
struct CasePageTemplate {
slug: String,
case_id: String,
case_id_short: String,
oneliner: Option<String>,
@@ -175,7 +153,6 @@ struct CaseRecordingsTemplate {
oneliner: Option<String>,
recordings: Vec<RecordingView>,
transcribe_busy: bool,
is_admin: bool,
}
/// Flags derived from filesystem state + config.
@@ -275,57 +252,6 @@ pub async fn handle_my_cases(
.map_err(|e| AppError::Internal(format!("Template render failed: {e}")))
}
pub async fn handle_case_detail(
user: AuthenticatedWebUser,
State(config): State<Arc<Config>>,
State(pipeline): State<PipelineState>,
CaseIdPath(case_id): CaseIdPath,
) -> Result<Html<String>, AppError> {
// IDOR guard: case_dir must live under the session's user_slug.
let user_root = config.data_path.join(&user.slug);
pipeline.heal_orphans_if_idle(&user_root, &user.slug).await;
let case_dir = locate_case_or_404(
&user_root,
&case_id,
&user.slug,
"case detail (possible IDOR probe)",
)
.await?;
let case_id_str = case_id.to_string();
info!(slug = %user.slug, case_id = %case_id, "case detail viewed");
let recordings = scan_recordings(&case_dir).await;
let oneliner = tokio::fs::read_to_string(case_dir.join("oneliner.txt"))
.await
.ok()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty());
let a_busy = pipeline.analyze_busy.0.load(Ordering::Acquire);
let t_busy = pipeline.transcribe_busy.0.load(Ordering::Acquire);
let flags = compute_flags(&case_dir, &recordings, config.llm_configured(), a_busy).await;
let case_id_short = case_id_str.chars().take(8).collect();
let is_admin = user.is_admin();
CaseDetailTemplate {
slug: user.slug,
case_id: case_id_str,
case_id_short,
oneliner,
recordings,
can_analyze: flags.can_analyze,
llm_missing: flags.llm_missing,
analyzing: flags.analyzing,
has_document: flags.has_document,
transcribe_busy: t_busy,
is_admin,
}
.render()
.map(Html)
.map_err(|e| AppError::Internal(format!("Template render failed: {e}")))
}
/// GET /web/cases/{case_id}
///
/// Canonical case page. Renders the document inline if present; otherwise
@@ -374,7 +300,6 @@ pub async fn handle_case_page(
let is_admin = user.is_admin();
CasePageTemplate {
slug: user.slug,
case_id: case_id_str,
case_id_short,
oneliner,
@@ -424,7 +349,6 @@ pub async fn handle_case_recordings(
let t_busy = pipeline.transcribe_busy.0.load(Ordering::Acquire);
let case_id_short = case_id_str.chars().take(8).collect();
let is_admin = user.is_admin();
CaseRecordingsTemplate {
slug: user.slug,
@@ -433,7 +357,6 @@ pub async fn handle_case_recordings(
oneliner,
recordings,
transcribe_busy: t_busy,
is_admin,
}
.render()
.map(Html)