Files
nas-tools/dashboard/frontend/src/App.svelte

223 lines
7.5 KiB
Svelte

<script>
import Sidebar from "./components/Sidebar.svelte";
import Dashboard from "./routes/Dashboard.svelte";
import Docker from "./routes/Docker.svelte";
import Gitea from "./routes/Gitea.svelte";
import Files from "./routes/Files.svelte";
import Terminal from "./routes/Terminal.svelte";
import OpenClaw from "./routes/OpenClaw.svelte";
import Settings from "./routes/Settings.svelte";
import ChatSummary from "./routes/ChatSummary.svelte";
import Security from "./routes/Security.svelte";
import LiteLLM from "./routes/LiteLLM.svelte";
import CcConnect from "./routes/CcConnect.svelte";
import InfoEngine from "./routes/InfoEngine.svelte";
import Login from "./routes/Login.svelte";
import { onMount } from "svelte";
import { getToken, checkAuth, setToken, currentUser, setCurrentUser, getPreferences, savePreferences, tryRefreshSession, logout as logoutSession } from "./lib/api.js";
const KNOWN_PAGES = new Set([
"dashboard",
"docker",
"gitea",
"files",
"terminal",
"openclaw",
"chat-digest",
"settings",
"security",
"litellm",
"cc-connect",
"info-engine",
]);
let page = $state("dashboard");
let dark = $state(false);
let sidebarOpen = $state(false);
let authorized = $state(false);
let loading = $state(true);
let userRole = $state("");
let allowedPages = $state([]);
let allowedSidebarLinks = $state("*");
let sidebarOrder = $state(null);
let orderSaveTimer = null;
function getRequestedPage() {
const requestedPage = new URLSearchParams(window.location.search).get("page");
return KNOWN_PAGES.has(requestedPage) ? requestedPage : null;
}
function canOpenPage(pageId) {
if (!pageId || !KNOWN_PAGES.has(pageId)) return false;
if (pageId === "settings") return userRole === "admin";
if (["litellm", "cc-connect", "info-engine"].includes(pageId)) return hasPageAccess("dashboard");
return hasPageAccess(pageId);
}
function syncPageQuery(pageId) {
const url = new URL(window.location.href);
if (pageId === "dashboard") url.searchParams.delete("page");
else url.searchParams.set("page", pageId);
if (pageId !== "terminal") url.searchParams.delete("host");
history.replaceState({}, "", `${url.pathname}${url.search}${url.hash}`);
}
async function fetchUserInfo() {
try {
const data = await checkAuth();
setCurrentUser(data);
userRole = data.role || "admin";
allowedPages = data.pages || "*";
allowedSidebarLinks = data.sidebar_links || "*";
// 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((e) => {
console.error("Failed to save sidebar order:", e);
});
}, 500);
}
function hasPageAccess(pageId) {
if (allowedPages === "*") return true;
return Array.isArray(allowedPages) && allowedPages.includes(pageId);
}
onMount(async () => {
// Theme init
if (localStorage.theme === 'dark' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
dark = true;
document.documentElement.classList.add('dark');
} else {
dark = false;
document.documentElement.classList.remove('dark');
}
// Try proxy auth first (Authelia forward-auth sets Remote-User header)
try {
const proxyRes = await fetch("/api/auth/proxy", { credentials: "same-origin" });
if (proxyRes.ok) {
const data = await proxyRes.json();
setToken(data.access_token);
authorized = true;
await fetchUserInfo();
const requestedPage = getRequestedPage();
if (canOpenPage(requestedPage)) page = requestedPage;
loading = false;
return;
}
} catch (e) {
// Proxy auth not available, fall through to regular auth
}
// Regular token auth check — always try refresh to get fresh cookies
const refreshed = await tryRefreshSession();
if (!refreshed && !getToken()) {
loading = false;
authorized = false;
return;
}
try {
await fetchUserInfo();
authorized = true;
const requestedPage = getRequestedPage();
if (canOpenPage(requestedPage)) page = requestedPage;
} catch (e) {
console.error("Auth check failed:", e);
setToken("");
authorized = false;
} finally {
loading = false;
}
});
$effect(() => {
if (!authorized || loading || !canOpenPage(page)) return;
syncPageQuery(page);
});
function toggleTheme() {
dark = !dark;
if (dark) {
document.documentElement.classList.add("dark");
localStorage.theme = 'dark';
} else {
document.documentElement.classList.remove("dark");
localStorage.theme = 'light';
}
}
async function logout() {
try {
await logoutSession();
} catch (e) {
console.error("Logout failed:", e);
}
setToken("");
window.location.reload();
}
</script>
{#if loading}
<div class="flex h-screen items-center justify-center bg-surface-50 dark:bg-surface-950">
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600"></div>
</div>
{:else if !authorized}
<Login />
{:else}
<div class="flex h-screen overflow-hidden bg-surface-50 dark:bg-surface-900">
<Sidebar bind:page {dark} {toggleTheme} {logout} bind:sidebarOpen {allowedPages} {allowedSidebarLinks} {userRole} {sidebarOrder} onOrderChange={handleOrderChange} />
<main class="flex-1 overflow-auto">
<div class="max-w-[1400px] mx-auto p-6 lg:p-8">
<!-- Mobile hamburger -->
<button class="md:hidden mb-4 p-2 rounded-lg text-surface-600 dark:text-surface-300 hover:bg-surface-200 dark:hover:bg-surface-700" onclick={() => sidebarOpen = true} aria-label="Open menu">
<svg class="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h16M4 18h16" /></svg>
</button>
{#if page === "dashboard" && hasPageAccess("dashboard")}
<Dashboard />
{:else if page === "docker" && hasPageAccess("docker")}
<Docker />
{:else if page === "gitea" && hasPageAccess("gitea")}
<Gitea />
{:else if page === "files" && hasPageAccess("files")}
<Files />
{:else if page === "openclaw" && hasPageAccess("openclaw")}
<OpenClaw />
{:else if page === "chat-digest" && hasPageAccess("chat-digest")}
<ChatSummary />
{:else if page === "settings" && userRole === "admin"}
<Settings />
{:else if page === "security" && hasPageAccess("security")}
<Security />
{:else if page === "litellm" && hasPageAccess("dashboard")}
<LiteLLM />
{:else if page === "cc-connect" && hasPageAccess("dashboard")}
<CcConnect />
{:else if page === "info-engine" && hasPageAccess("dashboard")}
<InfoEngine />
{:else if page !== "terminal"}
<Dashboard />
{/if}
{#if hasPageAccess("terminal")}
<div class:hidden={page !== "terminal"}>
<Terminal />
</div>
{/if}
</div>
</main>
</div>
{/if}