diff --git a/public/app.js b/public/app.js index 471cba7..5c1719d 100644 --- a/public/app.js +++ b/public/app.js @@ -1,12 +1,17 @@ -const states = ["Backlog", "In Progress", "Review", "Done"]; -const priorities = ["Low", "Normal", "Medium", "High", "Urgent"]; +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: "maxi", label: "Maxi" }, - { value: "luna", label: "Luna" }, - { value: "iris", label: "Iris" } + { value: "iris", label: "Iris" }, + { value: "maxi", label: "Maxi" } ]; -const createTaskDefaultAssignee = "bao"; const app = document.querySelector("#app"); const state = { @@ -15,6 +20,10 @@ const state = { settings: null, openClaw: null, view: "board", + dialogMode: null, + selectedTaskId: null, + draggedTaskId: null, + dragOverKey: null, error: "" }; @@ -43,17 +52,21 @@ async function init() { function renderLogin() { app.innerHTML = `
-
-

Maxi Mission Control

-

Schlanke Kommandozentrale fuer Aufgaben und OpenClaw-Status.

- - - - + + + + + + + +
`; @@ -76,7 +89,7 @@ function renderLogin() { async function loadTasks() { const result = await api("/api/tasks"); - state.tasks = result.tasks; + state.tasks = result.tasks.map(normalizeTask); } async function loadSettings() { @@ -85,28 +98,49 @@ async function loadSettings() { 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 = ` -
+
-
- ${state.view === "settings" ? settingsView() : boardView()} -
+
+
+ + +
+ OpenClaw ${state.openClaw?.ok ? "live" : "pruefen"} + +
+
+
${state.view === "settings" ? settingsView() : boardView()}
+
- ${taskDialog()} + ${createModal()} + ${detailModal()} `; document.querySelectorAll("[data-view]").forEach((button) => { @@ -116,247 +150,253 @@ function renderApp() { 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() { - const connected = state.openClaw?.ok; return ` -
-
-

Taskboard

-

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

+
+
+
+

Aufgaben

+

Task Board - Uebersicht aller Arbeitspakete

+
+ ${state.openClaw?.ok ? "Live" : "Polling"} + ${state.tasks.length} Aufgaben +
+
+
+ +
-
- OpenClaw ${connected ? "verbunden" : "pruefen"} +
+ ${columns.map(column).join("")}
-
-
-
-
- ${states.map((name) => column(name)).join("")} -
`; } -function column(name) { - const tasks = state.tasks.filter((task) => task.state === name); +function column(columnDef) { + const tasks = state.tasks.filter((task) => task.state === columnDef.state); return ` -
-
${name}${tasks.length}
-
- ${tasks.map(taskCard).join("") || `

Keine Aufgaben

`} +
+
+ + ${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 ` -
-
- ${escapeHtml(task.priority)} - +
+
${escapeHtml(task.title)}
+ ${task.detail ? `
${escapeHtml(task.detail)}
` : ""} +
Update ${relativeTime(task.updatedAt || task.createdAt)}
+ `; } -function taskDialog() { +function createModal() { + if (state.dialogMode !== "create") return ""; return ` - -
-

Aufgabe

-
- - - -
- - + + `; +} + +function detailModal() { + if (state.dialogMode !== "detail") return ""; + const task = selectedTask(); + if (!task) return ""; + return ` +
+
+ + `; } 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"); - const saveTaskButton = document.querySelector("#save-task"); - let draggedTaskId = null; - const resetDialog = () => { - form.reset(); - form.elements.id.value = ""; - form.elements.state.value = "Backlog"; - form.elements.priority.value = "Medium"; - form.elements.assignee.value = createTaskDefaultAssignee; - form.classList.add("task-form-create"); - dialogTitle.textContent = "Neue Aufgabe"; - dialogMeta.textContent = "Erstelle eine neue Aufgabe fuer das Mission-Control-Board."; - saveTaskButton.textContent = "Aufgabe erstellen"; - }; - const openTaskDialog = (task) => { - form.classList.remove("task-form-create"); - 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; - ensureAssigneeOption(form.elements.assignee, task.assignee || state.settings?.defaultAssignee || "luna"); - form.elements.assignee.value = normalizeAssignee(task.assignee || state.settings?.defaultAssignee || "luna"); - dialogTitle.textContent = task.title; - dialogMeta.textContent = `Status ${task.state} · Aktualisiert ${formatDate(task.updatedAt)} · Erstellt ${formatDate(task.createdAt)}`; - saveTaskButton.textContent = "Speichern"; - dialog.showModal(); - }; - document.querySelector("#open-create-task").addEventListener("click", () => { - resetDialog(); - dialog.showModal(); + document.querySelector("#new-task").addEventListener("click", () => { + state.dialogMode = "create"; + renderApp(); }); - document.querySelector("#cancel-task").addEventListener("click", () => dialog.close()); - dialog.addEventListener("click", (event) => { - if (event.target === dialog) dialog.close(); - }); - document.querySelectorAll("[data-status-task]").forEach((select) => { - select.addEventListener("change", async (event) => { - event.stopPropagation(); - await api(`/api/tasks/${select.dataset.statusTask}`, { method: "PATCH", body: JSON.stringify({ state: select.value }) }); - 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; + document.querySelectorAll(".col").forEach((columnEl) => { + columnEl.addEventListener("dragover", (event) => { event.preventDefault(); - const task = state.tasks.find((x) => x.id === card.dataset.taskId); - if (task) openTaskDialog(task); + 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) => { - draggedTaskId = card.dataset.taskId; - card.classList.add("dragging"); + state.draggedTaskId = card.dataset.card; event.dataTransfer.effectAllowed = "move"; - event.dataTransfer.setData("text/plain", draggedTaskId); + event.dataTransfer.setData("text/plain", card.dataset.card); + card.classList.add("dragging"); }); card.addEventListener("dragend", () => { - draggedTaskId = null; + state.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(); + 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)); - const path = data.id ? `/api/tasks/${data.id}` : "/api/tasks"; - await api(path, { method: data.id ? "PATCH" : "POST", body: JSON.stringify(data) }); - dialog.close(); + 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 ` -
-
-

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

-
-
- - -
-
- -
-
+
+
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
+
+
+
+ +
+
`; } @@ -375,6 +415,64 @@ function bindSettings() { }); } +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("&", "&") @@ -384,35 +482,8 @@ function escapeHtml(value) { .replaceAll("'", "'"); } -function formatDate(value) { - if (!value) return "unbekannt"; - return new Date(value).toLocaleString("de-DE", { dateStyle: "short", timeStyle: "short" }); -} - -function normalizeAssignee(value) { - return String(value || "").trim().toLowerCase(); -} - -function assigneeLabel(value) { - const normalized = normalizeAssignee(value); - const known = assignees.find((x) => x.value === normalized); - if (known) return known.label; - return normalized ? normalized.charAt(0).toUpperCase() + normalized.slice(1) : "Luna"; -} - -function assigneeOptions(selected) { - const normalized = normalizeAssignee(selected); - const options = [...assignees]; - if (normalized && !options.some((x) => x.value === normalized)) { - options.push({ value: normalized, label: assigneeLabel(normalized) }); - } - return options.map((x) => ``).join(""); -} - -function ensureAssigneeOption(select, value) { - const normalized = normalizeAssignee(value); - if (!normalized || Array.from(select.options).some((option) => option.value === normalized)) return; - select.add(new Option(assigneeLabel(normalized), normalized)); +function escapeAttr(value) { + return escapeHtml(value).replaceAll("`", "`"); } init(); diff --git a/public/styles.css b/public/styles.css index 841bc87..f69fef6 100644 --- a/public/styles.css +++ b/public/styles.css @@ -1,544 +1,233 @@ :root { color-scheme: dark; - --bg: #080a0f; - --panel: #10131a; - --panel-soft: #0d1016; - --card: #12151c; - --text: #e8eaf0; - --muted: #7e8799; - --line: #202530; - --line-strong: #292f3b; - --accent: #8b7cf6; - --accent-strong: #b8adff; - --accent-soft: rgba(139, 124, 246, 0.12); - --warn: #e5b05e; - --danger: #ec7b82; - --ok: #51d49a; - --shadow: 0 28px 90px rgba(0, 0, 0, 0.42); + font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + color: #e8eaf0; + background: #080a0f; + font-synthesis: none; + --nx-panel: #10131a; + --nx-panel-soft: #0d1016; + --nx-line: #202530; + --nx-muted: #7e8799; + --nx-accent: #8b7cf6; + --nx-accent-soft: rgba(139, 124, 246, .12); + --nx-green: #51d49a; + --space-0: #050410; + --glass: rgba(20, 17, 48, .55); + --glass-2: rgba(28, 24, 64, .55); + --line: rgba(150, 140, 255, .10); + --line-2: rgba(150, 140, 255, .18); + --tx: #ece9ff; + --tx-2: #a8a3d6; + --tx-3: #6f6aa0; + --a-blue: #4f7cff; + --a-purple: #b557f6; + --a-mid: #7c6cff; + --grad: linear-gradient(120deg, var(--a-blue), var(--a-purple)); + --st-work: #3ddc97; + --st-queue: #fbbf24; + --st-block: #fb7185; + --glow-purple: 0 0 24px -2px rgba(181,87,246,.60); + --r: 14px; + --r-sm: 10px; } -* { - box-sizing: border-box; -} +*, +*::before, +*::after { box-sizing: border-box; } body { margin: 0; min-width: 320px; min-height: 100vh; - background: radial-gradient(circle at 70% -20%, #17152b 0, transparent 34%), var(--bg); - color: var(--text); - font: 12px/1.45 Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + background: radial-gradient(circle at 70% -20%, #17152b 0, transparent 34%), #080a0f; + color: #e8eaf0; + font: 15px/1.45 Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; letter-spacing: 0; } -button, input, textarea, select { - font: inherit; -} - -button { - color: inherit; - cursor: pointer; -} - -.login-shell { - min-height: 100vh; - display: grid; - place-items: center; - padding: 24px; -} - -.login { - width: min(420px, 100%); - padding: 32px; - border: 1px solid var(--line); - border-radius: 12px; - background: linear-gradient(145deg, rgba(18, 21, 29, 0.98), rgba(10, 12, 18, 0.98)); - box-shadow: var(--shadow); -} - -.login h1, .view-title h1 { - margin: 0; - font-size: 26px; - line-height: 1.1; -} - -.login p, .view-title p { - margin: 8px 0 0; - color: var(--muted); - font-size: 11px; -} - -.field { - display: grid; - gap: 7px; - margin-top: 16px; -} - -label { - color: #aab1bf; - font-size: 10px; - font-weight: 650; -} - -input, textarea, select { - width: 100%; - min-height: 38px; - padding: 9px 10px; - border: 1px solid var(--line-strong); - border-radius: 7px; - outline: none; - color: #e7e9ef; - background: #0c0f14; -} - -textarea { - resize: vertical; - min-height: 92px; -} - -input:focus, textarea:focus, select:focus { - border-color: #6559b8; - box-shadow: 0 0 0 3px rgba(139, 124, 246, 0.1); -} - -.primary, .ghost, .danger { - min-height: 38px; - padding: 8px 12px; - border-radius: 7px; - font-weight: 700; -} - -.primary { - border: 1px solid #443d7c; - background: #211d39; - color: #c1b8ff; -} - -.primary:hover { - background: #29234a; -} - -.ghost { - border: 1px solid var(--line); - background: var(--panel); - color: #a5adba; -} - -.danger { - border: 1px solid rgba(236, 123, 130, 0.3); - background: rgba(193, 70, 79, 0.12); - color: #efa2a7; -} - -.app-shell { - min-height: 100vh; - display: grid; - grid-template-columns: 224px minmax(0, 1fr); -} - -.sidebar { - position: sticky; - top: 0; - height: 100vh; - display: flex; - flex-direction: column; - gap: 20px; - padding: 22px 14px 14px; - border-right: 1px solid #1a1e27; - background: rgba(9, 11, 16, 0.94); - backdrop-filter: blur(18px); -} - -.brand { - display: grid; - gap: 3px; - padding: 0 8px 5px; -} - -.brand strong { - font-size: 13px; - letter-spacing: 0.14em; - text-transform: uppercase; -} - -.brand span, .user small, .meta { - color: var(--muted); - font-size: 10px; -} - -.nav { - display: flex; - flex-direction: column; - gap: 3px; -} - -.nav button { - display: flex; - align-items: center; - gap: 10px; - width: 100%; - min-height: 34px; - padding: 9px 10px; - border: 0; - border-radius: 7px; - background: transparent; - color: #8991a1; - text-align: left; -} - -.nav button:hover, .nav button.active { - background: var(--accent-soft); - color: #ececf5; -} - -.nav button.active { - box-shadow: inset 2px 0 var(--accent); -} - -.user { - margin-top: auto; - display: grid; - gap: 12px; - padding-top: 10px; - border-top: 1px solid #1b1f28; -} - -.content { - min-width: 0; - padding: 16px; -} - -.topbar { - min-height: 62px; - display: flex; - gap: 16px; - justify-content: space-between; - align-items: center; - margin: -16px -16px 16px; - padding: 0 30px; - border-bottom: 1px solid #191d25; - background: rgba(8, 10, 15, 0.68); - backdrop-filter: blur(16px); -} - -.actions { - display: flex; - flex-wrap: wrap; - gap: 8px; - justify-content: flex-end; -} - -.status-pill { - display: inline-flex; - align-items: center; - gap: 7px; - min-height: 30px; - padding: 6px 10px; - border: 1px solid var(--line); - border-radius: 8px; - background: var(--panel); - color: #8c95a5; - font-size: 10px; - white-space: nowrap; -} - -.dot { - width: 7px; - height: 7px; - border-radius: 50%; - background: var(--warn); -} - -.dot.ok { - background: var(--ok); - box-shadow: 0 0 7px rgba(81, 212, 154, 0.4); -} - -.quick-create { - display: flex; - justify-content: flex-start; - margin-bottom: 12px; -} - -.quick-create button { - min-width: 112px; -} - -.kanban { - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: 12px; - align-items: start; -} - -.kanban-column { - min-width: 0; - min-height: 380px; - padding: 12px; - border: 1px solid var(--line); - border-radius: 9px; - background: rgba(14, 17, 23, 0.8); -} - -.kanban-column > header { - display: flex; - justify-content: space-between; - padding: 5px 3px 14px; - color: #aeb4c0; - font-size: 10px; -} - -.kanban-column > header b { - padding: 1px 6px; - border-radius: 8px; - background: #242936; -} - -.dropzone { - min-height: 320px; - border-radius: 8px; - transition: background 0.16s ease, box-shadow 0.16s ease; -} - -.dropzone.drag-over { - background: rgba(139, 124, 246, 0.08); - box-shadow: inset 0 0 0 2px rgba(139, 124, 246, 0.25); -} - -.empty-state { - margin: 0; - padding: 35px 0; - text-align: center; - color: #596171; - font-size: 9px; -} - -.task-card { - margin-bottom: 9px; - padding: 14px; - border: 1px solid #252a35; - border-radius: 8px; - background: var(--card); - cursor: grab; - transition: border-color 0.14s ease, transform 0.14s ease; -} - -.task-card:hover, -.task-card:focus-visible { - border-color: #343947; - outline: none; - transform: translateY(-1px); -} - -.task-card.dragging { - opacity: 0.55; - cursor: grabbing; -} - -.task-card-head { - display: flex; - gap: 8px; - align-items: center; - justify-content: space-between; -} - -.task-card h3 { - margin: 10px 0 12px; - color: #eef0f5; - font-size: 11px; - line-height: 1.4; - overflow-wrap: anywhere; -} - -.task-card p { - margin: -4px 0 12px; - color: var(--muted); - font-size: 10px; - line-height: 1.45; - overflow-wrap: anywhere; -} - -.task-card select { - width: 100%; - min-height: 32px; - margin-bottom: 12px; - padding: 7px 8px; - border: 1px solid #292f3b; - border-radius: 7px; - color: #c8ccd5; - background: #0c0f14; - font-size: 9px; - cursor: pointer; -} - -.task-footer { - display: flex; - gap: 5px; - align-items: center; - justify-content: space-between; - color: var(--muted); - font-size: 8px; -} - -.priority { - color: #9b91e6; - font-size: 8px; - text-transform: uppercase; -} - -.priority.urgent, .priority.high { - color: var(--warn); -} - -.priority.low { - color: var(--ok); -} - -.task-edit-btn { - width: 24px; - height: 24px; - display: grid; - place-items: center; - border: 1px solid transparent; - border-radius: 6px; - background: transparent; - color: #687181; - opacity: 0; -} - -.task-card:hover .task-edit-btn, -.task-card:focus-within .task-edit-btn { - opacity: 1; -} - -.task-edit-btn:hover { - border-color: #343947; - color: #c1b8ff; - background: #18152a; -} - -.badge { - display: inline-flex; - align-items: center; - min-height: 22px; - padding: 3px 8px; - border-radius: 10px; - background: var(--accent-soft); - color: var(--accent-strong); - font-size: 9px; - font-weight: 750; -} - -.panel { - max-width: 760px; - padding: 18px; - border: 1px solid var(--line); - border-radius: 9px; - background: linear-gradient(145deg, rgba(18, 21, 29, 0.96), rgba(12, 15, 21, 0.96)); -} - -.panel + .panel { - margin-top: 14px; -} - -.grid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 12px; -} - -.error { - margin-top: 12px; - color: var(--danger); - font-weight: 700; -} - -dialog { - width: min(560px, calc(100vw - 32px)); - padding: 24px; - border: 1px solid var(--line); - border-radius: 8px; - background: linear-gradient(145deg, rgba(18, 21, 29, 0.98), rgba(10, 12, 18, 0.98)); - color: var(--text); - box-shadow: var(--shadow); -} - -dialog::backdrop { - background: rgba(0, 0, 0, 0.68); - backdrop-filter: blur(3px); -} - -dialog h2 { - margin: 0; - font-size: 20px; -} - -.dialog-actions { - display: flex; - gap: 8px; - justify-content: flex-end; - margin-top: 16px; -} - -.dialog-meta { - min-height: 20px; - margin-top: 4px; - color: var(--muted); - font-size: 10px; -} - -.task-form-create .status-field { - display: none; -} - -.task-form-create .grid { - grid-template-columns: 1fr; -} - -@media (max-width: 980px) { - .app-shell { - grid-template-columns: 1fr; - } - - .sidebar { - position: sticky; - z-index: 2; - height: auto; - border-right: 0; - border-bottom: 1px solid var(--line); - padding: 12px; - } - - .nav, .user { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - - .kanban { - grid-template-columns: repeat(2, minmax(220px, 1fr)); - } -} - -@media (max-width: 640px) { - .content { - padding: 16px; - } - - .topbar { - display: grid; - margin: -16px -16px 16px; - padding: 16px; - } - - .actions { - justify-content: flex-start; - } - - .quick-create, .kanban, .grid { - grid-template-columns: 1fr; - } - - .kanban-column { - min-height: 260px; - } +button, input, textarea, select { color: inherit; font: inherit; } +button { cursor: pointer; } + +.shell { min-height: 100vh; display: grid; grid-template-columns: 224px 1fr; } +.sidebar { position: sticky; top: 0; height: 100vh; display: flex; flex-direction: column; padding: 22px 14px 14px; border-right: 1px solid #1a1e27; background: rgba(9, 11, 16, .94); backdrop-filter: blur(18px); } +.brand { display: flex; align-items: center; gap: 11px; padding: 0 8px 25px; } +.brand-mark { width: 35px; height: 35px; display: grid; place-items: center; border: 1px solid #443d7c; border-radius: 10px; background: linear-gradient(145deg, #241f44, #12121f); color: #b8adff; box-shadow: 0 0 24px rgba(139,124,246,.13); font-size: 11px; font-weight: 800; } +.brand strong { display: block; font-size: 13px; letter-spacing: .14em; } +.brand span, .owner span { display: block; color: var(--nx-muted); font-size: 10px; margin-top: 2px; } +.nav { display: flex; flex-direction: column; gap: 3px; } +.nav button, .sidebar-bottom > button { width: 100%; display: flex; align-items: center; gap: 10px; border: 0; padding: 9px 10px; border-radius: 7px; background: transparent; color: #8991a1; font-size: 12px; text-align: left; } +.nav button:hover, .nav button.active, .sidebar-bottom > button:hover { color: #ececf5; background: var(--nx-accent-soft); } +.nav button.active { box-shadow: inset 2px 0 var(--nx-accent); } +.nav button i { margin-left: auto; padding: 1px 6px; border: 1px solid #343947; border-radius: 8px; font-size: 9px; font-style: normal; } +.sidebar-bottom { margin-top: auto; border-top: 1px solid #1b1f28; padding-top: 10px; } +.owner { display: grid; grid-template-columns: 31px 1fr; gap: 9px; align-items: center; margin-top: 10px; padding: 10px 8px; } +.owner strong { font-size: 11px; } +.avatar { width: 31px; height: 31px; display: grid; place-items: center; border-radius: 50%; background: #28243f; color: #bcb3ff; font-size: 10px; } +main { min-width: 0; } +.topbar { height: 62px; display: flex; align-items: center; justify-content: space-between; padding: 0 30px; border-bottom: 1px solid #191d25; background: rgba(8,10,15,.68); backdrop-filter: blur(16px); } +.search { width: min(390px, 42vw); display: flex; align-items: center; gap: 9px; padding: 8px 10px; border: 1px solid #202530; border-radius: 7px; color: #6f7889; font-size: 11px; } +.search kbd { margin-left: auto; padding: 2px 5px; border: 1px solid #2c313d; border-radius: 4px; color: #606979; font-size: 9px; } +.top-actions { display: flex; align-items: center; gap: 10px; } +.connection { display: flex; gap: 6px; align-items: center; font-size: 10px; color: #8c95a5; } +.connection.live { color: var(--nx-green); } +.connection.preview { color: #e6b75d; } +.refresh { display: flex; align-items: center; gap: 7px; padding: 8px 11px; border: 1px solid var(--nx-line); border-radius: 7px; background: var(--nx-panel); color: #a5adba; font-size: 10px; } +.content { padding: 20px; } +.mobile-menu { display: none; border: 0; background: transparent; color: #aaa4e7; } +.status-dot { width: 6px; height: 6px; border-radius: 50%; background: #657083; } +.status-dot.online { background: var(--nx-green); box-shadow: 0 0 7px rgba(81,212,154,.4); } +.status-dot.offline { background: #e16e75; } + +.board-wrap { width: 100%; min-height: 100%; display: flex; flex-direction: column; gap: 20px; animation: board-fade-in .35s ease-out; } +@keyframes board-fade-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } } +.board-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; } +.board-header h1 { margin: 0; font-size: 22px; font-weight: 700; letter-spacing: -.02em; } +.grad-text { background: var(--grad); -webkit-background-clip: text; background-clip: text; color: transparent; } +.board-subtitle { margin: 4px 0 0; font-size: 11px; color: var(--tx-3); } +.board-live-row { display: flex; align-items: center; gap: 8px; margin-top: 8px; } +.live-pill { border: 1px solid var(--line-2); border-radius: 999px; padding: 3px 8px; font-size: 9px; text-transform: uppercase; letter-spacing: .08em; } +.live-pill-connected { color: #86efac; background: rgba(34,197,94,.10); } +.live-pill-degraded { color: #facc15; background: rgba(250,204,21,.10); } +.live-meta { color: var(--tx-3); font-size: 10px; } +.board-header-actions { display: flex; align-items: center; gap: 8px; } +.create-btn { display: flex; align-items: center; gap: 6px; padding: 8px 16px; border: none; border-radius: var(--r-sm); background: var(--grad); color: #fff; font-size: 12.5px; font-weight: 600; box-shadow: var(--glow-purple); transition: opacity .15s, transform .15s; } +.create-btn:hover { opacity: .85; transform: translateY(-1px); } +.plus-icon { font-size: 16px; line-height: 1; } +.board-columns { display: flex; gap: 14px; flex: 1; overflow-x: auto; padding-bottom: 20px; min-height: calc(100vh - 200px); scrollbar-width: thin; scrollbar-color: rgba(124,108,255,.22) transparent; } +.col { flex: 1; min-width: 240px; max-width: 320px; display: flex; flex-direction: column; background: var(--glass); border: 1px solid var(--line); border-radius: var(--r); padding: 12px; transition: border-color .2s, background .2s; backdrop-filter: blur(12px); } +.col.drag-over { border-color: var(--a-mid); background: linear-gradient(160deg, rgba(124,108,255,.10), rgba(20,17,48,.55)); box-shadow: 0 0 0 1px rgba(124,108,255,.15); } +.col-blocked { max-width: 240px; } +.col-header { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; padding-bottom: 10px; border-bottom: 1px solid var(--line); } +.col-icon { width: 9px; height: 9px; border-radius: 50%; flex: 0 0 auto; } +.col-name { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; color: var(--tx-2); } +.col-count { margin-left: auto; font-size: 10px; font-weight: 700; font-variant-numeric: tabular-nums; padding: 2px 8px; border-radius: 10px; background: var(--glass-2); color: var(--tx-2); border: 1px solid var(--line); } +.col-cards { display: flex; flex-direction: column; gap: 8px; flex: 1; } +.card { width: 100%; padding: 10px 12px; border-radius: var(--r-sm); background: linear-gradient(160deg, rgba(28,24,64,.45), rgba(20,17,48,.35)); border: 1px solid var(--line); text-align: left; transition: transform .15s, box-shadow .2s, border-color .15s, opacity .15s; } +.card:hover { transform: scale(1.02); border-color: var(--line-2); box-shadow: 0 0 0 1px rgba(124,108,255,.10), 0 8px 24px -6px rgba(0,0,0,.4); } +.card:active { cursor: grabbing; } +.card.dragging { opacity: .4; cursor: grabbing; } +.card-blocked { border-left: 3px solid var(--st-block); } +.card-top { display: flex; align-items: center; gap: 6px; margin-bottom: 6px; flex-wrap: wrap; } +.prio-badge { font-size: 9px; font-weight: 700; padding: 1px 5px; border-radius: 4px; border: 1px solid; background: transparent; } +.assignee { font-size: 10px; font-weight: 600; padding: 1px 6px; border-radius: 4px; } +.assignee-iris { background: rgba(147, 51, 234, .12); color: #c084fc; } +.assignee-bao { background: rgba(59, 130, 246, .12); color: #60a5fa; } +.assignee-agent { background: rgba(16, 185, 129, .12); color: #6ee7b7; } +.card-title { font-size: 12.5px; font-weight: 600; color: var(--tx); line-height: 1.4; word-break: break-word; } +.card-preview { margin-top: 6px; font-size: 11px; line-height: 1.45; color: var(--tx-2); display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } +.card-meta { font-size: 10px; color: var(--tx-3); margin-top: 5px; font-variant-numeric: tabular-nums; } +.empty-col, .detail-empty { display: flex; align-items: center; justify-content: center; padding: 24px 12px; font-size: 11px; color: var(--tx-3); font-style: italic; } +.board-columns::-webkit-scrollbar { height: 9px; } +.board-columns::-webkit-scrollbar-thumb { background: rgba(124,108,255,.22); border-radius: 9px; border: 2px solid transparent; background-clip: padding-box; } +.board-columns::-webkit-scrollbar-thumb:hover { background: rgba(124,108,255,.4); background-clip: padding-box; } +.board-columns::-webkit-scrollbar-track { background: transparent; } + +.modal-overlay, .detail-overlay { position: fixed; inset: 0; z-index: 100; display: flex; align-items: center; justify-content: center; background: rgba(5,4,16,.75); backdrop-filter: blur(16px); } +.modal-card { width: min(460px, calc(100vw - 32px)); background: linear-gradient(160deg, rgba(20,17,48,.85), rgba(14,12,32,.85)); border: 1px solid var(--line-2); border-radius: var(--r); padding: 24px; box-shadow: 0 0 0 1px rgba(124,108,255,.12), 0 20px 60px -12px rgba(0,0,0,.5); backdrop-filter: blur(12px); } +.modal-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 18px; padding-bottom: 14px; border-bottom: 1px solid var(--line); } +.modal-header h2 { margin: 0; font-size: 16px; font-weight: 700; color: var(--tx); } +.modal-close, .detail-close { width: 28px; height: 28px; display: flex; align-items: center; justify-content: center; border: none; background: transparent; color: var(--tx-3); border-radius: 6px; transition: background .15s, color .15s; } +.modal-close { font-size: 20px; } +.modal-close:hover, .detail-close:hover { background: rgba(124,108,255,.10); color: var(--tx); } +.modal-form { display: flex; flex-direction: column; gap: 14px; } +.field { display: flex; flex-direction: column; gap: 5px; flex: 1; } +.field label, .sidebar-field span, .login-card label span { font-size: 10.5px; font-weight: 600; color: var(--tx-2); text-transform: uppercase; letter-spacing: .04em; } +.req { color: var(--st-block); } +.field-input, .login-card input { width: 100%; padding: 8px 12px; border: 1px solid var(--line); border-radius: var(--r-sm); background: rgba(14,12,32,.5); color: var(--tx); font-size: 13px; outline: none; transition: border-color .15s, box-shadow .15s; } +.field-input:focus, .detail-title-input:focus, .detail-textarea:focus, .login-card input:focus { border-color: var(--a-mid); box-shadow: 0 0 0 2px rgba(124,108,255,.15); } +.field-textarea, .detail-textarea { resize: vertical; min-height: 60px; font-family: inherit; } +.field-select { cursor: pointer; } +.field-row { display: flex; gap: 12px; } +.form-error, .detail-flash.error { color: var(--st-block); font-size: 11px; margin: 0; } +.detail-flash.success { color: #86efac; font-size: 11px; margin: 0; } +.modal-actions, .detail-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 4px; flex-wrap: wrap; } +.btn-cancel, .btn-ghost { padding: 7px 14px; border: 1px solid var(--line); border-radius: var(--r-sm); background: transparent; color: var(--tx-2); font-size: 12px; font-weight: 500; transition: background .15s, color .15s; } +.btn-cancel:hover, .btn-ghost:hover { background: rgba(124,108,255,.08); color: var(--tx); } +.btn-submit { padding: 7px 18px; border: none; border-radius: var(--r-sm); background: var(--grad); color: #fff; font-size: 12px; font-weight: 600; box-shadow: var(--glow-purple); display: inline-flex; align-items: center; gap: 6px; transition: opacity .15s, transform .15s; } +.btn-submit:hover { opacity: .85; transform: translateY(-1px); } +.btn-danger { padding: 7px 14px; border: 1px solid rgba(244,63,94,.25); border-radius: var(--r-sm); background: rgba(244,63,94,.08); color: #fda4af; font-size: 12px; font-weight: 600; } + +.detail-panel { width: min(1120px, calc(100vw - 48px)); height: min(88vh, 860px); display: flex; flex-direction: column; background: linear-gradient(180deg, rgba(13,11,28,.97), rgba(10,9,24,.96)); border: 1px solid rgba(124,108,255,.18); border-radius: 22px; box-shadow: 0 28px 90px rgba(0,0,0,.45); overflow: hidden; } +.detail-topbar { display: flex; align-items: center; justify-content: space-between; padding: 18px 22px; border-bottom: 1px solid var(--line); } +.detail-breadcrumb { font-size: 11px; letter-spacing: .08em; text-transform: uppercase; color: var(--tx-3); } +.detail-content { display: grid; grid-template-columns: minmax(0, 1fr) 320px; min-height: 0; flex: 1; } +.detail-main { padding: 24px; overflow-y: auto; display: flex; flex-direction: column; gap: 20px; } +.detail-sidebar { padding: 24px; border-left: 1px solid var(--line); background: rgba(255,255,255,.02); display: flex; flex-direction: column; gap: 14px; } +.detail-title-block { display: flex; flex-direction: column; gap: 12px; } +.detail-state-pill { width: fit-content; border-radius: 999px; padding: 5px 10px; font-size: 11px; font-weight: 700; letter-spacing: .03em; border: 1px solid transparent; } +.detail-state-pill.is-backlog { color: #fde68a; background: rgba(251,191,36,.12); border-color: rgba(251,191,36,.25); } +.detail-state-pill.is-progress { color: #86efac; background: rgba(34,197,94,.12); border-color: rgba(34,197,94,.25); } +.detail-state-pill.is-review { color: #fdba74; background: rgba(249,115,22,.12); border-color: rgba(249,115,22,.25); } +.detail-state-pill.is-blocked { color: #fda4af; background: rgba(244,63,94,.12); border-color: rgba(244,63,94,.25); } +.detail-state-pill.is-done { color: #86efac; background: rgba(34,197,94,.12); border-color: rgba(34,197,94,.25); } +.detail-title-input { width: 100%; background: transparent; border: 1px solid transparent; border-radius: 12px; padding: 0; color: var(--tx); font-size: 31px; font-weight: 700; letter-spacing: -.03em; outline: none; } +.detail-meta-row { display: flex; flex-wrap: wrap; gap: 14px; color: var(--tx-3); font-size: 11.5px; } +.detail-section { background: rgba(255,255,255,.02); border: 1px solid var(--line); border-radius: 16px; padding: 16px; } +.detail-section-header, .sidebar-heading { display: flex; align-items: center; gap: 8px; margin-bottom: 12px; color: var(--tx-2); font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; } +.detail-textarea { width: 100%; min-height: 180px; border-radius: 12px; border: 1px solid var(--line); background: rgba(10,9,24,.55); color: var(--tx); padding: 14px; font-size: 14px; line-height: 1.6; outline: none; } +.detail-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; } +.activity-list { display: flex; flex-direction: column; gap: 10px; } +.activity-item { display: grid; grid-template-columns: 10px minmax(0, 1fr); gap: 10px; align-items: start; } +.activity-dot { width: 8px; height: 8px; border-radius: 999px; margin-top: 5px; background: linear-gradient(135deg, #8b5cf6, #3b82f6); box-shadow: 0 0 0 4px rgba(124,108,255,.12); } +.activity-message { color: var(--tx); font-size: 12.5px; line-height: 1.45; } +.activity-time { color: var(--tx-3); font-size: 10.5px; margin-top: 4px; } +.sidebar-card { border: 1px solid var(--line); border-radius: 16px; padding: 16px; background: rgba(10,9,24,.45); } +.sidebar-card.subtle { background: rgba(255,255,255,.02); } +.sidebar-field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 12px; } +.slim { min-height: 38px; } +.snapshot-list { display: flex; flex-direction: column; gap: 12px; margin: 0; } +.snapshot-list div { display: flex; justify-content: space-between; gap: 12px; } +.snapshot-list dt { color: var(--tx-3); font-size: 11px; } +.snapshot-list dd { margin: 0; color: var(--tx); font-size: 12px; text-align: right; } + +.settings-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; } +.module-card { min-height: 165px; padding: 18px; border: 1px solid var(--nx-line); border-radius: 9px; background: linear-gradient(145deg, rgba(18,21,29,.96), rgba(12,15,21,.96)); } +.module-card h3 { margin: 8px 0 4px; font-size: 13px; } +.module-card p { margin: 8px 0 0; color: var(--nx-muted); font-size: 10px; line-height: 1.5; overflow-wrap: anywhere; } +.module-card-head { display: flex; align-items: center; justify-content: space-between; } +.badge { padding: 4px 8px; border-radius: 10px; font-size: 8px; } +.badge.positive { color: var(--nx-green); background: rgba(81,212,154,.1); } +.badge.warning { color: #e7b660; background: rgba(231,182,96,.1); } +.page-heading { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 20px; gap: 12px; } +.page-heading h1 { margin: 0; font-size: 18px; } +.page-heading p { margin: 4px 0 0; font-size: 10px; color: var(--nx-muted); } +.eyebrow { font-size: 8.5px; font-weight: 700; letter-spacing: .12em; color: var(--nx-accent); text-transform: uppercase; } + +.login-shell { min-height: 100vh; display: grid; place-items: center; padding: 24px; } +.login-card { width: min(420px, 100%); padding: 32px; border: 1px solid var(--nx-line); border-radius: 14px; background: linear-gradient(145deg, rgba(18,21,29,.98), rgba(10,12,18,.98)); box-shadow: 0 28px 90px rgba(0,0,0,.4); } +.login-brand { display: flex; align-items: center; gap: 12px; padding-bottom: 28px; border-bottom: 1px solid #1d222c; } +.login-heading { padding: 28px 0 20px; } +.login-heading h1 { margin: 9px 0 0; font-size: 25px; } +.login-heading p { margin: 0; color: var(--nx-muted); font-size: 11px; line-height: 1.6; } +.login-card form, .login-card { display: grid; gap: 14px; } +.login-card button[type="submit"] { display: flex; align-items: center; justify-content: center; gap: 8px; margin-top: 5px; padding: 12px; border: 1px solid #554a9b; border-radius: 8px; color: #e6e2ff; background: linear-gradient(135deg, #332b68, #211d43); } +.login-error:empty { display: none; } +.login-error { margin: 0; padding: 10px 12px; border: 1px solid rgba(225,110,117,.25); border-radius: 8px; color: #efa2a7; background: rgba(193,70,79,.1); font-size: 10px; } +.login-card footer { margin-top: 10px; color: #626b7b; font-size: 8px; line-height: 1.5; text-align: center; } + +@media (max-width: 1100px) { + .detail-content { grid-template-columns: 1fr; } + .detail-sidebar { border-left: none; border-top: 1px solid var(--line); } +} + +@media (max-width: 900px) { + .shell { grid-template-columns: 1fr; } + .sidebar { position: fixed; z-index: 20; left: -240px; width: 224px; transition: left .2s ease; } + .mobile-menu { display: block; } + .topbar { padding: 0 18px; } + .settings-grid { grid-template-columns: 1fr; } +} + +@media (max-width: 860px) { + .board-columns { overflow-x: auto; -webkit-overflow-scrolling: touch; scrollbar-width: thin; } + .col { min-width: 260px; } + .detail-panel { width: 100vw; height: 100vh; border-radius: 0; } + .detail-grid { grid-template-columns: 1fr; } + .detail-main, .detail-sidebar { padding: 18px; } + .detail-title-input { font-size: 24px; } +} + +@media (max-width: 600px) { + .search { display: none; } + .field-row, .board-header, .page-heading { flex-direction: column; } } diff --git a/server.js b/server.js index d56d179..4df89a6 100644 --- a/server.js +++ b/server.js @@ -19,8 +19,8 @@ const adminName = process.env.ADMIN_NAME || "Maxi"; const openClawBaseUrl = (process.env.OPENCLAW_BASE_URL || "http://openclaw-gateway-maxi:18790").replace(/\/+$/, ""); const openClawToken = process.env.OPENCLAW_GATEWAY_TOKEN || ""; -const states = ["Backlog", "In Progress", "Review", "Done"]; -const priorities = ["Low", "Normal", "Medium", "High", "Urgent"]; +const states = ["Backlog", "In progress", "Review", "Done", "Blocked"]; +const priorities = ["Low", "Medium", "High"]; let db; function mustEnv(name) { @@ -64,6 +64,14 @@ async function loadDb() { await mkdir(dataDir, { recursive: true }); if (existsSync(dbPath)) { db = JSON.parse(await readFile(dbPath, "utf8")); + db.settings = { openClawBaseUrl, defaultAssignee: "bao", timezone: "Europe/Berlin", ...(db.settings || {}) }; + if (!db.settings.defaultAssignee || db.settings.defaultAssignee.toLowerCase() === "luna") db.settings.defaultAssignee = "bao"; + for (const task of db.tasks || []) { + if (task.state === "In Progress") task.state = "In progress"; + if (task.priority === "Normal" || task.priority === "Urgent") task.priority = task.priority === "Urgent" ? "High" : "Medium"; + if (!task.assignee || task.assignee.toLowerCase() === "luna") task.assignee = "bao"; + } + await saveDb(); } else { db = { users: [{ @@ -77,7 +85,7 @@ async function loadDb() { tasks: seedTasks(), settings: { openClawBaseUrl, - defaultAssignee: "luna", + defaultAssignee: "bao", timezone: "Europe/Berlin" } }; @@ -94,7 +102,7 @@ function seedTasks() { detail: "Login, Board und Einstellungen pruefen; OpenClaw-Verbindung muss auf Maxis Gateway zeigen.", state: "Review", priority: "High", - assignee: "luna", + assignee: "bao", createdAt: now, updatedAt: now }, @@ -103,8 +111,8 @@ function seedTasks() { title: "Erste Maxi-Aufgaben sammeln", detail: "Backlog als Eingang fuer Maxis operative Aufgaben nutzen.", state: "Backlog", - priority: "Normal", - assignee: "luna", + priority: "Medium", + assignee: "bao", createdAt: now, updatedAt: now } @@ -243,8 +251,8 @@ async function handleApi(req, res) { title, detail: String(body.detail || "").trim(), state: states.includes(body.state) ? body.state : "Backlog", - priority: priorities.includes(body.priority) ? body.priority : "Normal", - assignee: String(body.assignee || db.settings.defaultAssignee || "luna").trim(), + priority: priorities.includes(body.priority) ? body.priority : "Medium", + assignee: String(body.assignee || db.settings.defaultAssignee || "bao").trim(), createdAt: now, updatedAt: now }; @@ -288,7 +296,7 @@ async function handleApi(req, res) { if (url.pathname === "/api/settings" && req.method === "PATCH") { const body = await readJson(req); - if (typeof body.defaultAssignee === "string") db.settings.defaultAssignee = body.defaultAssignee.trim() || "luna"; + if (typeof body.defaultAssignee === "string") db.settings.defaultAssignee = body.defaultAssignee.trim() || "bao"; if (typeof body.timezone === "string") db.settings.timezone = body.timezone.trim() || "Europe/Berlin"; await saveDb(); send(res, 200, { settings: db.settings, openClaw: await openClawStatus() }); @@ -317,7 +325,7 @@ async function serveStatic(req, res) { const file = await readFile(filePath); const ext = path.extname(filePath); const type = ext === ".html" ? "text/html; charset=utf-8" : ext === ".css" ? "text/css; charset=utf-8" : ext === ".js" ? "text/javascript; charset=utf-8" : "application/octet-stream"; - send(res, 200, file, { "content-type": type, "cache-control": "no-store, no-cache, must-revalidate, proxy-revalidate" }); + send(res, 200, file, { "content-type": type, "cache-control": ext === ".html" ? "no-store" : "public, max-age=3600" }); } catch { const file = await readFile(path.join(publicDir, "index.html")); send(res, 200, file, { "content-type": "text/html; charset=utf-8" });