From b57f44cc73643ebf8f6d5891b824d8db04b1f294 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 14 May 2026 02:20:21 +0000 Subject: [PATCH] =?UTF-8?q?ui:=20Job-Tabelle=20aufr=C3=A4umen=20=E2=80=94?= =?UTF-8?q?=20feste=20Spaltenbreiten,=20Download-Buttons,=20Fehler=20unter?= =?UTF-8?q?=20Status?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - table-layout: fixed mit definierten Spaltenbreiten (Status 110px, Downloads 280px) - Status-Pill bleibt kompakt (white-space: nowrap, nur Status-Name) - Fehlertext jetzt als eigene Zeile unter dem Pill, max. 3 Zeilen, mit Tooltip - Downloads als 2x2-Grid aus Pill-Buttons (DOCX / Protokoll / Summary / Transkript) - Disabled-Buttons grau und ohne href - HTML-Escape für Titel/Filename/Fehler --- lxc-frontend/app/static/app.js | 53 ++++++++++++++++++-------- lxc-frontend/app/static/index.html | 7 +++- lxc-frontend/app/static/style.css | 61 ++++++++++++++++++++++++++---- 3 files changed, 98 insertions(+), 23 deletions(-) diff --git a/lxc-frontend/app/static/app.js b/lxc-frontend/app/static/app.js index 3c79169..3e4c54b 100644 --- a/lxc-frontend/app/static/app.js +++ b/lxc-frontend/app/static/app.js @@ -32,27 +32,50 @@ function fmtDate(s) { return d.toLocaleString("de-DE", { dateStyle: "short", timeStyle: "medium" }); } +const DOWNLOADS = [ + { kind: "docx", label: "DOCX", title: "Sitzungsprotokoll als Word-Dokument" }, + { kind: "protocol", label: "Protokoll", title: "Strukturiertes Protokoll (JSON)" }, + { kind: "summary", label: "Summary", title: "Zusammenfassung (JSON)" }, + { kind: "transcript", label: "Transkript", title: "Rohes Transkript (JSON)" }, +]; + +function escapeHtml(s) { + return String(s ?? "").replace(/[&<>"']/g, (c) => ({ + "&": "&", "<": "<", ">": ">", '"': """, "'": "'", + }[c])); +} + function row(job) { const tr = document.createElement("tr"); const title = job.title || job.original_name; - const dl = (kind, label) => { - const a = document.createElement("a"); - a.textContent = label; - a.href = `/api/jobs/${job.id}/download/${kind}`; - if (!job.has[kind]) a.classList.add("disabled"); - return a; - }; + const dlCell = document.createElement("td"); dlCell.className = "dl"; - ["docx", "protocol", "summary", "transcript"].forEach((k) => - dlCell.appendChild(dl(k, k === "docx" ? "DOCX" : k.charAt(0).toUpperCase() + k.slice(1) + " (JSON)")) - ); + DOWNLOADS.forEach(({ kind, label, title: tip }) => { + const a = document.createElement("a"); + a.className = "btn-dl"; + a.textContent = label; + a.title = tip; + a.href = `/api/jobs/${job.id}/download/${kind}`; + if (!job.has[kind]) { + a.classList.add("disabled"); + a.removeAttribute("href"); + a.title = `${tip} — noch nicht verfügbar`; + } + dlCell.appendChild(a); + }); + + const statusCell = ` + ${escapeHtml(job.status)} + ${job.error ? `
${escapeHtml(job.error)}
` : ""} + `; + tr.innerHTML = ` - #${job.id} - ${title}
${job.original_name} - ${job.status}${job.error ? " — " + job.error : ""} -
${job.progress}% - ${fmtDate(job.updated_at)} + #${job.id} + ${escapeHtml(title)}
${escapeHtml(job.original_name)} + ${statusCell} +
${job.progress}% + ${fmtDate(job.updated_at)} `; tr.appendChild(dlCell); return tr; diff --git a/lxc-frontend/app/static/index.html b/lxc-frontend/app/static/index.html index af1f8ee..14ebb2c 100644 --- a/lxc-frontend/app/static/index.html +++ b/lxc-frontend/app/static/index.html @@ -39,7 +39,12 @@ - + + + + + + diff --git a/lxc-frontend/app/static/style.css b/lxc-frontend/app/static/style.css index 90d28d2..48c1e29 100644 --- a/lxc-frontend/app/static/style.css +++ b/lxc-frontend/app/static/style.css @@ -92,26 +92,73 @@ button:disabled { opacity: .6; cursor: not-allowed; } background: var(--accent); transition: width .25s ease; } -table { width: 100%; border-collapse: collapse; font-size: 14px; } -th, td { padding: 10px 8px; text-align: left; border-bottom: 1px solid var(--border); } +table { width: 100%; border-collapse: collapse; font-size: 14px; table-layout: fixed; } +th, td { padding: 10px 8px; text-align: left; border-bottom: 1px solid var(--border); vertical-align: top; } th { color: var(--muted); font-weight: 600; font-size: 12px; text-transform: uppercase; letter-spacing: .5px; } + +/* Spaltenbreiten — Status bleibt schmal, Downloads bekommen Platz */ +.col-id { width: 56px; } +.col-status { width: 110px; } +.col-progress { width: 130px; } +.col-updated { width: 130px; } +.col-dl { width: 280px; } +/* col-title nimmt den Rest */ + +.col-title { word-break: break-word; } + .status-pill { display: inline-block; padding: 2px 10px; border-radius: 999px; font-size: 12px; border: 1px solid var(--border); + white-space: nowrap; } .status-queued { color: var(--muted); } .status-transcribing, .status-summarizing, .status-protocolling, .status-exporting { color: var(--warn); border-color: rgba(241,196,15,.4); } .status-done { color: var(--ok); border-color: rgba(46,204,113,.4); } .status-failed { color: var(--err); border-color: rgba(231,76,60,.4); } -.dl a { + +/* Fehlertext unter dem Pill — bricht um, max. 3 Zeilen */ +.status-error { + margin-top: 6px; + font-size: 11px; + color: var(--err); + line-height: 1.35; + word-break: break-word; + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; +} + +/* Download-Buttons — sauberes 2×2-Grid, alle gleich breit */ +.dl { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 6px; +} +.btn-dl { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 5px 10px; + border-radius: 6px; + background: rgba(79,140,255,.12); + border: 1px solid rgba(79,140,255,.4); color: var(--accent); text-decoration: none; - margin-right: 10px; - font-size: 13px; + font-size: 12px; + font-weight: 500; + white-space: nowrap; + transition: background .15s ease, border-color .15s ease; +} +.btn-dl:hover { background: rgba(79,140,255,.22); border-color: var(--accent); } +.btn-dl.disabled { + color: var(--muted); + background: transparent; + border-color: var(--border); + pointer-events: none; + opacity: .55; } -.dl a:hover { text-decoration: underline; } -.dl .disabled { color: var(--muted); pointer-events: none; } footer { text-align: center; padding: 24px 16px 32px; }
#Titel / DateiStatusFortschrittAktualisiertDownloads#Titel / DateiStatusFortschrittAktualisiertDownloads