ui: Job-Tabelle aufräumen — feste Spaltenbreiten, Download-Buttons, Fehler unter Status

- 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
This commit is contained in:
root 2026-05-14 02:20:21 +00:00
parent 955075e3fd
commit b57f44cc73
3 changed files with 98 additions and 23 deletions

View File

@ -32,27 +32,50 @@ function fmtDate(s) {
return d.toLocaleString("de-DE", { dateStyle: "short", timeStyle: "medium" }); 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) => ({
"&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;",
}[c]));
}
function row(job) { function row(job) {
const tr = document.createElement("tr"); const tr = document.createElement("tr");
const title = job.title || job.original_name; 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"); const dlCell = document.createElement("td");
dlCell.className = "dl"; dlCell.className = "dl";
["docx", "protocol", "summary", "transcript"].forEach((k) => DOWNLOADS.forEach(({ kind, label, title: tip }) => {
dlCell.appendChild(dl(k, k === "docx" ? "DOCX" : k.charAt(0).toUpperCase() + k.slice(1) + " (JSON)")) 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 = `
<span class="status-pill status-${job.status}">${escapeHtml(job.status)}</span>
${job.error ? `<div class="status-error" title="${escapeHtml(job.error)}">${escapeHtml(job.error)}</div>` : ""}
`;
tr.innerHTML = ` tr.innerHTML = `
<td>#${job.id}</td> <td class="col-id">#${job.id}</td>
<td>${title}<br><small class="muted">${job.original_name}</small></td> <td class="col-title">${escapeHtml(title)}<br><small class="muted">${escapeHtml(job.original_name)}</small></td>
<td><span class="status-pill status-${job.status}">${job.status}${job.error ? " — " + job.error : ""}</span></td> <td class="col-status">${statusCell}</td>
<td><div class="bar"><div class="bar-fill" style="width:${job.progress}%"></div></div><small class="muted">${job.progress}%</small></td> <td class="col-progress"><div class="bar"><div class="bar-fill" style="width:${job.progress}%"></div></div><small class="muted">${job.progress}%</small></td>
<td><small class="muted">${fmtDate(job.updated_at)}</small></td> <td class="col-updated"><small class="muted">${fmtDate(job.updated_at)}</small></td>
`; `;
tr.appendChild(dlCell); tr.appendChild(dlCell);
return tr; return tr;

View File

@ -39,7 +39,12 @@
<table id="jobs-table"> <table id="jobs-table">
<thead> <thead>
<tr> <tr>
<th>#</th><th>Titel / Datei</th><th>Status</th><th>Fortschritt</th><th>Aktualisiert</th><th>Downloads</th> <th class="col-id">#</th>
<th class="col-title">Titel / Datei</th>
<th class="col-status">Status</th>
<th class="col-progress">Fortschritt</th>
<th class="col-updated">Aktualisiert</th>
<th class="col-dl">Downloads</th>
</tr> </tr>
</thead> </thead>
<tbody id="jobs-body"> <tbody id="jobs-body">

View File

@ -92,26 +92,73 @@ button:disabled { opacity: .6; cursor: not-allowed; }
background: var(--accent); background: var(--accent);
transition: width .25s ease; transition: width .25s ease;
} }
table { width: 100%; border-collapse: collapse; font-size: 14px; } 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); } 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; } 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 { .status-pill {
display: inline-block; display: inline-block;
padding: 2px 10px; padding: 2px 10px;
border-radius: 999px; border-radius: 999px;
font-size: 12px; font-size: 12px;
border: 1px solid var(--border); border: 1px solid var(--border);
white-space: nowrap;
} }
.status-queued { color: var(--muted); } .status-queued { color: var(--muted); }
.status-transcribing, .status-summarizing, .status-protocolling, .status-exporting { color: var(--warn); border-color: rgba(241,196,15,.4); } .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-done { color: var(--ok); border-color: rgba(46,204,113,.4); }
.status-failed { color: var(--err); border-color: rgba(231,76,60,.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); color: var(--accent);
text-decoration: none; text-decoration: none;
margin-right: 10px; font-size: 12px;
font-size: 13px; 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; } footer { text-align: center; padding: 24px 16px 32px; }