diff --git a/dashboard/backend/rbac.py b/dashboard/backend/rbac.py index 20da62e..a385ee7 100644 --- a/dashboard/backend/rbac.py +++ b/dashboard/backend/rbac.py @@ -61,6 +61,18 @@ def get_pages(username: str, role: str) -> list | str: return rbac.get("role_defaults", {}).get(role, {}).get("pages", []) +def get_sidebar_order(username: str) -> dict | None: + rbac = load_rbac() + overrides = rbac.get("user_overrides", {}) + return overrides.get(username, {}).get("sidebar_order") + + +def save_sidebar_order(username: str, order: dict): + rbac = load_rbac() + rbac.setdefault("user_overrides", {}).setdefault(username, {})["sidebar_order"] = order + save_rbac(rbac) + + def has_page_access(user: User, page: str) -> bool: if user.pages == "*": return True diff --git a/dashboard/backend/routers/auth.py b/dashboard/backend/routers/auth.py index 9246002..0e3775f 100644 --- a/dashboard/backend/routers/auth.py +++ b/dashboard/backend/routers/auth.py @@ -364,10 +364,28 @@ async def rbac_set_override(username: str, request: Request, current_user = Depe if pages is None: raise HTTPException(status_code=400, detail="Missing pages field") rbac = load_rbac() - rbac.setdefault("user_overrides", {})[username] = {"pages": pages} + existing = rbac.setdefault("user_overrides", {}).get(username, {}) + existing["pages"] = pages + rbac["user_overrides"][username] = existing save_rbac(rbac) return {"ok": True} +@router.get("/preferences") +async def get_preferences(current_user = Depends(auth.get_current_user)): + from rbac import get_sidebar_order + order = get_sidebar_order(current_user.username) + return {"sidebar_order": order} + +@router.put("/preferences") +async def save_preferences(request: Request, current_user = Depends(auth.get_current_user)): + from rbac import save_sidebar_order + body = await request.json() + order = body.get("sidebar_order") + if order is None or not isinstance(order, dict): + raise HTTPException(status_code=400, detail="Missing sidebar_order dict") + save_sidebar_order(current_user.username, order) + return {"ok": True} + @router.delete("/rbac/overrides/{username}") async def rbac_delete_override(username: str, current_user = Depends(auth.get_current_user)): if current_user.role != "admin": diff --git a/dashboard/frontend/src/App.svelte b/dashboard/frontend/src/App.svelte index eeb0f2b..e0dd134 100644 --- a/dashboard/frontend/src/App.svelte +++ b/dashboard/frontend/src/App.svelte @@ -11,7 +11,7 @@ import Security from "./routes/Security.svelte"; import Login from "./routes/Login.svelte"; import { onMount } from "svelte"; - import { getToken, checkAuth, setToken, setRefreshToken, currentUser, setCurrentUser } from "./lib/api.js"; + import { getToken, checkAuth, setToken, setRefreshToken, currentUser, setCurrentUser, getPreferences, savePreferences } from "./lib/api.js"; let page = $state("dashboard"); let dark = $state(false); @@ -20,6 +20,8 @@ let loading = $state(true); let userRole = $state(""); let allowedPages = $state([]); + let sidebarOrder = $state(null); + let orderSaveTimer = null; async function fetchUserInfo() { try { @@ -27,11 +29,24 @@ setCurrentUser(data); userRole = data.role || "admin"; allowedPages = data.pages || "*"; + // Fetch sidebar preferences + try { + const prefs = await getPreferences(); + sidebarOrder = prefs.sidebar_order || null; + } catch {} } catch (e) { console.error("Failed to fetch user info:", e); } } + function handleOrderChange(newOrder) { + sidebarOrder = newOrder; + clearTimeout(orderSaveTimer); + orderSaveTimer = setTimeout(() => { + savePreferences({ sidebar_order: newOrder }).catch(() => {}); + }, 500); + } + function hasPageAccess(pageId) { if (allowedPages === "*") return true; return Array.isArray(allowedPages) && allowedPages.includes(pageId); @@ -108,12 +123,12 @@ {:else if !authorized} {:else} -
- +
+
- {#if page === "dashboard" && hasPageAccess("dashboard")} diff --git a/dashboard/frontend/src/app.css b/dashboard/frontend/src/app.css index 28e114e..c8cbe81 100644 --- a/dashboard/frontend/src/app.css +++ b/dashboard/frontend/src/app.css @@ -1,5 +1,7 @@ @import "tailwindcss"; +@custom-variant dark (&:where(.dark, .dark *)); + @theme { --font-sans: "Inter", system-ui, -apple-system, sans-serif; @@ -22,6 +24,7 @@ --color-surface-700: #334155; --color-surface-800: #1e293b; --color-surface-900: #0f172a; + --color-surface-950: #020617; --color-emerald-400: #34d399; --color-emerald-500: #10b981; @@ -57,8 +60,10 @@ body { ::-webkit-scrollbar-thumb:hover { background: var(--color-surface-400); } /* Dark mode overrides */ -.dark body, -.dark { - background: var(--color-surface-900); +html.dark body { + background: var(--color-surface-950); color: var(--color-surface-200); } + +html.dark ::-webkit-scrollbar-thumb { background: var(--color-surface-600); } +html.dark ::-webkit-scrollbar-thumb:hover { background: var(--color-surface-500); } diff --git a/dashboard/frontend/src/components/Sidebar.svelte b/dashboard/frontend/src/components/Sidebar.svelte index 2a4d968..d35f29c 100644 --- a/dashboard/frontend/src/components/Sidebar.svelte +++ b/dashboard/frontend/src/components/Sidebar.svelte @@ -1,13 +1,13 @@ +{#snippet iconSvg(icon)} + + {#if icon === "grid"} + + {:else if icon === "box"} + + {:else if icon === "folder"} + + {:else if icon === "terminal"} + + {:else if icon === "shield"} + + {:else if icon === "music"} + + {:else if icon === "play"} + + {:else if icon === "headphones"} + + {:else if icon === "image"} + + {:else if icon === "openclaw"} + + {:else if icon === "chat"} + + {:else if icon === "git"} + + {:else if icon === "pdf"} + + {:else if icon === "lock"} + + {:else if icon === "speedtest"} + + {:else if icon === "users"} + + {:else if icon === "n8n"} + + {/if} + +{/snippet} + {#if sidebarOpen} {/if} -