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: "luna", label: "Luna" },
{ 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,
toast: 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: normalizeAssignee(task.assignee)
};
}
function renderApp() {
app.innerHTML = `
${state.view === "settings" ? settingsView() : boardView()}
${toastView()}
${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;
state.toast = null;
renderLogin();
});
document.querySelector("#refresh-board").addEventListener("click", async () => {
await Promise.all([loadTasks(), loadSettings()]);
renderApp();
showToast("Aktualisiert");
});
if (state.view === "board") bindBoard();
if (state.view === "settings") bindSettings();
}
function boardView() {
return `
${columns.map(column).join("")}
`;
}
function column(columnDef) {
const tasks = state.tasks.filter((task) => task.state === columnDef.state);
return `
${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();
showToast("Aufgabe erstellt");
} catch (error) {
document.querySelector("#form-error").textContent = error.message;
showToast(error.message, "error");
}
});
}
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();
showToast("Aufgabe gespeichert");
} catch (error) {
document.querySelector("#detail-error").textContent = error.message;
showToast(error.message, "error");
}
});
document.querySelector("#delete-detail").addEventListener("click", async () => {
await api(`/api/tasks/${state.selectedTaskId}`, { method: "DELETE" });
closeModal();
await loadTasks();
renderApp();
showToast("Aufgabe geloescht");
});
}
async function moveTask(id, nextState) {
await api(`/api/tasks/${id}`, { method: "PATCH", body: JSON.stringify({ state: nextState }) });
await loadTasks();
renderApp();
showToast(`Status: ${stateLabel(nextState)}`);
}
function settingsView() {
const openClaw = state.openClaw || {};
return `
MAXISettings
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();
showToast("Settings aktualisiert");
});
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();
showToast("Settings gespeichert");
});
}
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 === normalizeAssignee(assignee));
return match?.label || "Luna";
}
function assigneeClass(assignee) {
return normalizeAssignee(assignee) === "maxi" ? "assignee-maxi" : "assignee-luna";
}
function normalizeAssignee(assignee) {
const lower = String(assignee || "").toLowerCase();
return lower === "maxi" ? "maxi" : "luna";
}
function toastView() {
if (!state.toast) return "";
return `${escapeHtml(state.toast.message)}
`;
}
function showToast(message, type = "success") {
state.toast = { message, type, id: Date.now() };
const toastId = state.toast.id;
renderApp();
window.clearTimeout(showToast.timer);
showToast.timer = window.setTimeout(() => {
if (state.toast?.id === toastId) {
state.toast = null;
renderApp();
}
}, 2600);
}
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();