root b57f44cc73 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
2026-05-14 02:20:21 +00:00

149 lines
4.8 KiB
JavaScript

const form = document.getElementById("upload-form");
const titleInput = document.getElementById("title");
const audioInput = document.getElementById("audio");
const submitBtn = document.getElementById("submit-btn");
const uploadProgress = document.getElementById("upload-progress");
const uploadBar = uploadProgress.querySelector(".bar-fill");
const uploadMsg = document.getElementById("upload-msg");
const jobsBody = document.getElementById("jobs-body");
const macStatus = document.getElementById("mac-status");
async function checkMac() {
try {
const r = await fetch("/api/mac/health");
const j = await r.json();
if (r.ok && j.reachable) {
macStatus.textContent = "Mac-Backend: online";
macStatus.classList.add("ok");
macStatus.classList.remove("err");
} else {
macStatus.textContent = "Mac-Backend: nicht erreichbar";
macStatus.classList.add("err");
macStatus.classList.remove("ok");
}
} catch {
macStatus.textContent = "Mac-Backend: Fehler";
macStatus.classList.add("err");
}
}
function fmtDate(s) {
const d = new Date(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) => ({
"&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;",
}[c]));
}
function row(job) {
const tr = document.createElement("tr");
const title = job.title || job.original_name;
const dlCell = document.createElement("td");
dlCell.className = "dl";
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 = `
<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 = `
<td class="col-id">#${job.id}</td>
<td class="col-title">${escapeHtml(title)}<br><small class="muted">${escapeHtml(job.original_name)}</small></td>
<td class="col-status">${statusCell}</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 class="col-updated"><small class="muted">${fmtDate(job.updated_at)}</small></td>
`;
tr.appendChild(dlCell);
return tr;
}
async function loadJobs() {
try {
const r = await fetch("/api/jobs");
const jobs = await r.json();
jobsBody.innerHTML = "";
if (!jobs.length) {
jobsBody.innerHTML = '<tr><td colspan="6" class="muted">Noch keine Jobs.</td></tr>';
return;
}
jobs.forEach((j) => jobsBody.appendChild(row(j)));
} catch (e) {
console.error(e);
}
}
form.addEventListener("submit", (ev) => {
ev.preventDefault();
if (!audioInput.files.length) return;
const fd = new FormData();
fd.append("audio", audioInput.files[0]);
fd.append("title", titleInput.value);
submitBtn.disabled = true;
uploadProgress.classList.remove("hidden");
uploadBar.style.width = "0%";
uploadMsg.textContent = "";
uploadMsg.className = "msg";
const xhr = new XMLHttpRequest();
xhr.open("POST", "/api/jobs");
xhr.upload.onprogress = (e) => {
if (e.lengthComputable) {
const p = Math.round((e.loaded / e.total) * 100);
uploadBar.style.width = p + "%";
}
};
xhr.onload = () => {
submitBtn.disabled = false;
uploadProgress.classList.add("hidden");
if (xhr.status >= 200 && xhr.status < 300) {
const j = JSON.parse(xhr.responseText);
uploadMsg.textContent = `Job #${j.id} erstellt. Verarbeitung läuft …`;
uploadMsg.className = "msg ok";
form.reset();
loadJobs();
} else {
let err = "Upload fehlgeschlagen";
try { err = JSON.parse(xhr.responseText).detail || err; } catch {}
uploadMsg.textContent = err;
uploadMsg.className = "msg err";
}
};
xhr.onerror = () => {
submitBtn.disabled = false;
uploadProgress.classList.add("hidden");
uploadMsg.textContent = "Netzwerkfehler beim Upload";
uploadMsg.className = "msg err";
};
xhr.send(fd);
});
checkMac();
loadJobs();
setInterval(loadJobs, 3000);
setInterval(checkMac, 15000);