diff --git a/.gitignore b/.gitignore index 4751dac..9704051 100644 --- a/.gitignore +++ b/.gitignore @@ -59,3 +59,7 @@ Thanks.md PUBLIC.en.md PR/* package-lock.json + + +#Ignore vscode AI rules +.github/instructions/codacy.instructions.md diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..5a586b3 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "css.lint.unknownAtRules": "ignore" +} diff --git a/src/app/(dashboard)/dashboard/cli-tools/components/ClaudeToolCard.js b/src/app/(dashboard)/dashboard/cli-tools/components/ClaudeToolCard.js index 7ed642c..b15caf9 100644 --- a/src/app/(dashboard)/dashboard/cli-tools/components/ClaudeToolCard.js +++ b/src/app/(dashboard)/dashboard/cli-tools/components/ClaudeToolCard.js @@ -46,14 +46,14 @@ export default function ClaudeToolCard({ if (apiKeys?.length > 0 && !selectedApiKey) { setSelectedApiKey(apiKeys[0].key); } - }, [apiKeys]); + }, [apiKeys, selectedApiKey]); useEffect(() => { if (isExpanded && !claudeStatus) { checkClaudeStatus(); fetchModelAliases(); } - }, [isExpanded]); + }, [isExpanded, claudeStatus]); const fetchModelAliases = async () => { try { @@ -80,7 +80,7 @@ export default function ClaudeToolCard({ setSelectedApiKey(tokenFromFile); } } - }, [claudeStatus, apiKeys]); + }, [claudeStatus, apiKeys, tool.defaultModels, onModelMappingChange]); const checkClaudeStatus = async () => { setCheckingClaude(true); diff --git a/src/app/(dashboard)/dashboard/cli-tools/components/CodexToolCard.js b/src/app/(dashboard)/dashboard/cli-tools/components/CodexToolCard.js index 5a5688a..fee7b34 100644 --- a/src/app/(dashboard)/dashboard/cli-tools/components/CodexToolCard.js +++ b/src/app/(dashboard)/dashboard/cli-tools/components/CodexToolCard.js @@ -21,14 +21,14 @@ export default function CodexToolCard({ tool, isExpanded, onToggle, baseUrl, api if (apiKeys?.length > 0 && !selectedApiKey) { setSelectedApiKey(apiKeys[0].key); } - }, [apiKeys]); + }, [apiKeys, selectedApiKey]); useEffect(() => { if (isExpanded && !codexStatus) { checkCodexStatus(); fetchModelAliases(); } - }, [isExpanded]); + }, [isExpanded, codexStatus]); const fetchModelAliases = async () => { try { diff --git a/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.js b/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.js index ea7dc10..4d4e1fe 100644 --- a/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.js +++ b/src/app/(dashboard)/dashboard/endpoint/EndpointPageClient.js @@ -18,7 +18,7 @@ export default function APIPageClient({ machineId }) { const [showCloudModal, setShowCloudModal] = useState(false); const [showDisableModal, setShowDisableModal] = useState(false); const [cloudSyncing, setCloudSyncing] = useState(false); - const [cloudStatus, setCloudStatus] = useState(null); + const [setCloudStatus] = useState(null); const [syncStep, setSyncStep] = useState(""); // "syncing" | "verifying" | "disabling" | "" const { copied, copy } = useCopyToClipboard(); @@ -110,7 +110,7 @@ export default function APIPageClient({ machineId }) { try { // Step 1: Sync latest data from cloud - const syncRes = await fetch("/api/sync/cloud", { + await fetch("/api/sync/cloud", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ action: "sync" }) @@ -198,13 +198,9 @@ export default function APIPageClient({ machineId }) { } }; - const isLocalhost = typeof window !== "undefined" && - (window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1"); const baseUrl = typeof window !== "undefined" ? `${window.location.origin}/v1` : "/v1"; - const localApiKey = "HELLO"; // New format: /v1 (machineId in key), Old format: /{machineId}/v1 const cloudEndpointNew = `${CLOUD_URL}/v1`; - const cloudEndpointOld = `${CLOUD_URL}/${machineId}/v1`; if (loading) { return ( @@ -244,7 +240,7 @@ export default function APIPageClient({ machineId }) { icon="cloud_off" onClick={() => handleCloudToggle(false)} disabled={cloudSyncing} - className="!bg-red-500/10 !text-red-500 hover:!bg-red-500/20 !border-red-500/30" + className="bg-red-500/10! text-red-500! hover:bg-red-500/20! border-red-500/30!" > Disable Cloud @@ -254,7 +250,7 @@ export default function APIPageClient({ machineId }) { icon="cloud_upload" onClick={() => handleCloudToggle(true)} disabled={cloudSyncing} - className="bg-gradient-to-r from-primary to-blue-500 hover:from-primary-hover hover:to-blue-600" + className="bg-linear-to-r from-primary to-blue-500 hover:from-primary-hover hover:to-blue-600" > Enable Cloud @@ -371,7 +367,7 @@ export default function APIPageClient({ machineId }) { icon="cloud_off" onClick={() => handleCloudToggle(false)} disabled={cloudSyncing} - className="!bg-red-500/10 !text-red-500 hover:!bg-red-500/20 !border-red-500/30" + className="bg-red-500/10! text-red-500! hover:bg-red-500/20! border-red-500/30!" > Disable @@ -381,7 +377,7 @@ export default function APIPageClient({ machineId }) { icon="cloud_upload" onClick={() => handleCloudToggle(true)} disabled={cloudSyncing} - className="bg-gradient-to-r from-primary to-blue-500 hover:from-primary-hover hover:to-blue-600 px-6" + className="bg-linear-to-r from-primary to-blue-500 hover:from-primary-hover hover:to-blue-600 px-6" > Enable Cloud @@ -580,7 +576,7 @@ export default function APIPageClient({ machineId }) { onClick={handleConfirmDisable} fullWidth disabled={cloudSyncing} - className="!bg-red-500 hover:!bg-red-600 !text-white" + className="bg-red-500! hover:bg-red-600! text-white!" > {cloudSyncing ? ( @@ -602,4 +598,8 @@ export default function APIPageClient({ machineId }) { ); -} \ No newline at end of file +} + +APIPageClient.propTypes = { + machineId: import("prop-types").string.isRequired, +}; \ No newline at end of file diff --git a/src/app/(dashboard)/dashboard/providers/[id]/page.js b/src/app/(dashboard)/dashboard/providers/[id]/page.js index 0fbb56a..2780f5a 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/page.js +++ b/src/app/(dashboard)/dashboard/providers/[id]/page.js @@ -30,9 +30,9 @@ export default function ProviderDetailPage() { useEffect(() => { fetchConnections(); fetchAliases(); - }, [providerId]); + }, [fetchConnections, fetchAliases]); - const fetchAliases = async () => { + const fetchAliases = useCallback(async () => { try { const res = await fetch("/api/models/alias"); const data = await res.json(); @@ -42,7 +42,7 @@ export default function ProviderDetailPage() { } catch (error) { console.log("Error fetching aliases:", error); } - }; + }, []); const handleSetAlias = async (modelId, alias) => { const fullModel = `${providerAlias}/${modelId}`; @@ -76,7 +76,7 @@ export default function ProviderDetailPage() { } }; - const fetchConnections = async () => { + const fetchConnections = useCallback(async () => { try { const res = await fetch("/api/providers"); const data = await res.json(); @@ -89,7 +89,7 @@ export default function ProviderDetailPage() { } finally { setLoading(false); } - }; + }, [providerId]); const handleDelete = async (id) => { if (!confirm("Delete this connection?")) return; @@ -228,11 +228,13 @@ export default function ProviderDetailPage() { className="rounded-lg flex items-center justify-center" style={{ backgroundColor: `${providerInfo.color}15` }} > - {providerInfo.name} { e.target.style.display = "none"; }} + width={48} + height={48} + className="object-contain rounded-lg" + onError={(e) => { e.currentTarget.style.display = "none"; }} />
diff --git a/src/app/(dashboard)/dashboard/providers/page.js b/src/app/(dashboard)/dashboard/providers/page.js index 8daa968..ebee4b4 100644 --- a/src/app/(dashboard)/dashboard/providers/page.js +++ b/src/app/(dashboard)/dashboard/providers/page.js @@ -1,6 +1,7 @@ "use client"; import { useState, useEffect } from "react"; +import Image from "next/image"; import { Card, CardSkeleton, Badge } from "@/shared/components"; import { OAUTH_PROVIDERS, APIKEY_PROVIDERS } from "@/shared/constants/config"; import Link from "next/link"; @@ -11,21 +12,20 @@ export default function ProvidersPage() { const [loading, setLoading] = useState(true); useEffect(() => { + const fetchData = async () => { + try { + const res = await fetch("/api/providers"); + const data = await res.json(); + if (res.ok) setConnections(data.connections || []); + } catch (error) { + console.log("Error fetching data:", error); + } finally { + setLoading(false); + } + }; fetchData(); }, []); - const fetchData = async () => { - try { - const res = await fetch("/api/providers"); - const data = await res.json(); - if (res.ok) setConnections(data.connections || []); - } catch (error) { - console.log("Error fetching data:", error); - } finally { - setLoading(false); - } - }; - const getProviderStats = (providerId, authType) => { const providerConnections = connections.filter( c => c.provider === providerId && c.authType === authType @@ -141,10 +141,12 @@ function ProviderCard({ providerId, provider, stats }) { style={{ backgroundColor: `${provider.color}15` }} > {!imgError ? ( - {provider.name} setImgError(true)} /> ) : ( diff --git a/src/app/(dashboard)/dashboard/usage/page.js b/src/app/(dashboard)/dashboard/usage/page.js index ba55033..a02d310 100644 --- a/src/app/(dashboard)/dashboard/usage/page.js +++ b/src/app/(dashboard)/dashboard/usage/page.js @@ -1,7 +1,7 @@ "use client"; import { useState, Suspense } from "react"; -import { UsageStats, RequestLogger } from "@/shared/components"; +import { UsageStats, RequestLogger, CardSkeleton } from "@/shared/components"; export default function UsagePage() { const [activeTab, setActiveTab] = useState("overview"); @@ -33,9 +33,13 @@ export default function UsagePage() {
{/* Content */} - Loading...}> - {activeTab === "overview" ? : } - + {activeTab === "overview" ? ( + }> + + + ) : ( + + )} ); } diff --git a/src/app/api/auth/login/route.js b/src/app/api/auth/login/route.js index dddbd4e..f6cbdb0 100644 --- a/src/app/api/auth/login/route.js +++ b/src/app/api/auth/login/route.js @@ -18,7 +18,9 @@ export async function POST(request) { let isValid = false; if (!storedHash) { - isValid = password === "123456"; + // Use env var or default + const initialPassword = process.env.INITIAL_PASSWORD || "123456"; + isValid = password === initialPassword; } else { isValid = await bcrypt.compare(password, storedHash); } diff --git a/src/app/globals.css b/src/app/globals.css index c20e6d7..282048b 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,3 +1,4 @@ +@import url('https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap'); @import "tailwindcss"; @custom-variant dark (&:where(.dark, .dark *)); diff --git a/src/app/landing/components/HowItWorks.js b/src/app/landing/components/HowItWorks.js index d5c381a..c9b2b7a 100644 --- a/src/app/landing/components/HowItWorks.js +++ b/src/app/landing/components/HowItWorks.js @@ -13,7 +13,7 @@ export default function HowItWorks() {
{/* Connection line */} -
+
{/* Step 1: CLI & SDKs */}
diff --git a/src/app/landing/components/Navigation.js b/src/app/landing/components/Navigation.js index 4d20d2f..e6887f1 100644 --- a/src/app/landing/components/Navigation.js +++ b/src/app/landing/components/Navigation.js @@ -11,7 +11,7 @@ export default function Navigation() {
{/* Logo */}
router.push("/")}> -
+
hub

9Router

diff --git a/src/app/landing/page.js b/src/app/landing/page.js index 8bab89a..0887505 100644 --- a/src/app/landing/page.js +++ b/src/app/landing/page.js @@ -47,7 +47,7 @@ export default function LandingPage() { {/* CTA Section */}
-
+

Ready to Simplify Your AI Infrastructure?

diff --git a/src/app/layout.js b/src/app/layout.js index 8c9c52d..45521e4 100644 --- a/src/app/layout.js +++ b/src/app/layout.js @@ -17,10 +17,7 @@ export default function RootLayout({ children }) { return ( - + diff --git a/src/lib/localDb.js b/src/lib/localDb.js index 8055f1f..5f06aa5 100644 --- a/src/lib/localDb.js +++ b/src/lib/localDb.js @@ -16,6 +16,8 @@ function getAppName() { function getUserDataDir() { if (isCloud) return "/tmp"; // Fallback for Workers + if (process.env.DATA_DIR) return process.env.DATA_DIR; + const platform = process.platform; const homeDir = os.homedir(); const appName = getAppName(); diff --git a/src/shared/components/Header.js b/src/shared/components/Header.js index f08ad5b..8d1e91b 100644 --- a/src/shared/components/Header.js +++ b/src/shared/components/Header.js @@ -2,6 +2,7 @@ import { usePathname } from "next/navigation"; import Link from "next/link"; +import Image from "next/image"; import { useRouter } from "next/navigation"; import { ThemeToggle } from "@/shared/components"; import { APP_CONFIG, OAUTH_PROVIDERS, APIKEY_PROVIDERS } from "@/shared/constants/config"; @@ -88,11 +89,13 @@ export default function Header({ onMenuClick, showMenuButton = true }) { ) : (

{crumb.image && ( - {crumb.label} { e.target.style.display = "none"; }} + width={28} + height={28} + className="object-contain rounded" + onError={(e) => { e.currentTarget.style.display = "none"; }} /> )}

diff --git a/src/shared/components/ModelSelectModal.js b/src/shared/components/ModelSelectModal.js index 99a208e..8df74c0 100644 --- a/src/shared/components/ModelSelectModal.js +++ b/src/shared/components/ModelSelectModal.js @@ -1,6 +1,7 @@ "use client"; import { useState, useMemo, useEffect } from "react"; +import PropTypes from "prop-types"; import Modal from "./Modal"; import { getModelsByProviderId, PROVIDER_ID_TO_ALIAS } from "@/shared/constants/models"; import { OAUTH_PROVIDERS, APIKEY_PROVIDERS } from "@/shared/constants/providers"; @@ -33,7 +34,7 @@ export default function ModelSelectModal({ } }, [isOpen]); - const allProviders = { ...OAUTH_PROVIDERS, ...APIKEY_PROVIDERS }; + const allProviders = useMemo(() => ({ ...OAUTH_PROVIDERS, ...APIKEY_PROVIDERS }), []); // Group models by provider with priority order const groupedModels = useMemo(() => { @@ -141,7 +142,7 @@ export default function ModelSelectModal({ }} title={title} size="md" - className="!p-4" + className="p-4!" > {/* Search - compact */}
@@ -246,3 +247,17 @@ export default function ModelSelectModal({ ); } +ModelSelectModal.propTypes = { + isOpen: PropTypes.bool.isRequired, + onClose: PropTypes.func.isRequired, + onSelect: PropTypes.func.isRequired, + selectedModel: PropTypes.string, + activeProviders: PropTypes.arrayOf( + PropTypes.shape({ + provider: PropTypes.string.isRequired, + }) + ), + title: PropTypes.string, + modelAliases: PropTypes.object, +}; + diff --git a/src/shared/components/OAuthModal.js b/src/shared/components/OAuthModal.js index 5a4ab3e..65163b7 100644 --- a/src/shared/components/OAuthModal.js +++ b/src/shared/components/OAuthModal.js @@ -36,7 +36,7 @@ export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess, // Auto start OAuth startOAuthFlow(); } - }, [isOpen, provider]); + }, [isOpen, provider, startOAuthFlow]); // Listen for OAuth callback via multiple methods const callbackProcessedRef = useRef(false); @@ -114,10 +114,11 @@ export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess, window.removeEventListener("storage", handleStorage); if (channel) channel.close(); }; - }, [authData]); + }, [authData, exchangeTokens]); // Exchange tokens - const exchangeTokens = async (code, state) => { + const exchangeTokens = useCallback(async (code, state) => { + if (!authData) return; try { const res = await fetch(`/api/oauth/${provider}/exchange`, { method: "POST", @@ -139,10 +140,54 @@ export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess, setError(err.message); setStep("error"); } - }; + }, [authData, provider, onSuccess]); + + // Poll for device code token + const startPolling = useCallback(async (deviceCode, codeVerifier, interval, extraData) => { + setPolling(true); + const maxAttempts = 60; + + for (let i = 0; i < maxAttempts; i++) { + await new Promise((r) => setTimeout(r, interval * 1000)); + + try { + const res = await fetch(`/api/oauth/${provider}/poll`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ deviceCode, codeVerifier, extraData }), + }); + + const data = await res.json(); + + if (data.success) { + setStep("success"); + setPolling(false); + onSuccess?.(); + return; + } + + if (data.error === "expired_token" || data.error === "access_denied") { + throw new Error(data.errorDescription || data.error); + } + + if (data.error === "slow_down") { + interval = Math.min(interval + 5, 30); + } + } catch (err) { + setError(err.message); + setStep("error"); + setPolling(false); + return; + } + } + + setError("Authorization timeout"); + setStep("error"); + setPolling(false); + }, [provider, onSuccess]); // Start OAuth flow - const startOAuthFlow = async () => { + const startOAuthFlow = useCallback(async () => { if (!provider) return; try { setError(null); @@ -207,51 +252,7 @@ export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess, setError(err.message); setStep("error"); } - }; - - // Poll for device code token - const startPolling = async (deviceCode, codeVerifier, interval, extraData) => { - setPolling(true); - const maxAttempts = 60; - - for (let i = 0; i < maxAttempts; i++) { - await new Promise((r) => setTimeout(r, interval * 1000)); - - try { - const res = await fetch(`/api/oauth/${provider}/poll`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ deviceCode, codeVerifier, extraData }), - }); - - const data = await res.json(); - - if (data.success) { - setStep("success"); - setPolling(false); - onSuccess?.(); - return; - } - - if (data.error === "expired_token" || data.error === "access_denied") { - throw new Error(data.errorDescription || data.error); - } - - if (data.error === "slow_down") { - interval = Math.min(interval + 5, 30); - } - } catch (err) { - setError(err.message); - setStep("error"); - setPolling(false); - return; - } - } - - setError("Authorization timeout"); - setStep("error"); - setPolling(false); - }; + }, [provider, isLocalhost, startPolling]); // Handle manual URL input const handleManualSubmit = async () => { diff --git a/src/shared/components/Sidebar.js b/src/shared/components/Sidebar.js index 2ef629a..c1b549b 100644 --- a/src/shared/components/Sidebar.js +++ b/src/shared/components/Sidebar.js @@ -1,6 +1,7 @@ "use client"; import { useState } from "react"; +import PropTypes from "prop-types"; import Link from "next/link"; import { usePathname } from "next/navigation"; import { cn } from "@/shared/utils/cn"; @@ -38,7 +39,7 @@ export default function Sidebar({ onClose }) { try { await fetch("/api/shutdown", { method: "POST" }); } catch (e) { - // Expected to fail as server shuts down + // Expected to fail as server shuts down; ignore error } setIsShuttingDown(false); setShowShutdownModal(false); @@ -51,7 +52,7 @@ export default function Sidebar({ onClose }) { {/* Logo */}
-
+
hub

@@ -161,7 +162,7 @@ export default function Sidebar({ onClose }) {

Server Disconnected

The proxy server has been stopped.

-
@@ -170,3 +171,7 @@ export default function Sidebar({ onClose }) { ); } + +Sidebar.propTypes = { + onClose: PropTypes.func, +}; diff --git a/src/shared/components/UsageStats.js b/src/shared/components/UsageStats.js index da405e7..c9937be 100644 --- a/src/shared/components/UsageStats.js +++ b/src/shared/components/UsageStats.js @@ -52,7 +52,7 @@ export default function UsageStats() { router.replace(`?${params.toString()}`, { scroll: false }); }; - const sortData = (dataMap, pendingMap = {}) => { + const sortData = useCallback((dataMap, pendingMap = {}) => { return Object.entries(dataMap || {}) .map(([key, data]) => { const totalTokens = @@ -91,11 +91,11 @@ export default function UsageStats() { if (valA > valB) return sortOrder === "asc" ? 1 : -1; return 0; }); - }; + }, [sortBy, sortOrder]); const sortedModels = useMemo( () => sortData(stats?.byModel, stats?.pending?.byModel), - [stats?.byModel, stats?.pending?.byModel, sortBy, sortOrder] + [stats?.byModel, stats?.pending?.byModel, sortData] ); const sortedAccounts = useMemo(() => { // For accounts, pendingMap is by connectionId, but dataMap is by accountKey @@ -114,7 +114,7 @@ export default function UsageStats() { }); } return sortData(stats?.byAccount, accountPendingMap); - }, [stats?.byAccount, stats?.pending?.byAccount, sortBy, sortOrder]); + }, [stats?.byAccount, stats?.pending?.byAccount, sortData]); const fetchStats = useCallback(async (showLoading = true) => { if (showLoading) setLoading(true); diff --git a/src/shared/utils/api.js b/src/shared/utils/api.js index 2e0ad90..5712ef9 100644 --- a/src/shared/utils/api.js +++ b/src/shared/utils/api.js @@ -88,5 +88,6 @@ async function handleResponse(response) { return data; } -export default { get, post, put, del }; +const api = { get, post, put, del }; +export default api;