const columns = [ { key: "offen", state: "Backlog", label: "Offen", color: "var(--st-queue)" }, { key: "inProgress", state: "In progress", label: "In Bearbeitung", color: "var(--st-work)" }, { key: "review", state: "Review", label: "Review", color: "#fb923c" }, { key: "done", state: "Done", label: "Erledigt", color: "var(--st-work)" }, { key: "blocked", state: "Blocked", label: "Blockiert", color: "var(--st-block)", blocked: true } ]; const states = columns.map((column) => column.state); const priorities = ["High", "Medium", "Low"]; const assignees = [ { value: "bao", label: "Bao" }, { value: "iris", label: "Iris" }, { value: "maxi", label: "Maxi" } ]; const app = document.querySelector("#app"); const state = { user: null, tasks: [], settings: null, openClaw: null, view: "board", dialogMode: null, selectedTaskId: null, draggedTaskId: null, dragOverKey: null, error: "" }; async function api(path, options = {}) { const response = await fetch(path, { credentials: "same-origin", headers: { "content-type": "application/json", ...(options.headers || {}) }, ...options }); const data = await response.json().catch(() => ({})); if (!response.ok) throw new Error(data.error || `HTTP ${response.status}`); return data; } async function init() { try { const session = await api("/api/session"); state.user = session.user; await Promise.all([loadTasks(), loadSettings()]); renderApp(); } catch { renderLogin(); } } function renderLogin() { app.innerHTML = `
`; document.querySelector("#login-form").addEventListener("submit", async (event) => { event.preventDefault(); const form = new FormData(event.currentTarget); try { const result = await api("/api/login", { method: "POST", body: JSON.stringify({ email: form.get("email"), password: form.get("password") }) }); state.user = result.user; await Promise.all([loadTasks(), loadSettings()]); renderApp(); } catch (error) { document.querySelector("#login-error").textContent = error.message; } }); } async function loadTasks() { const result = await api("/api/tasks"); state.tasks = result.tasks.map(normalizeTask); } async function loadSettings() { const result = await api("/api/settings"); state.settings = result.settings; state.openClaw = result.openClaw; } function normalizeTask(task) { return { ...task, state: task.state === "In Progress" ? "In progress" : task.state, priority: task.priority === "Normal" ? "Medium" : task.priority === "Urgent" ? "High" : task.priority, assignee: task.assignee || "bao" }; } function renderApp() { app.innerHTML = `
OpenClaw ${state.openClaw?.ok ? "live" : "pruefen"}
${state.view === "settings" ? settingsView() : boardView()}
${createModal()} ${detailModal()} `; document.querySelectorAll("[data-view]").forEach((button) => { button.addEventListener("click", async () => { state.view = button.dataset.view; if (state.view === "settings") await loadSettings(); renderApp(); }); }); document.querySelector("#logout").addEventListener("click", async () => { await api("/api/logout", { method: "POST" }); state.user = null; renderLogin(); }); document.querySelector("#refresh-board").addEventListener("click", async () => { await Promise.all([loadTasks(), loadSettings()]); renderApp(); }); if (state.view === "board") bindBoard(); if (state.view === "settings") bindSettings(); } function boardView() { return `

Aufgaben

Task Board - Uebersicht aller Arbeitspakete

${state.openClaw?.ok ? "Live" : "Polling"} ${state.tasks.length} Aufgaben
${columns.map(column).join("")}
`; } function column(columnDef) { const tasks = state.tasks.filter((task) => task.state === columnDef.state); return `
${columnDef.label} ${tasks.length}
${tasks.map(taskCard).join("") || `
${columnDef.blocked ? "Keine Blockierer" : "Keine Aufgaben"}
`}
`; } function taskCard(task) { const blockedClass = task.state === "Blocked" ? "card-blocked" : ""; return ` `; } function createModal() { if (state.dialogMode !== "create") return ""; return ` `; } function detailModal() { if (state.dialogMode !== "detail") return ""; const task = selectedTask(); if (!task) return ""; return `
`; } function bindBoard() { document.querySelector("#new-task").addEventListener("click", () => { state.dialogMode = "create"; renderApp(); }); document.querySelectorAll(".col").forEach((columnEl) => { columnEl.addEventListener("dragover", (event) => { event.preventDefault(); state.dragOverKey = columnEl.dataset.column; columnEl.classList.add("drag-over"); }); columnEl.addEventListener("dragleave", () => columnEl.classList.remove("drag-over")); columnEl.addEventListener("drop", async (event) => { event.preventDefault(); columnEl.classList.remove("drag-over"); const id = event.dataTransfer?.getData("text/plain") || state.draggedTaskId; state.dragOverKey = null; if (!id) return; await moveTask(id, columnEl.dataset.state); }); }); document.querySelectorAll("[data-card]").forEach((card) => { card.addEventListener("dragstart", (event) => { state.draggedTaskId = card.dataset.card; event.dataTransfer.effectAllowed = "move"; event.dataTransfer.setData("text/plain", card.dataset.card); card.classList.add("dragging"); }); card.addEventListener("dragend", () => { state.draggedTaskId = null; card.classList.remove("dragging"); }); card.addEventListener("click", () => { state.selectedTaskId = card.dataset.card; state.dialogMode = "detail"; renderApp(); }); }); bindCreateModal(); bindDetailModal(); } function bindCreateModal() { const form = document.querySelector("#create-form"); if (!form) return; document.querySelector("#close-modal").addEventListener("click", closeModal); document.querySelector("#cancel-modal").addEventListener("click", closeModal); document.querySelector("#modal-overlay").addEventListener("click", (event) => { if (event.target.id === "modal-overlay") closeModal(); }); form.addEventListener("submit", async (event) => { event.preventDefault(); const data = Object.fromEntries(new FormData(form)); try { await api("/api/tasks", { method: "POST", body: JSON.stringify({ ...data, state: "Backlog" }) }); closeModal(); await loadTasks(); renderApp(); } catch (error) { document.querySelector("#form-error").textContent = error.message; } }); } function bindDetailModal() { const form = document.querySelector("#detail-form"); if (!form) return; document.querySelector("#close-detail").addEventListener("click", closeModal); document.querySelector("#cancel-detail").addEventListener("click", closeModal); document.querySelector("#detail-overlay").addEventListener("click", (event) => { if (event.target.id === "detail-overlay") closeModal(); }); document.querySelector("#save-detail").addEventListener("click", async () => { const data = Object.fromEntries(new FormData(form)); data.title = document.querySelector(".detail-title-input").value; data.detail = document.querySelector(".detail-textarea").value; try { await api(`/api/tasks/${state.selectedTaskId}`, { method: "PATCH", body: JSON.stringify(data) }); closeModal(); await loadTasks(); renderApp(); } catch (error) { document.querySelector("#detail-error").textContent = error.message; } }); document.querySelector("#delete-detail").addEventListener("click", async () => { await api(`/api/tasks/${state.selectedTaskId}`, { method: "DELETE" }); closeModal(); await loadTasks(); renderApp(); }); } async function moveTask(id, nextState) { await api(`/api/tasks/${id}`, { method: "PATCH", body: JSON.stringify({ state: nextState }) }); await loadTasks(); renderApp(); } function settingsView() { const openClaw = state.openClaw || {}; return `
MAXI

Settings

Admin-Konfiguration und Maxi-OpenClaw-Verbindung.

OPENCLAW${openClaw.ok ? "Live" : "Check"}

Gateway

${escapeHtml(openClaw.baseUrl || state.settings?.openClawBaseUrl || "")}

Health ${escapeHtml(String(openClaw.healthStatus || openClaw.error || "unbekannt"))} - ${escapeHtml(String(openClaw.latencyMs || 0))} ms

BOARD
`; } function bindSettings() { document.querySelector("#refresh-settings").addEventListener("click", async () => { await loadSettings(); renderApp(); }); document.querySelector("#settings-form").addEventListener("submit", async (event) => { event.preventDefault(); const data = Object.fromEntries(new FormData(event.currentTarget)); await api("/api/settings", { method: "PATCH", body: JSON.stringify(data) }); await loadSettings(); renderApp(); }); } function closeModal() { state.dialogMode = null; state.selectedTaskId = null; renderApp(); } function selectedTask() { return state.tasks.find((task) => task.id === state.selectedTaskId); } function priorityLabel(priority) { return priority === "High" ? "High" : priority === "Low" ? "Low" : "Med"; } function priorityColor(priority) { return priority === "High" ? "#f87171" : priority === "Low" ? "#60a5fa" : "#facc15"; } function assigneeLabel(assignee) { const match = assignees.find((item) => item.value === String(assignee || "").toLowerCase()); return match?.label || escapeHtml(assignee || "Bao"); } function assigneeClass(assignee) { const lower = String(assignee || "").toLowerCase(); if (lower === "iris") return "assignee-iris"; if (lower === "bao") return "assignee-bao"; return "assignee-agent"; } function stateLabel(value) { const found = columns.find((column) => column.state === value); return found?.label || value; } function statusTone(value) { if (value === "Done") return "is-done"; if (value === "Blocked") return "is-blocked"; if (value === "Review") return "is-review"; if (value === "In progress") return "is-progress"; return "is-backlog"; } function relativeTime(date) { if (!date) return "keine Updates"; const mins = Math.max(0, Math.round((Date.now() - new Date(date).getTime()) / 60000)); if (mins < 1) return "gerade eben"; if (mins < 60) return `vor ${mins} min`; const hours = Math.round(mins / 60); if (hours < 24) return `vor ${hours} h`; return `vor ${Math.round(hours / 24)} d`; } function formatDate(date, withTime = false) { if (!date) return "-"; return new Date(date).toLocaleString("de-DE", withTime ? { dateStyle: "medium", timeStyle: "short" } : { dateStyle: "medium" }); } function escapeHtml(value) { return String(value ?? "") .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); } function escapeAttr(value) { return escapeHtml(value).replaceAll("`", "`"); } init();