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) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'", }[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 = ` ${escapeHtml(job.status)} ${job.error ? `
${escapeHtml(job.error)}
` : ""} `; tr.innerHTML = ` #${job.id} ${escapeHtml(title)}
${escapeHtml(job.original_name)} ${statusCell}
${job.progress}% ${fmtDate(job.updated_at)} `; 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 = 'Noch keine Jobs.'; 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);