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 @@
- | # | Titel / Datei | Status | Fortschritt | Aktualisiert | Downloads |
+ # |
+ Titel / Datei |
+ Status |
+ Fortschritt |
+ Aktualisiert |
+ Downloads |
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; }