All files / src App.svelte

0% Statements 0/177
0% Branches 0/1
0% Functions 0/1
0% Lines 0/177

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     
<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 Conversations from "./routes/Conversations.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 OPC from "./routes/OPC.svelte";
  import Transmission from "./routes/Transmission.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",
    "conversations",
    "settings",
    "security",
    "litellm",
    "cc-connect",
    "info-engine",
    "opc",
    "transmission",
  ]);
 
  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 === "conversations" && hasPageAccess("conversations")}
          <Conversations />
        {: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 === "opc" && hasPageAccess("opc")}
          <OPC />
        {:else if page === "transmission" && hasPageAccess("transmission")}
          <Transmission />
        {:else if page !== "terminal"}
          <Dashboard />
        {/if}
 
        {#if hasPageAccess("terminal")}
          <div class:hidden={page !== "terminal"}>
            <Terminal />
          </div>
        {/if}
      </div>
    </main>
  </div>
{/if}