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:
parent
955075e3fd
commit
b57f44cc73
@ -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) => ({
|
||||||
|
"&": "&", "<": "<", ">": ">", '"': """, "'": "'",
|
||||||
|
}[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;
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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; }
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user