// ─── DOM-Refs ──────────────────────────────────────────────────────────────
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");
const appMain = document.getElementById("app");
const authOverlay = document.getElementById("auth-overlay");
const authTitle = document.getElementById("auth-title");
const authInfo = document.getElementById("auth-info");
const authForm = document.getElementById("auth-form");
const authUsername = document.getElementById("auth-username");
const authPassword = document.getElementById("auth-password");
const authSubmit = document.getElementById("auth-submit");
const authMsg = document.getElementById("auth-msg");
const userBar = document.getElementById("user-bar");
const userName = document.getElementById("user-name");
const userAdminBadge = document.getElementById("user-admin-badge");
const logoutBtn = document.getElementById("logout-btn");
const adminCard = document.getElementById("admin-card");
const userForm = document.getElementById("user-form");
const newUsername = document.getElementById("new-username");
const newPassword = document.getElementById("new-password");
const newIsAdmin = document.getElementById("new-is-admin");
const userMsg = document.getElementById("user-msg");
const usersBody = document.getElementById("users-body");
const profileLabel = document.getElementById("profile-label");
const profileSelect = document.getElementById("profile-select");
const defaultProfileName = document.getElementById("default-profile-name");
const headerProfileWrap = document.getElementById("header-profile-wrap");
const headerProfile = document.getElementById("header-profile");
let currentUser = null; // { id, username, is_admin, default_profile }
let mode = "login"; // "login" | "setup"
let availableProfiles = []; // [{ name, display_name, language }, ...]
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)" },
];
// ─── Helpers ───────────────────────────────────────────────────────────────
function escapeHtml(s) {
return String(s ?? "").replace(/[&<>"']/g, (c) => ({
"&": "&", "<": "<", ">": ">", '"': """, "'": "'",
}[c]));
}
function fmtDate(s) {
const d = new Date(s);
return d.toLocaleString("de-DE", { dateStyle: "short", timeStyle: "medium" });
}
async function api(path, opts = {}) {
const r = await fetch(path, {
credentials: "same-origin",
headers: { "Content-Type": "application/json", ...(opts.headers || {}) },
...opts,
});
return r;
}
// ─── Auth-Flow ─────────────────────────────────────────────────────────────
async function bootstrap() {
const me = await api("/api/auth/me");
if (me.ok) {
currentUser = await me.json();
enterApp();
return;
}
// Nicht eingeloggt — Setup nötig?
const setupCheck = await api("/api/auth/needs-setup");
const setupData = await setupCheck.json();
if (setupData.needs_setup) {
showAuth("setup");
} else {
showAuth("login");
}
}
function showAuth(which) {
mode = which;
authMsg.textContent = "";
authMsg.className = "msg";
authForm.reset();
if (which === "setup") {
authTitle.textContent = "Initialen Admin anlegen";
authInfo.textContent =
"Noch kein Benutzer vorhanden. Lege jetzt den ersten Admin an. Bestehende Jobs werden diesem Konto zugeordnet.";
authInfo.classList.remove("hidden");
authPassword.autocomplete = "new-password";
authPassword.minLength = 8;
authSubmit.textContent = "Admin anlegen";
} else {
authTitle.textContent = "Anmelden";
authInfo.classList.add("hidden");
authInfo.textContent = "";
authPassword.autocomplete = "current-password";
authPassword.minLength = 1;
authSubmit.textContent = "Anmelden";
}
authOverlay.classList.remove("hidden");
appMain.classList.add("hidden");
userBar.classList.add("hidden");
setTimeout(() => authUsername.focus(), 50);
}
function enterApp() {
authOverlay.classList.add("hidden");
appMain.classList.remove("hidden");
userBar.classList.remove("hidden");
userName.textContent = currentUser.username;
userAdminBadge.classList.toggle("hidden", !currentUser.is_admin);
adminCard.classList.toggle("hidden", !currentUser.is_admin);
loadProfiles().then(applyProfileUI);
loadJobs();
if (currentUser.is_admin) loadUsers();
}
async function loadProfiles() {
try {
const r = await api("/api/profiles");
if (!r.ok) return;
const data = await r.json();
availableProfiles = data.profiles || [];
} catch (e) {
console.error("Profile laden fehlgeschlagen", e);
availableProfiles = [];
}
}
function profileLabelOf(name) {
const p = availableProfiles.find((x) => x.name === name);
return p ? p.display_name : name;
}
function applyProfileUI() {
// Upload-Dropdown und Header-Selector nur sichtbar wenn ≥2 Profile.
const showDropdown = availableProfiles.length > 1;
profileLabel.classList.toggle("hidden", !showDropdown);
headerProfileWrap.classList.toggle("hidden", !showDropdown);
for (const sel of [profileSelect, headerProfile]) {
sel.innerHTML = "";
availableProfiles.forEach((p) => {
const o = document.createElement("option");
o.value = p.name;
o.textContent = p.display_name;
sel.appendChild(o);
});
}
const def = currentUser.default_profile || "meeting";
profileSelect.value = def;
headerProfile.value = def;
defaultProfileName.textContent = profileLabelOf(def);
}
// Sofort-Speichern, sobald der User im Header das Standard-Profil ändert.
headerProfile.addEventListener("change", async () => {
const newDefault = headerProfile.value;
headerProfile.disabled = true;
try {
const r = await api("/api/auth/me", {
method: "PATCH",
body: JSON.stringify({ default_profile: newDefault }),
});
if (!r.ok) throw new Error("save failed");
currentUser = await r.json();
// Upload-Dropdown synchron mitziehen
profileSelect.value = currentUser.default_profile;
defaultProfileName.textContent = profileLabelOf(currentUser.default_profile);
} catch (e) {
alert("Konnte Standard-Profil nicht speichern.");
headerProfile.value = currentUser.default_profile || "meeting";
} finally {
headerProfile.disabled = false;
}
});
authForm.addEventListener("submit", async (ev) => {
ev.preventDefault();
authSubmit.disabled = true;
authMsg.textContent = "";
authMsg.className = "msg";
const body = JSON.stringify({
username: authUsername.value.trim(),
password: authPassword.value,
});
const url = mode === "setup" ? "/api/auth/setup" : "/api/auth/login";
try {
const r = await api(url, { method: "POST", body });
const data = await r.json();
if (!r.ok) {
authMsg.textContent = data.detail || "Fehler bei der Anmeldung";
authMsg.className = "msg err";
return;
}
currentUser = data;
if (mode === "setup" && typeof data.migrated_jobs === "number" && data.migrated_jobs > 0) {
console.info(`${data.migrated_jobs} Bestand-Jobs wurden zugeordnet.`);
}
enterApp();
} catch (e) {
authMsg.textContent = "Netzwerkfehler";
authMsg.className = "msg err";
} finally {
authSubmit.disabled = false;
}
});
logoutBtn.addEventListener("click", async () => {
await api("/api/auth/logout", { method: "POST" });
currentUser = null;
jobsBody.innerHTML = "";
showAuth("login");
});
// ─── 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");
}
}
// ─── Jobs ──────────────────────────────────────────────────────────────────
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 ownerLine = currentUser?.is_admin && job.owner_username
? `
von ${escapeHtml(job.owner_username)}`
: "";
// Profile-Tag nur sichtbar wenn ≥2 Profile in der Auswahl ODER es ein anderes als "meeting" ist.
const showProfileTag = availableProfiles.length > 1 || (job.profile && job.profile !== "meeting");
const profileTag = showProfileTag
? `${escapeHtml(profileLabelOf(job.profile || "meeting"))}`
: "";
const statusCell = `
${escapeHtml(job.status)}
${job.error ? `