const states = ["Backlog", "In Progress", "Review", "Done"]; const priorities = ["Low", "Normal", "High", "Urgent"]; const app = document.querySelector("#app"); const state = { user: null, tasks: [], settings: null, openClaw: null, view: "board", 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 = `

Maxi Mission Control

Schlanke Kommandozentrale fuer Aufgaben und OpenClaw-Status.

`; 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; } async function loadSettings() { const result = await api("/api/settings"); state.settings = result.settings; state.openClaw = result.openClaw; } function renderApp() { app.innerHTML = `
${state.view === "settings" ? settingsView() : boardView()}
${taskDialog()} `; 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(); }); if (state.view === "board") bindBoard(); if (state.view === "settings") bindSettings(); } function boardView() { const connected = state.openClaw?.ok; return `

Taskboard

${state.tasks.length} Aufgaben · Assignee ${escapeHtml(state.settings?.defaultAssignee || "luna")}

OpenClaw ${connected ? "verbunden" : "pruefen"}
${states.map((name) => column(name)).join("")}
`; } function column(name) { const tasks = state.tasks.filter((task) => task.state === name); return `

${name}${tasks.length}

${tasks.map(taskCard).join("") || `

Keine Aufgaben

`}
`; } function taskCard(task) { const index = states.indexOf(task.state); return `
${escapeHtml(task.priority)} ${escapeHtml(task.assignee || "luna")}

${escapeHtml(task.title)}

${task.detail ? `

${escapeHtml(task.detail)}

` : ""}
`; } function taskDialog() { return `

Aufgabe

`; } function bindBoard() { const dialog = document.querySelector("#task-dialog"); const form = document.querySelector("#task-form"); const dialogTitle = document.querySelector("#dialog-title"); const dialogMeta = document.querySelector("#dialog-meta"); let draggedTaskId = null; const resetDialog = () => { form.reset(); form.elements.id.value = ""; form.elements.assignee.value = state.settings?.defaultAssignee || "luna"; dialogTitle.textContent = "Neue Aufgabe"; dialogMeta.textContent = ""; }; const openTaskDialog = (task) => { form.elements.id.value = task.id; form.elements.title.value = task.title; form.elements.detail.value = task.detail || ""; form.elements.state.value = task.state; form.elements.priority.value = task.priority; form.elements.assignee.value = task.assignee || ""; dialogTitle.textContent = task.title; dialogMeta.textContent = `Status ${task.state} · Aktualisiert ${formatDate(task.updatedAt)} · Erstellt ${formatDate(task.createdAt)}`; dialog.showModal(); }; document.querySelector("#new-task").addEventListener("click", () => { resetDialog(); dialog.showModal(); }); document.querySelector("#cancel-task").addEventListener("click", () => dialog.close()); dialog.addEventListener("click", (event) => { if (event.target === dialog) dialog.close(); }); document.querySelectorAll("[data-move]").forEach((button) => { button.addEventListener("click", async (event) => { event.stopPropagation(); await api(`/api/tasks/${button.dataset.move}`, { method: "PATCH", body: JSON.stringify({ state: button.dataset.state }) }); await loadTasks(); renderApp(); }); }); document.querySelectorAll("[data-edit]").forEach((button) => { button.addEventListener("click", (event) => { event.stopPropagation(); const task = state.tasks.find((x) => x.id === button.dataset.edit); if (task) openTaskDialog(task); }); }); document.querySelectorAll("[data-task-id]").forEach((card) => { card.addEventListener("click", (event) => { if (event.target.closest("button, input, textarea, select, a")) return; const task = state.tasks.find((x) => x.id === card.dataset.taskId); if (task) openTaskDialog(task); }); card.addEventListener("keydown", (event) => { if (event.key !== "Enter" && event.key !== " ") return; event.preventDefault(); const task = state.tasks.find((x) => x.id === card.dataset.taskId); if (task) openTaskDialog(task); }); card.addEventListener("dragstart", (event) => { draggedTaskId = card.dataset.taskId; card.classList.add("dragging"); event.dataTransfer.effectAllowed = "move"; event.dataTransfer.setData("text/plain", draggedTaskId); }); card.addEventListener("dragend", () => { draggedTaskId = null; card.classList.remove("dragging"); document.querySelectorAll(".dropzone.drag-over").forEach((zone) => zone.classList.remove("drag-over")); }); }); document.querySelectorAll(".dropzone").forEach((zone) => { zone.addEventListener("dragover", (event) => { event.preventDefault(); event.dataTransfer.dropEffect = "move"; zone.classList.add("drag-over"); }); zone.addEventListener("dragleave", (event) => { if (!zone.contains(event.relatedTarget)) zone.classList.remove("drag-over"); }); zone.addEventListener("drop", async (event) => { event.preventDefault(); zone.classList.remove("drag-over"); const taskId = event.dataTransfer.getData("text/plain") || draggedTaskId; const nextState = zone.dataset.state; const task = state.tasks.find((x) => x.id === taskId); if (!task || task.state === nextState) return; await api(`/api/tasks/${taskId}`, { method: "PATCH", body: JSON.stringify({ state: nextState }) }); await loadTasks(); renderApp(); }); }); form.addEventListener("submit", async (event) => { event.preventDefault(); const data = Object.fromEntries(new FormData(form)); const path = data.id ? `/api/tasks/${data.id}` : "/api/tasks"; await api(path, { method: data.id ? "PATCH" : "POST", body: JSON.stringify(data) }); dialog.close(); await loadTasks(); renderApp(); }); } function settingsView() { const openClaw = state.openClaw || {}; return `

Einstellungen

Admin-Konfiguration und Maxi-OpenClaw-Verbindung.

OpenClaw

${openClaw.ok ? "Verbunden" : "Nicht verbunden"}

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

Health: ${escapeHtml(String(openClaw.healthStatus || openClaw.error || "unbekannt"))} · Tool: ${escapeHtml(String(openClaw.toolResult || "nicht geprueft"))} · ${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 escapeHtml(value) { return String(value ?? "") .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); } function formatDate(value) { if (!value) return "unbekannt"; return new Date(value).toLocaleString("de-DE", { dateStyle: "short", timeStyle: "short" }); } init();