Refine Maxi task board UX
This commit is contained in:
+50
-17
@@ -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,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 `
|
||||
<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>
|
||||
</div>
|
||||
<section class="settings-grid">
|
||||
<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>
|
||||
</div>
|
||||
<section class="settings-grid">
|
||||
<article class="module-card">
|
||||
<div class="module-card-head"><span class="eyebrow">OPENCLAW</span><span class="badge ${openClaw.ok ? "positive" : "warning"}">${openClaw.ok ? "Live" : "Check"}</span></div>
|
||||
<h3>Gateway</h3>
|
||||
@@ -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>
|
||||
</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
@@ -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
@@ -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; }
|
||||
|
||||
@@ -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() });
|
||||
|
||||
Reference in New Issue
Block a user