Files
doctate/server/templates/case_page.html
T
Brummel 1daf8db470 Refactor case action redirects and UI
Introduces a `resolve_return_path` function to determine the redirect
target after a case action. This function prioritizes the `Referer`
header for a more contextual redirect, falling back to the default case
list page if the header is absent or invalid.

Additionally, this commit refactors the UI elements for actions on the
case detail and case list pages, improving organization and clarity.
2026-04-20 10:39:21 +02:00

182 lines
8.9 KiB
HTML

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>Doctate — Fall {{ case_id_short }}</title>
<style>
body { font-family: sans-serif; max-width: 900px; margin: 2em auto; padding: 0 1em; }
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 .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; }
.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; }
.status-badge.done { background: #7ed321; }
.actions { margin: 1em 0 1.5em; display: flex; align-items: center; gap: 0.4em; }
.actions form { margin: 0; display: inline; }
.actions button { font-size: 1em; padding: 0.5em 1em; }
.actions .analyzing { color: #888; font-style: italic; }
.actions .llm-missing { color: #a66; font-style: italic; }
.actions:empty { display: none; }
.delete-btn { padding: 0.4em; background: transparent; border: none; color: #888; cursor: pointer; display: inline-flex; align-items: center; line-height: 0; border-radius: 4px; transition: color 0.15s, background 0.15s; font-size: 1em; }
.delete-btn:hover { color: #e24a4a; background: #fff0f0; }
.placeholder { color: #888; font-style: italic; margin: 1em 0; }
.status-panel { margin: 1em 0; padding: 0.8em 1em; background: #f6f6f6; border-radius: 4px; }
.recordings-link { display: inline-block; margin: 1em 0; color: #4a90e2; text-decoration: none; font-size: 0.95em; }
.recordings-link:hover { text-decoration: underline; }
.doc-content { position: relative; font-family: serif; font-size: 1.05em; line-height: 1.55; background: #f6f6f6; padding: 1em 1.2em; border-radius: 4px; }
.doc-content p { margin: 0.8em 0; }
.doc-content p:first-child { margin-top: 0; }
.doc-content p:last-child { margin-bottom: 0; }
.doc-content mark { background: #fff3a8; padding: 0 0.2em; border-radius: 2px; }
.doc-actions { position: absolute; bottom: 0.5em; right: 0.5em; display: flex; gap: 0.2em; }
.doc-actions form { margin: 0; }
.copy-btn { padding: 0.35em; background: transparent; border: none; border-radius: 4px; cursor: pointer; color: #888; display: inline-flex; align-items: center; justify-content: center; line-height: 0; opacity: 0.45; transition: opacity 0.15s, color 0.15s; }
.copy-btn:hover { color: #4a90e2; opacity: 1; }
.copy-btn .icon-check { display: none; }
.copy-btn.copied { color: #2d8c2d; opacity: 1; }
.copy-btn.copied .icon-copy { display: none; }
.copy-btn.copied .icon-check { display: block; }
.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; }
</style>
<script>
try {
if (localStorage.getItem('admin-view') === 'off') {
document.documentElement.classList.add('admin-view-off');
}
} catch (_) {}
</script>
</head>
<body>
<header>
<div><a class="back" href="/web/cases">&larr; Fälle</a></div>
<div class="header-right">
{% if is_admin %}<label class="admin-toggle"><input type="checkbox" id="admin-view-toggle"> Admin view</label>{% endif %}
<form method="post" action="/web/logout"><button type="submit">Logout</button></form>
</div>
</header>
<h1>
<span class="title">
{% match oneliner %}
{% when Some with (t) %}{{ t }}
{% when None %}Fall
{% endmatch %}
{% if has_document %}<span class="status-badge done">ausgewertet</span>{% else %}<span class="status-badge open">offen</span>{% endif %}
</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>
{% 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 %}
<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>
{% endif %}
{% if is_admin %}
<form class="admin-only" method="post" action="/web/cases/{{ case_id }}/reset"><button type="submit">Reset</button></form>
{% endif %}
</div>
{% match document_html %}
{% when Some with (html) %}
<div id="doc-content" class="doc-content">
<div class="doc-actions">
{% if can_analyze %}<form method="post" action="/web/cases/{{ case_id }}/analyze"><button type="submit" class="copy-btn" title="Neu analysieren" aria-label="Neu analysieren"><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="23 4 23 10 17 10"></polyline><polyline points="1 20 1 14 7 14"></polyline><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10"></path><path d="M20.49 15a9 9 0 0 1-14.85 3.36L1 14"></path></svg></button></form>{% endif %}
<button id="copy-btn" type="button" class="copy-btn" title="In Zwischenablage kopieren" hidden>
<svg class="icon-copy" 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"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>
<svg class="icon-check" 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="20 6 9 17 4 12"></polyline></svg>
</button>
</div>
<div id="doc-body">{{ html|safe }}</div>
</div>
<script>
(() => {
const btn = document.getElementById('copy-btn');
if (!btn) return;
btn.hidden = false;
btn.addEventListener('click', async () => {
const text = document.getElementById('doc-body').innerText;
let ok = false;
try {
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(text);
ok = true;
}
} catch (_) { /* fall through */ }
if (!ok) {
const ta = document.createElement('textarea');
ta.value = text;
ta.setAttribute('readonly', '');
ta.style.position = 'fixed';
ta.style.top = '-1000px';
document.body.appendChild(ta);
ta.select();
try { ok = document.execCommand('copy'); } catch (_) {}
ta.remove();
}
if (ok) {
btn.classList.add('copied');
setTimeout(() => btn.classList.remove('copied'), 1500);
}
});
})();
</script>
{% when None %}
{% if analyzing %}
<div class="placeholder">Wird analysiert …</div>
{% else if recordings_count == 0 %}
<div class="placeholder">Noch keine Aufnahmen vorhanden.</div>
{% else %}
<div class="status-panel">
{{ recordings_count }} Aufnahme{% if recordings_count != 1 %}n{% endif %}, davon {{ transcribed_count }} transkribiert.
</div>
{% endif %}
{% endmatch %}
<div>
<a class="recordings-link" href="/web/cases/{{ case_id }}/recordings">&rarr; Aufnahmen anzeigen ({{ recordings_count }})</a>
</div>
<script>
(() => {
const cb = document.getElementById('admin-view-toggle');
if (!cb) return;
cb.checked = !document.documentElement.classList.contains('admin-view-off');
cb.addEventListener('change', () => {
const on = cb.checked;
try { localStorage.setItem('admin-view', on ? 'on' : 'off'); } catch (_) {}
document.documentElement.classList.toggle('admin-view-off', !on);
});
})();
// 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();
});
})();
</script>
</body>
</html>