- 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
149 lines
4.8 KiB
JavaScript
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) => ({
|
|
"&": "&", "<": "<", ">": ">", '"': """, "'": "'",
|
|
}[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);
|