From 6be8de566ed3925837c5623efd375eab81edaa57 Mon Sep 17 00:00:00 2001 From: Iris Date: Fri, 26 Jun 2026 18:39:46 +0000 Subject: [PATCH] Refine Maxi task board UX --- public/app.js | 67 +++++++++++++++++++++++++++++++----------- public/index.html | 4 +-- public/styles.css | 75 ++++++++++++++++++++++++++++++++--------------- server.js | 24 +++++++++------ 4 files changed, 118 insertions(+), 52 deletions(-) diff --git a/public/app.js b/public/app.js index a0bfbca..90a600d 100644 --- a/public/app.js +++ b/public/app.js @@ -8,8 +8,7 @@ const columns = [ const states = columns.map((column) => column.state); const priorities = ["High", "Medium", "Low"]; const assignees = [ - { value: "bao", label: "Bao" }, - { value: "iris", label: "Iris" }, + { value: "luna", label: "Luna" }, { value: "maxi", label: "Maxi" } ]; @@ -24,6 +23,7 @@ const state = { selectedTaskId: null, draggedTaskId: null, dragOverKey: null, + toast: null, error: "" }; @@ -103,7 +103,7 @@ function normalizeTask(task) { ...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" + assignee: normalizeAssignee(task.assignee) }; } @@ -139,6 +139,7 @@ function renderApp() {
${state.view === "settings" ? settingsView() : boardView()}
+ ${toastView()} ${createModal()} ${detailModal()} `; @@ -153,11 +154,13 @@ function 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(); @@ -228,7 +231,7 @@ function createModal() {
-
+

@@ -264,7 +267,7 @@ function detailModal() { - +

@@ -334,8 +337,10 @@ function bindCreateModal() { closeModal(); await loadTasks(); renderApp(); + showToast("Aufgabe erstellt"); } catch (error) { document.querySelector("#form-error").textContent = error.message; + showToast(error.message, "error"); } }); } @@ -357,8 +362,10 @@ function bindDetailModal() { 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 () => { @@ -366,6 +373,7 @@ function bindDetailModal() { closeModal(); await loadTasks(); renderApp(); + showToast("Aufgabe geloescht"); }); } @@ -373,16 +381,18 @@ 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 ` -
-
MAXI

Settings

Admin-Konfiguration und Maxi-OpenClaw-Verbindung.

- -
-
+
+
+
MAXI

Settings

Admin-Konfiguration und Maxi-OpenClaw-Verbindung.

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

Gateway

@@ -392,12 +402,13 @@ function settingsView() {
BOARD
-
+
-
+
+ `; } @@ -405,6 +416,7 @@ 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(); @@ -412,6 +424,7 @@ function bindSettings() { await api("/api/settings", { method: "PATCH", body: JSON.stringify(data) }); await loadSettings(); renderApp(); + showToast("Settings gespeichert"); }); } @@ -434,15 +447,35 @@ function priorityColor(priority) { } function assigneeLabel(assignee) { - const match = assignees.find((item) => item.value === String(assignee || "").toLowerCase()); - return match?.label || escapeHtml(assignee || "Bao"); + 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(); - if (lower === "iris") return "assignee-iris"; - if (lower === "bao") return "assignee-bao"; - return "assignee-agent"; + 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) { diff --git a/public/index.html b/public/index.html index 08234fd..841696c 100644 --- a/public/index.html +++ b/public/index.html @@ -4,10 +4,10 @@ Maxi Mission Control - +
- + diff --git a/public/styles.css b/public/styles.css index 3c54de0..a9cc94f 100644 --- a/public/styles.css +++ b/public/styles.css @@ -49,40 +49,58 @@ button, input, textarea, select { color: inherit; font: inherit; } button { cursor: pointer; } .shell { min-height: 100vh; display: grid; grid-template-columns: 224px 1fr; } -.shell.board-mode { grid-template-columns: 64px minmax(0, 1fr); background: #05070c; } +.shell.board-mode, +.shell.settings-mode { grid-template-columns: 64px minmax(0, 1fr); background: #05070c; } .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); } -.board-mode .sidebar { padding: 16px 8px; align-items: center; background: #070910; border-right-color: #151a25; backdrop-filter: none; } +.board-mode .sidebar, +.settings-mode .sidebar { padding: 16px 8px; align-items: center; background: #070910; border-right-color: #151a25; backdrop-filter: none; } .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; } -.board-mode .brand { padding: 0 0 24px; } +.board-mode .brand, +.settings-mode .brand { padding: 0 0 24px; } .board-mode .brand-copy, .board-mode .nav-label, .board-mode .sidebar-bottom, -.board-mode .nav button i { display: none; } +.board-mode .nav button i, +.settings-mode .brand-copy, +.settings-mode .nav-label, +.settings-mode .sidebar-bottom, +.settings-mode .nav button i { display: none; } .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; } -.board-mode .nav { width: 100%; gap: 8px; align-items: center; } -.board-mode .nav button { width: 40px; height: 40px; justify-content: center; padding: 0; border: 1px solid transparent; } +.board-mode .nav, +.settings-mode .nav { width: 100%; gap: 8px; align-items: center; } +.board-mode .nav button, +.settings-mode .nav button { width: 40px; height: 40px; justify-content: center; padding: 0; border: 1px solid transparent; } .board-mode .nav button::before { font-size: 14px; font-weight: 800; } -.board-mode .nav button[data-view="board"]::before { content: "B"; } -.board-mode .nav button[data-view="settings"]::before { content: "S"; } -.board-mode .nav button.active { border-color: #6f63d9; box-shadow: 0 0 0 1px rgba(139,124,246,.18); } +.settings-mode .nav button::before { font-size: 14px; font-weight: 800; } +.board-mode .nav button[data-view="board"]::before, +.settings-mode .nav button[data-view="board"]::before { content: "B"; } +.board-mode .nav button[data-view="settings"]::before, +.settings-mode .nav button[data-view="settings"]::before { content: "S"; } +.board-mode .nav button.active, +.settings-mode .nav button.active { border-color: #6f63d9; box-shadow: 0 0 0 1px rgba(139,124,246,.18); } .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); } -.board-mode .topbar { position: fixed; top: 18px; right: 30px; z-index: 10; height: auto; padding: 0; border: 0; background: transparent; backdrop-filter: none; } +.board-mode .topbar, +.settings-mode .topbar { position: fixed; top: 18px; right: 30px; z-index: 10; height: auto; padding: 0; border: 0; background: transparent; backdrop-filter: none; } .board-mode .search, -.board-mode .mobile-menu { display: none; } -.board-mode .top-actions { padding: 0; } -.board-mode .refresh { background: #111521; border-color: #2b3344; color: #c8cfdb; } +.board-mode .mobile-menu, +.settings-mode .search, +.settings-mode .mobile-menu { display: none; } +.board-mode .top-actions, +.settings-mode .top-actions { padding: 0; } +.board-mode .refresh, +.settings-mode .refresh { background: #111521; border-color: #2b3344; color: #c8cfdb; } .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; } @@ -91,7 +109,7 @@ main { min-width: 0; } .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; } -.board-mode .content { min-height: 100vh; padding: 44px 40px 32px; } +.board-mode .content { min-height: 100vh; padding: 92px 40px 32px; } .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); } @@ -129,9 +147,8 @@ main { min-width: 0; } .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, .18); color: #d9b4ff; } -.assignee-bao { background: rgba(59, 130, 246, .18); color: #93c5fd; } -.assignee-agent { background: rgba(16, 185, 129, .18); color: #8af0c8; } +.assignee-luna { background: rgba(147, 51, 234, .18); color: #d9b4ff; } +.assignee-maxi { background: rgba(16, 185, 129, .18); color: #8af0c8; } .card-title { font-size: 13px; font-weight: 700; color: #ffffff; line-height: 1.4; word-break: break-word; } .card-preview { margin-top: 6px; font-size: 11.5px; line-height: 1.45; color: #c1c8d6; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } .card-meta { font-size: 10px; color: #a0a9ba; margin-top: 5px; font-variant-numeric: tabular-nums; } @@ -199,19 +216,29 @@ main { min-width: 0; } .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; } +.settings-mode { background: #05070c; } +.settings-mode .content { min-height: 100vh; padding: 92px 40px 32px; background: #05070c; } +.settings-wrap { max-width: 1040px; display: flex; flex-direction: column; gap: 18px; animation: board-fade-in .35s ease-out; } +.settings-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; } +.module-card { min-height: 190px; padding: 20px; border: 1px solid #242d42; border-radius: var(--r); background: #0e111d; box-shadow: 0 18px 54px -36px rgba(0,0,0,.8); } +.module-card h3 { margin: 10px 0 6px; font-size: 15px; color: #ffffff; } +.module-card p { margin: 8px 0 0; color: #c1c8d6; font-size: 12px; line-height: 1.5; overflow-wrap: anywhere; } .module-card-head { display: flex; align-items: center; justify-content: space-between; } +.module-card form { display: grid; gap: 14px; } .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); } +.page-heading { display: flex; justify-content: space-between; align-items: flex-start; gap: 12px; } +.page-heading h1 { margin: 0; font-size: 24px; color: #ffffff; } +.page-heading p { margin: 5px 0 0; font-size: 12px; color: #aeb6c6; } .eyebrow { font-size: 8.5px; font-weight: 700; letter-spacing: .12em; color: var(--nx-accent); text-transform: uppercase; } +.toast-stack { position: fixed; right: 28px; bottom: 28px; z-index: 150; display: flex; flex-direction: column; gap: 10px; pointer-events: none; } +.toast { min-width: 220px; max-width: min(360px, calc(100vw - 48px)); padding: 12px 14px; border-radius: var(--r-sm); background: #111521; border: 1px solid #303956; color: #f8fbff; font-size: 12px; font-weight: 700; box-shadow: 0 18px 48px -18px rgba(0,0,0,.7); animation: toast-in .18s ease-out; } +.toast-success { border-color: rgba(81,212,154,.35); box-shadow: 0 18px 48px -18px rgba(81,212,154,.22), 0 18px 48px -18px rgba(0,0,0,.7); } +.toast-error { border-color: rgba(251,113,133,.45); color: #fecdd3; } +@keyframes toast-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } } + .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; } diff --git a/server.js b/server.js index 4df89a6..f437809 100644 --- a/server.js +++ b/server.js @@ -21,6 +21,7 @@ const openClawToken = process.env.OPENCLAW_GATEWAY_TOKEN || ""; const states = ["Backlog", "In progress", "Review", "Done", "Blocked"]; const priorities = ["Low", "Medium", "High"]; +const assignees = ["luna", "maxi"]; let db; function mustEnv(name) { @@ -64,12 +65,12 @@ 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"; + db.settings = { openClawBaseUrl, defaultAssignee: "luna", timezone: "Europe/Berlin", ...(db.settings || {}) }; + db.settings.defaultAssignee = normalizeAssignee(db.settings.defaultAssignee); 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"; + task.assignee = normalizeAssignee(task.assignee); } await saveDb(); } else { @@ -85,7 +86,7 @@ async function loadDb() { tasks: seedTasks(), settings: { openClawBaseUrl, - defaultAssignee: "bao", + defaultAssignee: "luna", timezone: "Europe/Berlin" } }; @@ -102,7 +103,7 @@ function seedTasks() { detail: "Login, Board und Einstellungen pruefen; OpenClaw-Verbindung muss auf Maxis Gateway zeigen.", state: "Review", priority: "High", - assignee: "bao", + assignee: "luna", createdAt: now, updatedAt: now }, @@ -112,7 +113,7 @@ function seedTasks() { detail: "Backlog als Eingang fuer Maxis operative Aufgaben nutzen.", state: "Backlog", priority: "Medium", - assignee: "bao", + assignee: "luna", createdAt: now, updatedAt: now } @@ -138,6 +139,11 @@ function send(res, status, value, headers = {}) { res.end(body); } +function normalizeAssignee(value) { + const normalized = String(value || "").trim().toLowerCase(); + return assignees.includes(normalized) ? normalized : "luna"; +} + function getCookie(req, name) { const header = req.headers.cookie || ""; return header.split(";").map((x) => x.trim()).find((x) => x.startsWith(`${name}=`))?.slice(name.length + 1); @@ -252,7 +258,7 @@ async function handleApi(req, res) { detail: String(body.detail || "").trim(), state: states.includes(body.state) ? body.state : "Backlog", priority: priorities.includes(body.priority) ? body.priority : "Medium", - assignee: String(body.assignee || db.settings.defaultAssignee || "bao").trim(), + assignee: normalizeAssignee(body.assignee || db.settings.defaultAssignee), createdAt: now, updatedAt: now }; @@ -274,7 +280,7 @@ async function handleApi(req, res) { if (typeof body.detail === "string") task.detail = body.detail.trim(); if (states.includes(body.state)) task.state = body.state; if (priorities.includes(body.priority)) task.priority = body.priority; - if (typeof body.assignee === "string") task.assignee = body.assignee.trim(); + if (typeof body.assignee === "string") task.assignee = normalizeAssignee(body.assignee); task.updatedAt = new Date().toISOString(); await saveDb(); send(res, 200, { task }); @@ -296,7 +302,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() || "bao"; + if (typeof body.defaultAssignee === "string") db.settings.defaultAssignee = normalizeAssignee(body.defaultAssignee); if (typeof body.timezone === "string") db.settings.timezone = body.timezone.trim() || "Europe/Berlin"; await saveDb(); send(res, 200, { settings: db.settings, openClaw: await openClawStatus() });