Refine Maxi task board UX

This commit is contained in:
Iris
2026-06-26 18:39:46 +00:00
parent fe840da278
commit 6be8de566e
4 changed files with 118 additions and 52 deletions
+44 -11
View File
@@ -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() {
<section class="content">${state.view === "settings" ? settingsView() : boardView()}</section>
</main>
</section>
${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() {
<div class="field"><label for="task-detail">Beschreibung</label><textarea id="task-detail" name="detail" class="field-input field-textarea" rows="3" placeholder="Details zur Aufgabe..."></textarea></div>
<div class="field-row">
<div class="field"><label for="task-priority">Prioritaet</label><select id="task-priority" name="priority" class="field-input field-select">${priorities.map((p) => `<option ${p === "Medium" ? "selected" : ""}>${p}</option>`).join("")}</select></div>
<div class="field"><label for="task-assignee">Zugewiesen an</label><select id="task-assignee" name="assignee" class="field-input field-select">${assignees.map((a) => `<option value="${a.value}" ${a.value === "bao" ? "selected" : ""}>${a.label}</option>`).join("")}</select></div>
<div class="field"><label for="task-assignee">Zugewiesen an</label><select id="task-assignee" name="assignee" class="field-input field-select">${assignees.map((a) => `<option value="${a.value}" ${a.value === "luna" ? "selected" : ""}>${a.label}</option>`).join("")}</select></div>
</div>
<p class="form-error" id="form-error"></p>
<div class="modal-actions"><button type="button" class="btn-cancel" id="cancel-modal">Abbrechen</button><button type="submit" class="btn-submit">Aufgabe erstellen</button></div>
@@ -264,7 +267,7 @@ function detailModal() {
<div class="sidebar-heading">Eigenschaften</div>
<label class="sidebar-field"><span>Status</span><select name="state" class="field-input field-select slim">${states.map((s) => `<option value="${s}" ${s === task.state ? "selected" : ""}>${stateLabel(s)}</option>`).join("")}</select></label>
<label class="sidebar-field"><span>Prioritaet</span><select name="priority" class="field-input field-select slim">${priorities.map((p) => `<option ${p === task.priority ? "selected" : ""}>${p}</option>`).join("")}</select></label>
<label class="sidebar-field"><span>Zustaendig</span><select name="assignee" class="field-input field-select slim">${assignees.map((a) => `<option value="${a.value}" ${a.value === (task.assignee || "bao") ? "selected" : ""}>${a.label}</option>`).join("")}</select></label>
<label class="sidebar-field"><span>Zustaendig</span><select name="assignee" class="field-input field-select slim">${assignees.map((a) => `<option value="${a.value}" ${a.value === normalizeAssignee(task.assignee) ? "selected" : ""}>${a.label}</option>`).join("")}</select></label>
</form>
<section class="sidebar-card subtle"><div class="sidebar-heading">Snapshot</div><dl class="snapshot-list"><div><dt>Quelle</dt><dd>Maxi</dd></div><div><dt>Prioritaet</dt><dd>${escapeHtml(task.priority)}</dd></div><div><dt>Zustaendig</dt><dd>${assigneeLabel(task.assignee)}</dd></div></dl></section>
<p class="detail-flash error" id="detail-error"></p>
@@ -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,11 +381,13 @@ 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 `
<div class="settings-wrap">
<div class="page-heading">
<div><span class="eyebrow">MAXI</span><h1>Settings</h1><p>Admin-Konfiguration und Maxi-OpenClaw-Verbindung.</p></div>
<button class="refresh" id="refresh-settings">Aktualisieren</button>
@@ -392,12 +402,13 @@ function settingsView() {
<article class="module-card">
<div class="module-card-head"><span class="eyebrow">BOARD</span></div>
<form id="settings-form">
<div class="field"><label>Default-Assignee</label><input class="field-input" name="defaultAssignee" value="${escapeAttr(state.settings?.defaultAssignee || "bao")}"></div>
<div class="field"><label>Default-Assignee</label><select class="field-input field-select" name="defaultAssignee">${assignees.map((a) => `<option value="${a.value}" ${a.value === normalizeAssignee(state.settings?.defaultAssignee) ? "selected" : ""}>${a.label}</option>`).join("")}</select></div>
<div class="field"><label>Zeitzone</label><input class="field-input" name="timezone" value="${escapeAttr(state.settings?.timezone || "Europe/Berlin")}"></div>
<div class="modal-actions"><button class="btn-submit" type="submit">Speichern</button></div>
</form>
</article>
</section>
</div>
`;
}
@@ -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 `<div class="toast-stack" aria-live="polite"><div class="toast ${state.toast.type === "error" ? "toast-error" : "toast-success"}">${escapeHtml(state.toast.message)}</div></div>`;
}
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) {
+2 -2
View File
@@ -4,10 +4,10 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Maxi Mission Control</title>
<link rel="stylesheet" href="/styles.css?v=20260626-create-modal">
<link rel="stylesheet" href="/styles.css?v=20260626-luna-toast-settings">
</head>
<body>
<main id="app"></main>
<script type="module" src="/app.js?v=20260626-create-modal"></script>
<script type="module" src="/app.js?v=20260626-luna-toast-settings"></script>
</body>
</html>
+51 -24
View File
@@ -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; }
+15 -9
View File
@@ -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() });