8faba05b5b
Deploy Claude Dev (Dev) / deploy-claude-dev-dev (push) Failing after 2m3s
Deploy Dashboard (Main) / Backend Tests (push) Successful in 3m7s
Deploy Dashboard (Main) / Frontend Tests (push) Successful in 6s
Deploy Dashboard (Main) / Deploy to Production (push) Has been cancelled
Deploy Dashboard (Main) / Build Main Image (push) Has been cancelled
Deploy Dashboard (Dev) / Backend Tests (push) Successful in 1m18s
Deploy Dashboard (Dev) / Frontend Tests (push) Successful in 51s
Deploy Dashboard (Dev) / Build Dev Image (push) Successful in 1m42s
Deploy Dashboard (Dev) / Deploy to Dev (push) Successful in 58s
252 lines
10 KiB
Svelte
252 lines
10 KiB
Svelte
<script>
|
|
import Sidebar from "./components/Sidebar.svelte";
|
|
import CommandPalette from "./components/CommandPalette.svelte";
|
|
import Dashboard from "./routes/Dashboard.svelte";
|
|
import Gitea from "./routes/tools/Gitea.svelte";
|
|
import Files from "./routes/tools/Files.svelte";
|
|
import Terminal from "./routes/tools/Terminal.svelte";
|
|
import Settings from "./routes/admin/Settings.svelte";
|
|
import ChatSummary from "./routes/tools/ChatSummary.svelte";
|
|
import Conversations from "./routes/tools/Conversations.svelte";
|
|
import Security from "./routes/admin/Security.svelte";
|
|
import LiteLLM from "./routes/tools/LiteLLM.svelte";
|
|
import OPC from "./routes/tools/OPC.svelte";
|
|
import Transmission from "./routes/media/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",
|
|
"gitea",
|
|
"files",
|
|
"terminal",
|
|
"chat-digest",
|
|
"conversations",
|
|
"settings",
|
|
"security",
|
|
"litellm",
|
|
"opc",
|
|
"transmission",
|
|
]);
|
|
|
|
const navItems = [
|
|
// Main
|
|
{ id: "dashboard", label: "Overview", section: "main", description: "System stats, service health, CI runs" },
|
|
{ id: "litellm", label: "LiteLLM", section: "main", description: "LLM proxy and model management" },
|
|
{ id: "opc", label: "OPC", section: "main", description: "Open Project Console — kanban and agents" },
|
|
{ id: "files", label: "Files", section: "main", description: "NAS file browser" },
|
|
{ id: "terminal", label: "Terminal", section: "main", description: "SSH terminal to NAS" },
|
|
{ id: "security", label: "Security", section: "main", description: "WebAuthn passkeys and TOTP" },
|
|
// Media
|
|
{ id: "transmission", label: "Transmission", section: "media", description: "BitTorrent client" },
|
|
{ label: "Navidrome", section: "media", remoteHref: "https://music.jimmygan.com", description: "Music streaming" },
|
|
{ label: "Jellyfin", section: "media", remoteHref: "https://media.jimmygan.com", description: "Media server" },
|
|
{ label: "Audiobookshelf", section: "media", remoteHref: "https://books.jimmygan.com", description: "Audiobooks & podcasts" },
|
|
{ label: "Immich", section: "media", remoteHref: "https://photos.jimmygan.com", description: "Photo and video backup" },
|
|
{ label: "t-youtube", section: "media", remoteHref: "https://yt.jimmygan.com", description: "YouTube subscription reader" },
|
|
{ label: "Media Management", section: "media", remoteHref: "https://media.jimmygan.com", description: "Media library management" },
|
|
// Tools
|
|
{ id: "chat-digest", label: "Chat Digest", section: "tools", description: "Telegram group summaries" },
|
|
{ id: "conversations", label: "Code Sessions", section: "tools", description: "Claude Code session browser" },
|
|
{ id: "gitea", label: "Repos", section: "tools", description: "Gitea repository list" },
|
|
{ label: "Gitea Web", section: "tools", remoteHref: "https://git.jimmygan.com", description: "Gitea web interface" },
|
|
{ label: "Hermes", section: "tools", remoteHref: "https://hermes-agent.nousresearch.com/docs", description: "Hermes Agent docs" },
|
|
{ label: "Stirling PDF", section: "tools", remoteHref: "https://pdf.jimmygan.com", description: "PDF tools" },
|
|
{ label: "Vaultwarden", section: "tools", remoteHref: "https://vault.jimmygan.com", description: "Password manager" },
|
|
{ label: "n8n", section: "tools", remoteHref: "https://n8n.jimmygan.com", description: "Workflow automation" },
|
|
{ label: "Speedtest", section: "tools", remoteHref: "https://speed.jimmygan.com", description: "Network speed test" },
|
|
{ label: "OpenConnector", section: "tools", remoteHref: "https://connector.jimmygan.com", description: "Credential broker for AI agents" },
|
|
// Settings
|
|
{ id: "settings", label: "Settings", section: "tools", description: "Dashboard configuration" },
|
|
];
|
|
|
|
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"].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} />
|
|
<CommandPalette items={navItems} onNavigate={(id) => page = id} />
|
|
<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 === "gitea" && hasPageAccess("gitea")}
|
|
<Gitea />
|
|
{:else if page === "files" && hasPageAccess("files")}
|
|
<Files />
|
|
{: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 === "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}
|