From 75c89c132fcd13a2c612ba6274cefd8c2af47723 Mon Sep 17 00:00:00 2001 From: Brummel Date: Mon, 20 Apr 2026 14:44:05 +0200 Subject: [PATCH] Refactor case page template and oneliner rendering Introduced a new macro `render` in `partials/oneliner.html` to handle the display of the oneliner for cases. This macro encapsulates the logic for rendering the oneliner, including states for ready, empty, error, pending, and generating. The `case_page.html` and `my_cases.html` templates are updated to use this new macro. This refactoring centralizes the oneliner rendering logic, making it more maintainable and consistent across different views. Additionally, the `OnelinerDisplay` enum is now used in `CasePageTemplate` and `MyCasesTemplate` to represent the different states of the oneliner, improving type safety and clarity. --- server/src/routes/case_actions.rs | 3 +- server/src/routes/user_web.rs | 83 +-- server/templates/case_page.html | 702 +++++++++++++++++------- server/templates/my_cases.html | 3 +- server/templates/partials/oneliner.html | 12 + 5 files changed, 578 insertions(+), 225 deletions(-) create mode 100644 server/templates/partials/oneliner.html diff --git a/server/src/routes/case_actions.rs b/server/src/routes/case_actions.rs index 01c0390..78f36d5 100644 --- a/server/src/routes/case_actions.rs +++ b/server/src/routes/case_actions.rs @@ -324,6 +324,7 @@ pub async fn handle_reset_case( State(config): State>, State(events_tx): State, CaseIdPath(case_id): CaseIdPath, + headers: HeaderMap, ) -> Result { if !user.is_admin() { return Err(AppError::Forbidden("Nur für Admins".into())); @@ -343,7 +344,7 @@ pub async fn handle_reset_case( case_id.to_string(), CaseEventKind::CaseReset, ); - Ok(Redirect::to("/web/cases")) + Ok(Redirect::to(&resolve_return_path(&headers))) } /// POST /web/cases/undo-delete diff --git a/server/src/routes/user_web.rs b/server/src/routes/user_web.rs index 3048a85..de91693 100644 --- a/server/src/routes/user_web.rs +++ b/server/src/routes/user_web.rs @@ -163,7 +163,7 @@ struct MyCasesTemplate { struct CasePageTemplate { case_id: String, case_id_short: String, - oneliner: Option, + oneliner: OnelinerDisplay, /// RFC3339 UTC timestamp of the most recent recording. `None` iff the /// case has no recordings at all. Drives the datetime header under the /// title; browser JS formats it to "Heute 11:34" / "13.12.2022 11:34". @@ -337,7 +337,7 @@ pub async fn handle_case_page( .map(|md| crate::analyze::render::md_to_html(&md)); let recordings = scan_recordings(&case_dir).await; - let oneliner = ready_text(&case_dir).await; + let oneliner = compute_oneliner_display(&case_dir, &recordings).await; let a_busy = pipeline.analyze_busy.0.load(Ordering::Acquire); let flags = compute_flags(&case_dir, &recordings, config.llm_configured(), a_busy).await; @@ -531,8 +531,54 @@ async fn compute_case_view( .filter(|r| !r.failed) .all(|r| r.transcript.is_some()); - let (state, _) = paths::read_oneliner_state(case_path).await; - let oneliner = if non_failed_count == 0 { + let oneliner = compute_oneliner_display(case_path, &recordings).await; + + let has_document = any_document_exists(case_path).await; + let analyzing = !has_document && worker_busy && any_analysis_input_exists(case_path).await; + let can_analyze = !analyzing && all_transcribed && llm_configured; + + let time_hms_utc = extract_hhmm_utc(&most_recent); + let recorded_at_iso = recorded_at_iso_of(&most_recent); + Some(UserCaseView { + case_id, + most_recent, + time_hms_utc, + recorded_at_iso, + oneliner, + recordings_count, + analyzing, + has_document, + can_analyze, + }) +} + +/// Extract only the text from a `Ready` state; other states (Empty, +/// Error, missing file) collapse to `None`. Used by the recordings +/// sub-page, which only needs a plain title string as heading. +async fn ready_text(case_dir: &Path) -> Option { + match paths::read_oneliner_state(case_dir).await.0 { + Some(OnelinerState::Ready { text, .. }) => Some(text), + _ => None, + } +} + +/// Compute the 5-state UI display for the oneliner. Identical logic to +/// what `compute_case_view` uses for the case list — extracted so the +/// case page can render the same states instead of a plain "Fall" +/// fallback. +async fn compute_oneliner_display( + case_dir: &Path, + recordings: &[RecordingView], +) -> OnelinerDisplay { + let non_failed_count = recordings.iter().filter(|r| !r.failed).count(); + let all_transcribed = non_failed_count > 0 + && recordings + .iter() + .filter(|r| !r.failed) + .all(|r| r.transcript.is_some()); + let (state, _) = paths::read_oneliner_state(case_dir).await; + + if non_failed_count == 0 { // All recordings failed — stable end state, no LLM will ever run. // Missing state collapses to Empty ("unbenannt"): no title is // expected, regardless of why. @@ -558,34 +604,5 @@ async fn compute_case_view( None => OnelinerDisplay::Pending, Some(_) => OnelinerDisplay::Generating, } - }; - - let has_document = any_document_exists(case_path).await; - let analyzing = !has_document && worker_busy && any_analysis_input_exists(case_path).await; - let can_analyze = !analyzing && all_transcribed && llm_configured; - - let time_hms_utc = extract_hhmm_utc(&most_recent); - let recorded_at_iso = recorded_at_iso_of(&most_recent); - Some(UserCaseView { - case_id, - most_recent, - time_hms_utc, - recorded_at_iso, - oneliner, - recordings_count, - analyzing, - has_document, - can_analyze, - }) -} - -/// Extract only the text from a `Ready` state; other states (Empty, -/// Error, missing file) collapse to `None`. Used by single-case pages -/// whose headings simply fall back to a generic placeholder — they do -/// not need the full five-state UI treatment. -async fn ready_text(case_dir: &Path) -> Option { - match paths::read_oneliner_state(case_dir).await.0 { - Some(OnelinerState::Ready { text, .. }) => Some(text), - _ => None, } } diff --git a/server/templates/case_page.html b/server/templates/case_page.html index 8e64837..3a05c4f 100644 --- a/server/templates/case_page.html +++ b/server/templates/case_page.html @@ -1,198 +1,520 @@ - +{%- import "partials/oneliner.html" as ol -%} + - - -Doctate — Fall {{ case_id_short }} - - - - -
- -
-{% if is_admin %}{% endif %} -
-
-
-

- -{% match oneliner %} -{% when Some with (t) %}{{ t }} -{% when None %}Fall -{% endmatch %} -{% if has_document %}ausgewertet{% else %}offen{% endif %} - -
-

-{% match recorded_at_iso %} -{% when Some with (iso) %}

-{% when None %} -{% endmatch %} -{% if is_admin %}
{{ case_id }}
{% endif %} + .header-right { + display: flex; + align-items: center; + gap: 0.8em; + } + .admin-toggle { + font-size: 0.85em; + color: #666; + display: inline-flex; + align-items: center; + gap: 0.3em; + cursor: pointer; + user-select: none; + } + html.admin-view-off .admin-only { + display: none !important; + } + + + + +
+ +
+ {% if is_admin %}{% endif %} +
+ +
+
+
+

+ + {% call ol::render(oneliner) %} {% if has_document %}ausgewertet{% else %}offen{% endif + %} + +
+ +
+

+ {% match recorded_at_iso %} {% when Some with (iso) %} +

+ +

+ {% when None %} {% endmatch %} {% if is_admin %} +
{{ case_id }}
+ {% endif %} -
-{% if can_analyze && !has_document %} -
-{% else if llm_missing && !has_document %} -LLM-Analyse nicht konfiguriert -{% endif %} -{% if is_admin %} -
-{% endif %} -
+
+ {% if can_analyze && !has_document %} +
+ +
+ {% else if llm_missing && !has_document %} + LLM-Analyse nicht konfiguriert + {% endif %} {% if is_admin %} +
+ +
+ {% endif %} +
-{% match document_html %} -{% when Some with (html) %} -
-
-{% if can_analyze %}
{% endif %} - -
-
{{ html|safe }}
-
- -{% when None %} -{% if analyzing %} -
Wird analysiert …
-{% else if recordings_count == 0 %} -
Noch keine Aufnahmen vorhanden.
-{% else %} -
-{{ recordings_count }} Aufnahme{% if recordings_count != 1 %}n{% endif %}, davon {{ transcribed_count }} transkribiert. -
-{% endif %} -{% endmatch %} + {% match document_html %} {% when Some with (html) %} +
+ {% if can_analyze %} +
+
+ +
+
+ {% endif %} +
+ +
+
{{ html|safe }}
+
+ + {% when None %} {% if analyzing %} +
Wird analysiert …
+ {% else if recordings_count == 0 %} +
Noch keine Aufnahmen vorhanden.
+ {% else %} +
+ {{ recordings_count }} Aufnahme{% if recordings_count != 1 %}n{% + endif %}, davon {{ transcribed_count }} transkribiert. +
+ {% endif %} {% endmatch %} - - - + - + // Live-update bus: only events for THIS case trigger a debounced reload. + (() => { + const MY_CASE_ID = "{{ case_id }}"; + let timer = null; + const scheduleReload = () => { + clearTimeout(timer); + timer = setTimeout(() => location.reload(), 300); + }; + const es = new EventSource("/web/events"); + // Release the socket-pool slot on navigation. Without this, Chrome + // keeps the SSE connection "draining" after unload; six rapid nav + // cycles exhaust the 6-per-origin pool and stall further requests. + window.addEventListener("pagehide", () => es.close()); + es.addEventListener("case", (msg) => { + let evt; + try { + evt = JSON.parse(msg.data); + } catch (_) { + return; + } + if (evt.case_id === MY_CASE_ID) scheduleReload(); + }); + })(); + + diff --git a/server/templates/my_cases.html b/server/templates/my_cases.html index 4f5a817..72d8a90 100644 --- a/server/templates/my_cases.html +++ b/server/templates/my_cases.html @@ -1,3 +1,4 @@ +{%- import "partials/oneliner.html" as ol -%} @@ -86,7 +87,7 @@ try {
-
{% match case.oneliner %}{% when OnelinerDisplay::Ready with (t) %} — {{ t }}{% when OnelinerDisplay::Empty %} — unbenannt{% when OnelinerDisplay::Error %} — Fehler{% when OnelinerDisplay::Pending %} — transkribiere …{% when OnelinerDisplay::Generating %} — generiere Titel …{% endmatch %}
+
— {% call ol::render(case.oneliner) %}
{{ case.recordings_count }} Aufnahmen{% if case.has_document %} — ausgewertet{% else %} — offen{% endif %}{% if case.analyzing %} wird analysiert{% endif %}
{% if is_admin %}
{{ case.case_id }}
{% endif %}
diff --git a/server/templates/partials/oneliner.html b/server/templates/partials/oneliner.html new file mode 100644 index 0000000..0e5d83c --- /dev/null +++ b/server/templates/partials/oneliner.html @@ -0,0 +1,12 @@ +{#- Shared 5-state oneliner renderer. Emits the inline content only (no + prefix/suffix), so callers can wrap it in their own context — a + " — " separator in the case list, an

span on the case page. -#} +{% macro render(ol) -%} +{% match ol -%} +{% when OnelinerDisplay::Ready with (t) %}{{ t }} +{%- when OnelinerDisplay::Empty %}unbenannt +{%- when OnelinerDisplay::Error %}Fehler +{%- when OnelinerDisplay::Pending %}transkribiere … +{%- when OnelinerDisplay::Generating %}generiere Titel … +{%- endmatch %} +{%- endmacro %}