diff --git a/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.js b/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.js index b6bfd2e4..66dd866b 100644 --- a/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.js +++ b/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.js @@ -13,6 +13,7 @@ import { CLIENT_PING_FAST_MS, } from "./endpointConstants"; import { clientPingUrl, clientPingAny } from "./endpointPing"; +import useSettingsStore from "@/store/settingsStore"; import EndpointRow from "./components/EndpointRow"; import StatusAlert from "./components/StatusAlert"; import Tooltip from "./components/Tooltip"; @@ -194,16 +195,15 @@ export default function APIPageClient({ machineId }) { const loadSettings = async () => { setTunnelChecking(true); try { - const [settingsRes, statusRes] = await Promise.all([ - fetch("/api/settings"), + const [settingsData, statusRes] = await Promise.all([ + useSettingsStore.getState().fetchSettings(), fetch("/api/tunnel/status", { cache: "no-store" }) ]); - if (settingsRes.ok) { - const data = await settingsRes.json(); - setRequireApiKey(data.requireApiKey || false); - setRequireLogin(data.requireLogin !== false); - setHasPassword(data.hasPassword || false); - setTunnelDashboardAccess(data.tunnelDashboardAccess || false); + if (settingsData) { + setRequireApiKey(settingsData.requireApiKey || false); + setRequireLogin(settingsData.requireLogin !== false); + setHasPassword(settingsData.hasPassword || false); + setTunnelDashboardAccess(settingsData.tunnelDashboardAccess || false); } if (statusRes.ok) { const data = await statusRes.json(); @@ -229,12 +229,8 @@ export default function APIPageClient({ machineId }) { const handleTunnelDashboardAccess = async (value) => { try { - const res = await fetch("/api/settings", { - method: "PATCH", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ tunnelDashboardAccess: value }), - }); - if (res.ok) setTunnelDashboardAccess(value); + const updated = await useSettingsStore.getState().patchSettings({ tunnelDashboardAccess: value }); + if (updated) setTunnelDashboardAccess(value); } catch (error) { console.log("Error updating tunnelDashboardAccess:", error); } @@ -242,12 +238,8 @@ export default function APIPageClient({ machineId }) { const handleRequireApiKey = async (value) => { try { - const res = await fetch("/api/settings", { - method: "PATCH", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ requireApiKey: value }), - }); - if (res.ok) setRequireApiKey(value); + const updated = await useSettingsStore.getState().patchSettings({ requireApiKey: value }); + if (updated) setRequireApiKey(value); } catch (error) { console.log("Error updating requireApiKey:", error); } diff --git a/src/app/(dashboard)/dashboard/usage/page.js b/src/app/(dashboard)/dashboard/usage/page.js index 2b43198b..3f47b644 100644 --- a/src/app/(dashboard)/dashboard/usage/page.js +++ b/src/app/(dashboard)/dashboard/usage/page.js @@ -2,7 +2,8 @@ import { Suspense, useState } from "react"; import { useSearchParams, useRouter } from "next/navigation"; -import { UsageStats, RequestLogger, CardSkeleton, SegmentedControl } from "@/shared/components"; +import { RequestLogger, CardSkeleton, SegmentedControl } from "@/shared/components"; +import UsageStats from "@/shared/components/UsageStats"; import RequestDetailsTab from "./components/RequestDetailsTab"; const PERIODS = [ diff --git a/src/shared/components/ChangelogModal.js b/src/shared/components/ChangelogModal.js index 591f6a9e..abc320a7 100644 --- a/src/shared/components/ChangelogModal.js +++ b/src/shared/components/ChangelogModal.js @@ -3,11 +3,8 @@ import { useEffect, useState, useRef } from "react"; import { createPortal } from "react-dom"; import PropTypes from "prop-types"; -import { marked } from "marked"; import { GITHUB_CONFIG } from "@/shared/constants/config"; -marked.setOptions({ gfm: true, breaks: true }); - export default function ChangelogModal({ isOpen, onClose }) { const [html, setHtml] = useState(""); const [loading, setLoading] = useState(false); @@ -18,12 +15,17 @@ export default function ChangelogModal({ isOpen, onClose }) { if (!isOpen || html) return; setLoading(true); setError(""); - fetch(GITHUB_CONFIG.changelogUrl) - .then((res) => { + Promise.all([ + fetch(GITHUB_CONFIG.changelogUrl).then((res) => { if (!res.ok) throw new Error(`HTTP ${res.status}`); return res.text(); + }), + import("marked"), + ]) + .then(([md, { marked }]) => { + marked.setOptions({ gfm: true, breaks: true }); + setHtml(marked.parse(md)); }) - .then((md) => setHtml(marked.parse(md))) .catch((err) => setError(err.message || "Failed to load")) .finally(() => setLoading(false)); }, [isOpen, html]); diff --git a/src/shared/components/Sidebar.js b/src/shared/components/Sidebar.js index 716f59fb..b3aa65a7 100644 --- a/src/shared/components/Sidebar.js +++ b/src/shared/components/Sidebar.js @@ -8,6 +8,7 @@ import { cn } from "@/shared/utils/cn"; import { APP_CONFIG, UPDATER_CONFIG } from "@/shared/constants/config"; import { MEDIA_PROVIDER_KINDS } from "@/shared/constants/providers"; import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard"; +import useSettingsStore from "@/store/settingsStore"; import Button from "./Button"; import { ConfirmModal } from "./Modal"; import NineRemotePromoModal from "./NineRemotePromoModal"; @@ -54,18 +55,20 @@ export default function Sidebar({ onClose }) { const INSTALL_CMD = UPDATER_CONFIG.installCmdLatest; useEffect(() => { - fetch("/api/settings") - .then(res => res.json()) - .then(data => { if (data.enableTranslator) setEnableTranslator(true); }) - .catch(() => {}); + useSettingsStore.getState().fetchSettings().then((data) => { + if (data?.enableTranslator) setEnableTranslator(true); + }); }, []); - // Lazy check for new npm version on mount + // Lazy check for new npm version in background after initial render useEffect(() => { - fetch("/api/version") - .then(res => res.json()) - .then(data => { if (data.hasUpdate) setUpdateInfo(data); }) - .catch(() => {}); + const timer = setTimeout(() => { + fetch("/api/version") + .then(res => res.json()) + .then(data => { if (data.hasUpdate) setUpdateInfo(data); }) + .catch(() => {}); + }, 2500); + return () => clearTimeout(timer); }, []); const isActive = (href) => { diff --git a/src/shared/components/UsageStats.js b/src/shared/components/UsageStats.js index bcac0bad..c4007812 100644 --- a/src/shared/components/UsageStats.js +++ b/src/shared/components/UsageStats.js @@ -15,11 +15,11 @@ import Card from "./Card"; import OverviewCards from "@/app/(dashboard)/dashboard/usage/components/OverviewCards"; import UsageTable, { fmt, fmtTime } from "@/app/(dashboard)/dashboard/usage/components/UsageTable"; import dynamic from "next/dynamic"; -// Lazy-load: keeps @xyflow/react out of the shared bundle until topology renders +// Lazy-load: keeps @xyflow/react and recharts out of the initial bundle const ProviderTopology = dynamic(() => import("@/app/(dashboard)/dashboard/usage/components/ProviderTopology"), { ssr: false }); -import UsageChart from "@/app/(dashboard)/dashboard/usage/components/UsageChart"; -import ProviderBarChart from "@/app/(dashboard)/dashboard/usage/components/ProviderBarChart"; -import TopModelsChart from "@/app/(dashboard)/dashboard/usage/components/TopModelsChart"; +const UsageChart = dynamic(() => import("@/app/(dashboard)/dashboard/usage/components/UsageChart"), { ssr: false }); +const ProviderBarChart = dynamic(() => import("@/app/(dashboard)/dashboard/usage/components/ProviderBarChart"), { ssr: false }); +const TopModelsChart = dynamic(() => import("@/app/(dashboard)/dashboard/usage/components/TopModelsChart"), { ssr: false }); function timeAgo(timestamp) { const diff = Math.floor((Date.now() - new Date(timestamp)) / 1000); diff --git a/src/shared/components/index.js b/src/shared/components/index.js index 3d508e91..e5698a63 100644 --- a/src/shared/components/index.js +++ b/src/shared/components/index.js @@ -18,7 +18,6 @@ export { default as ModelSelectModal } from "./ModelSelectModal"; export { default as ManualConfigModal } from "./ManualConfigModal"; export { default as ComboFormModal } from "./ComboFormModal"; export { default as McpMarketplaceModal } from "./McpMarketplaceModal"; -export { default as UsageStats } from "./UsageStats"; export { default as LanguageSwitcher } from "./LanguageSwitcher"; export { default as NineRemoteButton } from "./NineRemoteButton"; export { default as HeaderMenu } from "./HeaderMenu"; diff --git a/src/shared/components/layouts/DashboardLayout.js b/src/shared/components/layouts/DashboardLayout.js index aa555bd5..0f175f95 100644 --- a/src/shared/components/layouts/DashboardLayout.js +++ b/src/shared/components/layouts/DashboardLayout.js @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useState, useEffect } from "react"; import { usePathname } from "next/navigation"; import { useNotificationStore } from "@/store/notificationStore"; import Sidebar from "../Sidebar"; @@ -37,6 +37,24 @@ export default function DashboardLayout({ children }) { const notifications = useNotificationStore((state) => state.notifications); const removeNotification = useNotificationStore((state) => state.removeNotification); + // Preload heavy usage charts in background when browser is idle + useEffect(() => { + const preload = () => { + import("@/shared/components/UsageStats").catch(() => {}); + import("@/app/(dashboard)/dashboard/usage/components/UsageChart").catch(() => {}); + import("@/app/(dashboard)/dashboard/usage/components/ProviderBarChart").catch(() => {}); + import("@/app/(dashboard)/dashboard/usage/components/TopModelsChart").catch(() => {}); + }; + if (typeof window !== "undefined") { + if ("requestIdleCallback" in window) { + const id = window.requestIdleCallback(preload, { timeout: 4000 }); + return () => window.cancelIdleCallback(id); + } + const timer = setTimeout(preload, 2500); + return () => clearTimeout(timer); + } + }, []); + return (