feat: Add reanalyze case functionality

Introduce a new endpoint for reanalyzing cases. This feature allows
administrators
to rebuild analysis input files and enqueue new analysis jobs. The
system will
then generate a new document version, which will become the displayed
document.
This commit is contained in:
2026-04-15 21:19:56 +02:00
parent 7bcd94f0d0
commit 5608c8ba43
5 changed files with 300 additions and 2 deletions
+66
View File
@@ -87,6 +87,72 @@ pub async fn handle_close_case(
Ok(Redirect::to(&format!("/web/cases/{case_id}")))
}
/// POST /web/cases/{case_id}/reanalyze
///
/// Admin-only. Build a fresh `analysis_input_v{N+1}.json` from the current
/// transcripts and enqueue a new analyze job. `find_latest_document` picks the
/// currently-highest N; the worker will write `document_v{N+1}.md`, which then
/// wins the "highest version" race and becomes the displayed document.
pub async fn handle_reanalyze_case(
user: AuthenticatedWebUser,
State(config): State<Arc<Config>>,
State(analyze_tx): State<AnalyzeSender>,
AxumPath(case_id): AxumPath<String>,
) -> Result<Redirect, AppError> {
uuid::Uuid::parse_str(&case_id)
.map_err(|_| AppError::BadRequest("Invalid case_id".into()))?;
if !user.is_admin() {
return Err(AppError::Forbidden("Admin erforderlich".into()));
}
if !config.llm_configured() {
return Err(AppError::ServiceUnavailable(
"LLM-Analyse nicht konfiguriert".into(),
));
}
let user_root = config.data_path.join(&user.slug);
let case_dir = user_root.join("open").join(&case_id);
if !tokio::fs::try_exists(&case_dir).await.unwrap_or(false) {
warn!(slug = %user.slug, case_id = %case_id, "reanalyze: case not found in open/");
return Err(AppError::NotFound("Case not found".into()));
}
let current_version = find_latest_document(&case_dir)
.await
.map(|(v, _)| v)
.ok_or_else(|| {
AppError::BadRequest("Kein bestehendes Dokument — nutze 'Fall abschließen'".into())
})?;
let next_version = current_version + 1;
let input = build_analysis_input(&case_dir, next_version).await?;
let input_path = case_dir.join(format!("analysis_input_v{next_version}.json"));
write_input_create_new(&input_path, &input).await?;
if analyze_tx
.send(AnalyzeJob {
case_dir: case_dir.clone(),
version: next_version,
})
.is_err()
{
warn!(case_id = %case_id, "analyze send failed (worker gone)");
}
info!(
slug = %user.slug,
case_id = %case_id,
version = next_version,
recordings = input.recordings.len(),
"reanalyze requested; analysis enqueued"
);
Ok(Redirect::to(&format!("/web/cases/{case_id}")))
}
/// GET /web/cases/{case_id}/document
///
/// Render the latest `document_v{N}.md` for the case. The handler scans the
+4
View File
@@ -24,6 +24,10 @@ pub fn api_router() -> Router<AppState> {
"/web/cases/{case_id}/close",
post(case_actions::handle_close_case),
)
.route(
"/web/cases/{case_id}/reanalyze",
post(case_actions::handle_reanalyze_case),
)
.route(
"/web/cases/{case_id}/document",
get(case_actions::handle_document_view),
+13 -1
View File
@@ -43,6 +43,7 @@ struct CaseDetailTemplate {
llm_missing: bool,
analyzing: bool,
has_document: bool,
can_reanalyze: bool,
}
/// Four mutually exclusive flags derived from filesystem state + config.
@@ -54,6 +55,7 @@ struct CaseFlags {
analyzing: bool,
can_close: bool,
llm_missing: bool,
can_reanalyze: bool,
}
async fn compute_flags(
@@ -61,6 +63,7 @@ async fn compute_flags(
status: &str,
recordings: &[RecordingView],
llm_configured: bool,
is_admin: bool,
) -> CaseFlags {
let has_document = any_document_exists(case_dir).await;
let analyzing = !has_document
@@ -79,6 +82,7 @@ async fn compute_flags(
analyzing,
can_close: transcribed_stage && llm_configured,
llm_missing: transcribed_stage && !llm_configured,
can_reanalyze: has_document && is_admin && llm_configured,
}
}
@@ -140,7 +144,14 @@ pub async fn handle_case_detail(
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty());
let flags = compute_flags(&case_dir, &status, &recordings, config.llm_configured()).await;
let flags = compute_flags(
&case_dir,
&status,
&recordings,
config.llm_configured(),
user.is_admin(),
)
.await;
let case_id_short = case_id.chars().take(8).collect();
CaseDetailTemplate {
@@ -154,6 +165,7 @@ pub async fn handle_case_detail(
llm_missing: flags.llm_missing,
analyzing: flags.analyzing,
has_document: flags.has_document,
can_reanalyze: flags.can_reanalyze,
}
.render()
.map(Html)