feat(analyze): structured LLM failure diagnostics + SSE reload on failure

Failure markers now carry finish_reason and usage tokens (prompt /
completion / reasoning) pulled from the chat-completions response, so
the case-page "Technische Details" block can show why an analysis
truncated — e.g. gpt-oss-120b hitting max_completion_tokens with the
chain-of-thought before producing any visible answer.

The worker also emits a new CaseEventKind::AnalysisFailed after writing
the marker, so subscribed browsers reload immediately instead of leaving
the page on the "Wird analysiert …" placeholder.
This commit is contained in:
2026-05-05 11:01:29 +02:00
parent a142725a8a
commit 44e93f08ee
8 changed files with 574 additions and 59 deletions
+49 -4
View File
@@ -201,6 +201,26 @@
font-size: 0.85em;
color: #4a1014;
}
.failure-banner dl.failure-details {
display: grid;
grid-template-columns: max-content 1fr;
gap: 0.2em 0.8em;
margin: 0.4em 0 0.6em 0;
padding: 0.5em 0.8em;
background: #fff;
border: 1px solid #f5c6cb;
border-radius: 3px;
font-size: 0.85em;
color: #4a1014;
}
.failure-banner dl.failure-details dt {
font-weight: 600;
color: #6b1f25;
}
.failure-banner dl.failure-details dd {
margin: 0;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
.failure-banner form {
margin: 0.8em 0 0 0;
}
@@ -609,10 +629,7 @@
</script>
{% when None %} {% match analysis_failed %} {% when Some with (failure) %}
<div class="failure-banner" role="alert">
<strong>
Analyse{% if !failure.backend_label.is_empty() %} mit {{ failure.backend_label }}{% endif %} fehlgeschlagen.
</strong>
Bitte erneut versuchen.
<strong>Analyse fehlgeschlagen.</strong>
<span class="failure-time"
>Zuletzt versucht:
<time datetime="{{ failure.failed_at }}"
@@ -621,6 +638,34 @@
>
<details>
<summary>Technische Details</summary>
<dl class="failure-details">
{% if !failure.backend_label.is_empty() %}
<dt>Backend</dt>
<dd>{{ failure.backend_label }}</dd>
{% endif %}
{% if !failure.model_id.is_empty() %}
<dt>Modell</dt>
<dd><code>{{ failure.model_id }}</code></dd>
{% endif %}
{% if !failure.finish_reason.is_empty() %}
<dt>finish_reason</dt>
<dd><code>{{ failure.finish_reason }}</code></dd>
{% endif %}
{% match failure.completion_tokens %}{% when Some with (ct) %}
<dt>Completion-Tokens</dt>
<dd>
{{ ct }}{% match failure.max_completion_tokens %}{% when Some with (cap) %} / {{ cap }}{% when None %}{% endmatch %}
</dd>
{% when None %}{% endmatch %}
{% match failure.reasoning_tokens %}{% when Some with (rt) %}
<dt>davon Reasoning</dt>
<dd>{{ rt }}</dd>
{% when None %}{% endmatch %}
{% match failure.prompt_tokens %}{% when Some with (pt) %}
<dt>Prompt-Tokens</dt>
<dd>{{ pt }}</dd>
{% when None %}{% endmatch %}
</dl>
<pre class="failure-reason">{{ failure.reason }}</pre>
</details>
<form