feat: Show LLM failure banner and retry button
When an LLM analysis fails, a `.analysis_failed.json` marker is created. If no document exists yet and the auto-trigger is blocked by this marker, the case page must display a banner. This banner provides the failure reason and a button to retry the analysis, ensuring users are not left in a dead-end state. The `read_failure_marker` function is made public to allow the web layer to access this failure information. The `CasePageTemplate` is updated to include `analysis_failed` data, which conditionally renders the new `.failure-banner` HTML. This change prevents cases from becoming unrecoverable due to transient LLM errors.
This commit is contained in:
@@ -269,7 +269,13 @@ async fn scan_m4as(case_dir: &Path) -> M4aScan {
|
||||
scan
|
||||
}
|
||||
|
||||
async fn read_failure_marker(case_dir: &Path) -> Option<FailureMarker> {
|
||||
/// Read and parse the per-case failure marker. `pub(crate)` so the web
|
||||
/// layer can surface the failure state in the case page (otherwise a
|
||||
/// transport-level LLM error leaves the user with no UI affordance to
|
||||
/// retry — the auto-trigger sees the marker and skips, but no document
|
||||
/// exists yet either, so the existing `Neu analysieren` button is not
|
||||
/// rendered).
|
||||
pub(crate) async fn read_failure_marker(case_dir: &Path) -> Option<FailureMarker> {
|
||||
let bytes = tokio::fs::read(case_dir.join(FAILURE_MARKER_FILE))
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
@@ -333,6 +333,11 @@ struct CasePageTemplate {
|
||||
llm_missing: bool,
|
||||
analyzing: bool,
|
||||
has_document: bool,
|
||||
/// `Some` iff a previous LLM analysis failed AND no document exists.
|
||||
/// Renders a dead-end-recovery banner with reason + retry button so
|
||||
/// the user is never stranded when a transient LLM error left only a
|
||||
/// `.analysis_failed.json` marker behind. See `compute_flags`.
|
||||
analysis_failed: Option<FailureBanner>,
|
||||
/// True iff at least one `.m4a.failed` recording exists. Drives the
|
||||
/// verdrängende `fehler`-Badge in the case header; see the same
|
||||
/// field on `UserCaseView` for semantics.
|
||||
@@ -369,6 +374,22 @@ struct CaseRecordingsTemplate {
|
||||
csrf_token: String,
|
||||
}
|
||||
|
||||
/// Information about a previous failed analysis run, surfaced in the
|
||||
/// case page when no document exists yet so the user always has an
|
||||
/// affordance to retry. Populated from `.analysis_failed.json` only
|
||||
/// when `!has_document && !analyzing` — otherwise the marker is either
|
||||
/// already obsolete (a successful run will overwrite it) or the worker
|
||||
/// is currently producing a fresh result anyway.
|
||||
struct FailureBanner {
|
||||
/// Raw `reason` string from the marker. Shown verbatim inside a
|
||||
/// `<details>` block — useful for admins, harmless for users.
|
||||
reason: String,
|
||||
/// RFC3339 UTC timestamp of the failed run. Browser JS reformats it
|
||||
/// into the doctor's local timezone (same `time_format.js` that
|
||||
/// formats `recorded_at_iso`).
|
||||
failed_at: String,
|
||||
}
|
||||
|
||||
/// Flags derived from filesystem state + config.
|
||||
/// `can_analyze` covers both first analysis and re-analysis — same precondition
|
||||
/// (all non-failed recordings transcribed, LLM configured, no analysis in
|
||||
@@ -379,6 +400,9 @@ struct CaseFlags {
|
||||
analyzing: bool,
|
||||
can_analyze: bool,
|
||||
llm_missing: bool,
|
||||
/// `Some` iff a previous LLM run failed AND no document exists yet.
|
||||
/// Drives the dead-end-recovery banner on the case page.
|
||||
analysis_failed: Option<FailureBanner>,
|
||||
}
|
||||
|
||||
async fn compute_flags(
|
||||
@@ -399,11 +423,26 @@ async fn compute_flags(
|
||||
|
||||
let analyzable = !analyzing && all_transcribed;
|
||||
|
||||
// Only surface the failure marker when neither a document nor an in-flight
|
||||
// analysis would otherwise occupy the same UI slot. Skips the FS read on
|
||||
// the success hot-path (document already on disk).
|
||||
let analysis_failed = if !has_document && !analyzing {
|
||||
auto_trigger::read_failure_marker(case_dir)
|
||||
.await
|
||||
.map(|m| FailureBanner {
|
||||
reason: m.reason,
|
||||
failed_at: m.failed_at,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
CaseFlags {
|
||||
has_document,
|
||||
analyzing,
|
||||
can_analyze: analyzable && llm_configured,
|
||||
llm_missing: analyzable && !llm_configured,
|
||||
analysis_failed,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -702,6 +741,7 @@ pub async fn handle_case_page(
|
||||
llm_missing: flags.llm_missing,
|
||||
analyzing: flags.analyzing,
|
||||
has_document: flags.has_document,
|
||||
analysis_failed: flags.analysis_failed,
|
||||
has_failed_recording,
|
||||
is_admin,
|
||||
is_closed,
|
||||
|
||||
Reference in New Issue
Block a user