feat: Display most recent recording time

Add `recorded_at_iso` and `time_hms_utc` to `CasePageTemplate` to
display the timestamp of the most recent recording.
Introduce a new partial `partials/time_format.js` for consistent date
and time formatting in the browser.
Update templates to use the new partial for rendering timestamps and
group headings.
This commit is contained in:
2026-04-20 10:51:01 +02:00
parent 1daf8db470
commit e795bdf6a5
4 changed files with 75 additions and 32 deletions
+12
View File
@@ -134,6 +134,12 @@ struct CasePageTemplate {
case_id: String,
case_id_short: String,
oneliner: Option<String>,
/// 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".
recorded_at_iso: Option<String>,
/// UTC `HH:MM` as a no-JS fallback inside the `<time>` element.
time_hms_utc: String,
/// Rendered document HTML (already passed through `md_to_html`). `Some`
/// iff `document.md` exists at render time — template prioritises this
/// over status/analyzing placeholders.
@@ -315,10 +321,16 @@ pub async fn handle_case_page(
let case_id_short = case_id_str.chars().take(8).collect();
let is_admin = user.is_admin();
let most_recent = recordings.last().map(|r| r.filename.as_str());
let recorded_at_iso = most_recent.map(recorded_at_iso_of).filter(|s| !s.is_empty());
let time_hms_utc = most_recent.map(extract_hhmm_utc).unwrap_or_default();
CasePageTemplate {
case_id: case_id_str,
case_id_short,
oneliner,
recorded_at_iso,
time_hms_utc,
document_html,
recordings_count,
transcribed_count,
+21 -4
View File
@@ -8,9 +8,10 @@ body { font-family: sans-serif; max-width: 900px; margin: 2em auto; padding: 0 1
header { display: flex; justify-content: space-between; align-items: center; }
header form { margin: 0; }
.back { color: #4a90e2; text-decoration: none; }
h1 { display: flex; align-items: center; gap: 0.6em; flex-wrap: wrap; }
h1 { display: flex; align-items: center; gap: 0.6em; flex-wrap: wrap; margin-bottom: 0.2em; }
h1 .title { display: flex; align-items: center; gap: 0.6em; flex-wrap: wrap; flex: 1; min-width: 0; }
h1 .delete-form { margin: 0 0 0 auto; }
.case-time { color: #666; font-size: 1.05em; margin: 0 0 1em; }
.meta { color: #999; font-family: monospace; font-size: 0.8em; margin: 0 0 1em; }
.status-badge { display: inline-block; padding: 0.1em 0.6em; border-radius: 999px; font-size: 0.6em; font-weight: bold; color: white; vertical-align: middle; }
.status-badge.open { background: #4a90e2; }
@@ -71,12 +72,14 @@ try {
</span>
<form class="delete-form" method="post" action="/web/cases/{{ case_id }}/delete"><button type="submit" class="delete-btn" title="Fall entfernen" aria-label="Fall entfernen"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"></path><path d="M10 11v6"></path><path d="M14 11v6"></path><path d="M9 6V4a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2"></path></svg></button></form>
</h1>
{% match recorded_at_iso %}
{% when Some with (iso) %}<p class="case-time"><time datetime="{{ iso }}">{{ time_hms_utc }}</time></p>
{% when None %}
{% endmatch %}
{% if is_admin %}<div class="meta admin-only">{{ case_id }}</div>{% endif %}
<div class="actions">
{% if analyzing %}
<span class="analyzing">wird analysiert …</span>
{% else if can_analyze && !has_document %}
{% if can_analyze && !has_document %}
<form method="post" action="/web/cases/{{ case_id }}/analyze"><button type="submit">Analysieren</button></form>
{% else if llm_missing && !has_document %}
<span class="llm-missing">LLM-Analyse nicht konfiguriert</span>
@@ -146,6 +149,20 @@ try {
<a class="recordings-link" href="/web/cases/{{ case_id }}/recordings">&rarr; Aufnahmen anzeigen ({{ recordings_count }})</a>
</div>
<script>
{% include "partials/time_format.js" %}
</script>
<script>
// Format the case timestamp as "Heute HH:MM" / "Gestern HH:MM" / "DD.MM.YYYY HH:MM"
// in the browser's local timezone and locale. Server-rendered text is only a
// no-JS fallback (UTC HH:MM).
(() => {
const el = document.querySelector('.case-time time[datetime]');
if (!el) return;
const d = new Date(el.dateTime);
if (isNaN(d.getTime())) return;
el.textContent = TimeFmt.dateLabel(d) + ' - ' + TimeFmt.timeLabel(d);
})();
(() => {
const cb = document.getElementById('admin-view-toggle');
if (!cb) return;
+15 -28
View File
@@ -8,7 +8,8 @@ body { font-family: sans-serif; max-width: 900px; margin: 2em auto; padding: 0 1
header { display: flex; justify-content: space-between; align-items: center; }
header form { margin: 0; }
section { margin: 1.5em 0; }
section h2 { font-size: 1.1em; color: #555; border-bottom: 1px solid #ddd; padding-bottom: 0.2em; }
section h2 { font-size: 1.1em; color: #555; border-bottom: 1px solid #ddd; padding-bottom: 0.2em; display: flex; justify-content: space-between; align-items: baseline; gap: 1em; }
section h2 .count { font-weight: normal; font-size: 0.85em; color: #888; }
.empty { color: #888; font-style: italic; }
.case-list { list-style: none; padding: 0; margin: 0; }
@@ -77,7 +78,7 @@ try {
<form method="post" action="/web/cases/bulk">
{% for g in groups %}
<section>
<h2 class="date-group" data-iso="{{ g.label }}">{{ g.label }} ({{ g.cases.len() }})</h2>
<h2 class="date-group" data-iso="{{ g.label }}"><span class="date-label">{{ g.label }}</span><span class="count">{% if g.cases.len() == 1 %}1 Fall{% else %}{{ g.cases.len() }} Fälle{% endif %}</span></h2>
<ul class="case-list">
{% for case in g.cases %}
<li class="case-row {% if case.has_document %}done{% else %}open{% endif %}">
@@ -109,35 +110,21 @@ try {
</form>
{% endif %}
<script>
{% include "partials/time_format.js" %}
</script>
<script>
// Render all UTC timestamps in the browser's local timezone, and relabel
// date-group headings to Heute/Gestern based on the browser's current
// UTC date. Server-side renders only UTC so timezone logic belongs here.
(function () {
document.querySelectorAll('time[datetime]').forEach(function (el) {
var d = new Date(el.dateTime);
if (!isNaN(d.getTime())) {
el.textContent = d.toLocaleTimeString(undefined, {
hour: '2-digit',
minute: '2-digit',
hour12: false,
// date-group headings to Heute/Gestern/DD.MM.YYYY.
(() => {
document.querySelectorAll('time[datetime]').forEach((el) => {
const d = new Date(el.dateTime);
if (!isNaN(d.getTime())) el.textContent = TimeFmt.timeLabel(d);
});
}
});
var todayISO = new Date().toISOString().slice(0, 10);
var yesterday = new Date();
yesterday.setUTCDate(yesterday.getUTCDate() - 1);
var yesterdayISO = yesterday.toISOString().slice(0, 10);
document.querySelectorAll('h2.date-group').forEach(function (h) {
var iso = h.dataset.iso;
document.querySelectorAll('h2.date-group').forEach((h) => {
const iso = h.dataset.iso;
if (!iso) return;
var suffix = h.textContent.replace(iso, '').trim();
var label;
if (iso === todayISO) label = 'Heute';
else if (iso === yesterdayISO) label = 'Gestern';
else label = iso;
h.textContent = label + ' ' + suffix;
const labelEl = h.querySelector('.date-label');
if (labelEl) labelEl.textContent = TimeFmt.dateLabelFromISO(iso);
});
})();
+27
View File
@@ -0,0 +1,27 @@
// Inline helper module for timezone- and locale-aware date/time labels.
// Included verbatim by templates; exposes `window.TimeFmt`.
//
// Heute/Gestern is decided in UTC (same as server-side grouping) to keep
// client and server in lockstep. Locale date format follows the browser.
(() => {
const utcDayISO = (d) => d.toISOString().slice(0, 10);
const todayISO = () => utcDayISO(new Date());
const yesterdayISO = () => {
const y = new Date();
y.setUTCDate(y.getUTCDate() - 1);
return utcDayISO(y);
};
const dateLabelFromISO = (iso) => {
if (iso === todayISO()) return 'Heute';
if (iso === yesterdayISO()) return 'Gestern';
const parts = iso.split('-');
if (parts.length !== 3) return iso;
return new Date(Date.UTC(+parts[0], +parts[1] - 1, +parts[2]))
.toLocaleDateString();
};
const dateLabel = (d) => dateLabelFromISO(utcDayISO(d));
const timeLabel = (d) => d.toLocaleTimeString(undefined, {
hour: '2-digit', minute: '2-digit', hour12: false,
});
window.TimeFmt = { dateLabelFromISO, dateLabel, timeLabel };
})();