chore: fix build warnings, add deployment config, and cleanup lint errors

- Fix React Hook dependencies and Image optimization warnings
- Add DATA_DIR and INITIAL_PASSWORD env var support
- Fix Tailwind v4 legacy syntax and suppress CSS directives warnings
- Add PropTypes and remove unused variables
This commit is contained in:
apeltekci 2026-01-19 02:15:11 -08:00 committed by decolua
parent 0943387d24
commit 0848dd5d13
21 changed files with 156 additions and 114 deletions

4
.gitignore vendored
View file

@ -59,3 +59,7 @@ Thanks.md
PUBLIC.en.md PUBLIC.en.md
PR/* PR/*
package-lock.json package-lock.json
#Ignore vscode AI rules
.github/instructions/codacy.instructions.md

3
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,3 @@
{
"css.lint.unknownAtRules": "ignore"
}

View file

@ -46,14 +46,14 @@ export default function ClaudeToolCard({
if (apiKeys?.length > 0 && !selectedApiKey) { if (apiKeys?.length > 0 && !selectedApiKey) {
setSelectedApiKey(apiKeys[0].key); setSelectedApiKey(apiKeys[0].key);
} }
}, [apiKeys]); }, [apiKeys, selectedApiKey]);
useEffect(() => { useEffect(() => {
if (isExpanded && !claudeStatus) { if (isExpanded && !claudeStatus) {
checkClaudeStatus(); checkClaudeStatus();
fetchModelAliases(); fetchModelAliases();
} }
}, [isExpanded]); }, [isExpanded, claudeStatus]);
const fetchModelAliases = async () => { const fetchModelAliases = async () => {
try { try {
@ -80,7 +80,7 @@ export default function ClaudeToolCard({
setSelectedApiKey(tokenFromFile); setSelectedApiKey(tokenFromFile);
} }
} }
}, [claudeStatus, apiKeys]); }, [claudeStatus, apiKeys, tool.defaultModels, onModelMappingChange]);
const checkClaudeStatus = async () => { const checkClaudeStatus = async () => {
setCheckingClaude(true); setCheckingClaude(true);

View file

@ -21,14 +21,14 @@ export default function CodexToolCard({ tool, isExpanded, onToggle, baseUrl, api
if (apiKeys?.length > 0 && !selectedApiKey) { if (apiKeys?.length > 0 && !selectedApiKey) {
setSelectedApiKey(apiKeys[0].key); setSelectedApiKey(apiKeys[0].key);
} }
}, [apiKeys]); }, [apiKeys, selectedApiKey]);
useEffect(() => { useEffect(() => {
if (isExpanded && !codexStatus) { if (isExpanded && !codexStatus) {
checkCodexStatus(); checkCodexStatus();
fetchModelAliases(); fetchModelAliases();
} }
}, [isExpanded]); }, [isExpanded, codexStatus]);
const fetchModelAliases = async () => { const fetchModelAliases = async () => {
try { try {

View file

@ -18,7 +18,7 @@ export default function APIPageClient({ machineId }) {
const [showCloudModal, setShowCloudModal] = useState(false); const [showCloudModal, setShowCloudModal] = useState(false);
const [showDisableModal, setShowDisableModal] = useState(false); const [showDisableModal, setShowDisableModal] = useState(false);
const [cloudSyncing, setCloudSyncing] = useState(false); const [cloudSyncing, setCloudSyncing] = useState(false);
const [cloudStatus, setCloudStatus] = useState(null); const [setCloudStatus] = useState(null);
const [syncStep, setSyncStep] = useState(""); // "syncing" | "verifying" | "disabling" | "" const [syncStep, setSyncStep] = useState(""); // "syncing" | "verifying" | "disabling" | ""
const { copied, copy } = useCopyToClipboard(); const { copied, copy } = useCopyToClipboard();
@ -110,7 +110,7 @@ export default function APIPageClient({ machineId }) {
try { try {
// Step 1: Sync latest data from cloud // Step 1: Sync latest data from cloud
const syncRes = await fetch("/api/sync/cloud", { await fetch("/api/sync/cloud", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action: "sync" }) 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 baseUrl = typeof window !== "undefined" ? `${window.location.origin}/v1` : "/v1";
const localApiKey = "HELLO";
// New format: /v1 (machineId in key), Old format: /{machineId}/v1 // New format: /v1 (machineId in key), Old format: /{machineId}/v1
const cloudEndpointNew = `${CLOUD_URL}/v1`; const cloudEndpointNew = `${CLOUD_URL}/v1`;
const cloudEndpointOld = `${CLOUD_URL}/${machineId}/v1`;
if (loading) { if (loading) {
return ( return (
@ -244,7 +240,7 @@ export default function APIPageClient({ machineId }) {
icon="cloud_off" icon="cloud_off"
onClick={() => handleCloudToggle(false)} onClick={() => handleCloudToggle(false)}
disabled={cloudSyncing} 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 Disable Cloud
</Button> </Button>
@ -254,7 +250,7 @@ export default function APIPageClient({ machineId }) {
icon="cloud_upload" icon="cloud_upload"
onClick={() => handleCloudToggle(true)} onClick={() => handleCloudToggle(true)}
disabled={cloudSyncing} 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 Enable Cloud
</Button> </Button>
@ -371,7 +367,7 @@ export default function APIPageClient({ machineId }) {
icon="cloud_off" icon="cloud_off"
onClick={() => handleCloudToggle(false)} onClick={() => handleCloudToggle(false)}
disabled={cloudSyncing} 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 Disable
</Button> </Button>
@ -381,7 +377,7 @@ export default function APIPageClient({ machineId }) {
icon="cloud_upload" icon="cloud_upload"
onClick={() => handleCloudToggle(true)} onClick={() => handleCloudToggle(true)}
disabled={cloudSyncing} 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 Enable Cloud
</Button> </Button>
@ -580,7 +576,7 @@ export default function APIPageClient({ machineId }) {
onClick={handleConfirmDisable} onClick={handleConfirmDisable}
fullWidth fullWidth
disabled={cloudSyncing} disabled={cloudSyncing}
className="!bg-red-500 hover:!bg-red-600 !text-white" className="bg-red-500! hover:bg-red-600! text-white!"
> >
{cloudSyncing ? ( {cloudSyncing ? (
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
@ -602,4 +598,8 @@ export default function APIPageClient({ machineId }) {
</Modal> </Modal>
</div> </div>
); );
} }
APIPageClient.propTypes = {
machineId: import("prop-types").string.isRequired,
};

View file

@ -30,9 +30,9 @@ export default function ProviderDetailPage() {
useEffect(() => { useEffect(() => {
fetchConnections(); fetchConnections();
fetchAliases(); fetchAliases();
}, [providerId]); }, [fetchConnections, fetchAliases]);
const fetchAliases = async () => { const fetchAliases = useCallback(async () => {
try { try {
const res = await fetch("/api/models/alias"); const res = await fetch("/api/models/alias");
const data = await res.json(); const data = await res.json();
@ -42,7 +42,7 @@ export default function ProviderDetailPage() {
} catch (error) { } catch (error) {
console.log("Error fetching aliases:", error); console.log("Error fetching aliases:", error);
} }
}; }, []);
const handleSetAlias = async (modelId, alias) => { const handleSetAlias = async (modelId, alias) => {
const fullModel = `${providerAlias}/${modelId}`; const fullModel = `${providerAlias}/${modelId}`;
@ -76,7 +76,7 @@ export default function ProviderDetailPage() {
} }
}; };
const fetchConnections = async () => { const fetchConnections = useCallback(async () => {
try { try {
const res = await fetch("/api/providers"); const res = await fetch("/api/providers");
const data = await res.json(); const data = await res.json();
@ -89,7 +89,7 @@ export default function ProviderDetailPage() {
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; }, [providerId]);
const handleDelete = async (id) => { const handleDelete = async (id) => {
if (!confirm("Delete this connection?")) return; if (!confirm("Delete this connection?")) return;
@ -228,11 +228,13 @@ export default function ProviderDetailPage() {
className="rounded-lg flex items-center justify-center" className="rounded-lg flex items-center justify-center"
style={{ backgroundColor: `${providerInfo.color}15` }} style={{ backgroundColor: `${providerInfo.color}15` }}
> >
<img <Image
src={`/providers/${providerInfo.id}.png`} src={`/providers/${providerInfo.id}.png`}
alt={providerInfo.name} alt={providerInfo.name}
className="size-12 object-contain rounded-lg" width={48}
onError={(e) => { e.target.style.display = "none"; }} height={48}
className="object-contain rounded-lg"
onError={(e) => { e.currentTarget.style.display = "none"; }}
/> />
</div> </div>
<div> <div>

View file

@ -1,6 +1,7 @@
"use client"; "use client";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import Image from "next/image";
import { Card, CardSkeleton, Badge } from "@/shared/components"; import { Card, CardSkeleton, Badge } from "@/shared/components";
import { OAUTH_PROVIDERS, APIKEY_PROVIDERS } from "@/shared/constants/config"; import { OAUTH_PROVIDERS, APIKEY_PROVIDERS } from "@/shared/constants/config";
import Link from "next/link"; import Link from "next/link";
@ -11,21 +12,20 @@ export default function ProvidersPage() {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
useEffect(() => { 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(); 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 getProviderStats = (providerId, authType) => {
const providerConnections = connections.filter( const providerConnections = connections.filter(
c => c.provider === providerId && c.authType === authType c => c.provider === providerId && c.authType === authType
@ -141,10 +141,12 @@ function ProviderCard({ providerId, provider, stats }) {
style={{ backgroundColor: `${provider.color}15` }} style={{ backgroundColor: `${provider.color}15` }}
> >
{!imgError ? ( {!imgError ? (
<img <Image
src={`/providers/${provider.id}.png`} src={`/providers/${provider.id}.png`}
alt={provider.name} alt={provider.name}
className="size-10 object-contain rounded-lg" width={40}
height={40}
className="object-contain rounded-lg"
onError={() => setImgError(true)} onError={() => setImgError(true)}
/> />
) : ( ) : (

View file

@ -1,7 +1,7 @@
"use client"; "use client";
import { useState, Suspense } from "react"; import { useState, Suspense } from "react";
import { UsageStats, RequestLogger } from "@/shared/components"; import { UsageStats, RequestLogger, CardSkeleton } from "@/shared/components";
export default function UsagePage() { export default function UsagePage() {
const [activeTab, setActiveTab] = useState("overview"); const [activeTab, setActiveTab] = useState("overview");
@ -33,9 +33,13 @@ export default function UsagePage() {
</div> </div>
{/* Content */} {/* Content */}
<Suspense fallback={<div className="text-text-muted">Loading...</div>}> {activeTab === "overview" ? (
{activeTab === "overview" ? <UsageStats /> : <RequestLogger />} <Suspense fallback={<CardSkeleton />}>
</Suspense> <UsageStats />
</Suspense>
) : (
<RequestLogger />
)}
</div> </div>
); );
} }

View file

@ -18,7 +18,9 @@ export async function POST(request) {
let isValid = false; let isValid = false;
if (!storedHash) { if (!storedHash) {
isValid = password === "123456"; // Use env var or default
const initialPassword = process.env.INITIAL_PASSWORD || "123456";
isValid = password === initialPassword;
} else { } else {
isValid = await bcrypt.compare(password, storedHash); isValid = await bcrypt.compare(password, storedHash);
} }

View file

@ -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"; @import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *)); @custom-variant dark (&:where(.dark, .dark *));

View file

@ -13,7 +13,7 @@ export default function HowItWorks() {
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 relative"> <div className="grid grid-cols-1 md:grid-cols-3 gap-8 relative">
{/* Connection line */} {/* Connection line */}
<div className="hidden md:block absolute top-12 left-[16%] right-[16%] h-[2px] bg-gradient-to-r from-gray-700 via-[#f97815] to-gray-700 -z-10"></div> <div className="hidden md:block absolute top-12 left-[16%] right-[16%] h-[2px] bg-linear-to-r from-gray-700 via-[#f97815] to-gray-700 -z-10"></div>
{/* Step 1: CLI & SDKs */} {/* Step 1: CLI & SDKs */}
<div className="flex flex-col gap-6 relative group"> <div className="flex flex-col gap-6 relative group">

View file

@ -11,7 +11,7 @@ export default function Navigation() {
<div className="max-w-7xl mx-auto px-6 h-16 flex items-center justify-between"> <div className="max-w-7xl mx-auto px-6 h-16 flex items-center justify-between">
{/* Logo */} {/* Logo */}
<div className="flex items-center gap-3 cursor-pointer" onClick={() => router.push("/")}> <div className="flex items-center gap-3 cursor-pointer" onClick={() => router.push("/")}>
<div className="size-8 rounded bg-gradient-to-br from-[#f97815] to-orange-700 flex items-center justify-center text-white"> <div className="size-8 rounded bg-linear-to-br from-[#f97815] to-orange-700 flex items-center justify-center text-white">
<span className="material-symbols-outlined text-[20px]">hub</span> <span className="material-symbols-outlined text-[20px]">hub</span>
</div> </div>
<h2 className="text-white text-xl font-bold tracking-tight">9Router</h2> <h2 className="text-white text-xl font-bold tracking-tight">9Router</h2>

View file

@ -47,7 +47,7 @@ export default function LandingPage() {
{/* CTA Section */} {/* CTA Section */}
<section className="py-32 px-6 relative overflow-hidden"> <section className="py-32 px-6 relative overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-t from-[#f97815]/5 to-transparent pointer-events-none"></div> <div className="absolute inset-0 bg-linear-to-t from-[#f97815]/5 to-transparent pointer-events-none"></div>
<div className="max-w-4xl mx-auto text-center relative z-10"> <div className="max-w-4xl mx-auto text-center relative z-10">
<h2 className="text-4xl md:text-5xl font-black mb-6">Ready to Simplify Your AI Infrastructure?</h2> <h2 className="text-4xl md:text-5xl font-black mb-6">Ready to Simplify Your AI Infrastructure?</h2>
<p className="text-xl text-gray-400 mb-10 max-w-2xl mx-auto"> <p className="text-xl text-gray-400 mb-10 max-w-2xl mx-auto">

View file

@ -17,10 +17,7 @@ export default function RootLayout({ children }) {
return ( return (
<html lang="en" suppressHydrationWarning> <html lang="en" suppressHydrationWarning>
<head> <head>
<link
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap"
rel="stylesheet"
/>
<link rel="icon" href="/favicon.svg" type="image/svg+xml" /> <link rel="icon" href="/favicon.svg" type="image/svg+xml" />
</head> </head>
<body className={`${inter.variable} font-sans antialiased`}> <body className={`${inter.variable} font-sans antialiased`}>

View file

@ -16,6 +16,8 @@ function getAppName() {
function getUserDataDir() { function getUserDataDir() {
if (isCloud) return "/tmp"; // Fallback for Workers if (isCloud) return "/tmp"; // Fallback for Workers
if (process.env.DATA_DIR) return process.env.DATA_DIR;
const platform = process.platform; const platform = process.platform;
const homeDir = os.homedir(); const homeDir = os.homedir();
const appName = getAppName(); const appName = getAppName();

View file

@ -2,6 +2,7 @@
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import Link from "next/link"; import Link from "next/link";
import Image from "next/image";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { ThemeToggle } from "@/shared/components"; import { ThemeToggle } from "@/shared/components";
import { APP_CONFIG, OAUTH_PROVIDERS, APIKEY_PROVIDERS } from "@/shared/constants/config"; import { APP_CONFIG, OAUTH_PROVIDERS, APIKEY_PROVIDERS } from "@/shared/constants/config";
@ -88,11 +89,13 @@ export default function Header({ onMenuClick, showMenuButton = true }) {
) : ( ) : (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{crumb.image && ( {crumb.image && (
<img <Image
src={crumb.image} src={crumb.image}
alt={crumb.label} alt={crumb.label}
className="size-7 object-contain rounded" width={28}
onError={(e) => { e.target.style.display = "none"; }} height={28}
className="object-contain rounded"
onError={(e) => { e.currentTarget.style.display = "none"; }}
/> />
)} )}
<h1 className="text-2xl font-semibold text-text-main tracking-tight"> <h1 className="text-2xl font-semibold text-text-main tracking-tight">

View file

@ -1,6 +1,7 @@
"use client"; "use client";
import { useState, useMemo, useEffect } from "react"; import { useState, useMemo, useEffect } from "react";
import PropTypes from "prop-types";
import Modal from "./Modal"; import Modal from "./Modal";
import { getModelsByProviderId, PROVIDER_ID_TO_ALIAS } from "@/shared/constants/models"; import { getModelsByProviderId, PROVIDER_ID_TO_ALIAS } from "@/shared/constants/models";
import { OAUTH_PROVIDERS, APIKEY_PROVIDERS } from "@/shared/constants/providers"; import { OAUTH_PROVIDERS, APIKEY_PROVIDERS } from "@/shared/constants/providers";
@ -33,7 +34,7 @@ export default function ModelSelectModal({
} }
}, [isOpen]); }, [isOpen]);
const allProviders = { ...OAUTH_PROVIDERS, ...APIKEY_PROVIDERS }; const allProviders = useMemo(() => ({ ...OAUTH_PROVIDERS, ...APIKEY_PROVIDERS }), []);
// Group models by provider with priority order // Group models by provider with priority order
const groupedModels = useMemo(() => { const groupedModels = useMemo(() => {
@ -141,7 +142,7 @@ export default function ModelSelectModal({
}} }}
title={title} title={title}
size="md" size="md"
className="!p-4" className="p-4!"
> >
{/* Search - compact */} {/* Search - compact */}
<div className="mb-3"> <div className="mb-3">
@ -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,
};

View file

@ -36,7 +36,7 @@ export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess,
// Auto start OAuth // Auto start OAuth
startOAuthFlow(); startOAuthFlow();
} }
}, [isOpen, provider]); }, [isOpen, provider, startOAuthFlow]);
// Listen for OAuth callback via multiple methods // Listen for OAuth callback via multiple methods
const callbackProcessedRef = useRef(false); const callbackProcessedRef = useRef(false);
@ -114,10 +114,11 @@ export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess,
window.removeEventListener("storage", handleStorage); window.removeEventListener("storage", handleStorage);
if (channel) channel.close(); if (channel) channel.close();
}; };
}, [authData]); }, [authData, exchangeTokens]);
// Exchange tokens // Exchange tokens
const exchangeTokens = async (code, state) => { const exchangeTokens = useCallback(async (code, state) => {
if (!authData) return;
try { try {
const res = await fetch(`/api/oauth/${provider}/exchange`, { const res = await fetch(`/api/oauth/${provider}/exchange`, {
method: "POST", method: "POST",
@ -139,10 +140,54 @@ export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess,
setError(err.message); setError(err.message);
setStep("error"); 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 // Start OAuth flow
const startOAuthFlow = async () => { const startOAuthFlow = useCallback(async () => {
if (!provider) return; if (!provider) return;
try { try {
setError(null); setError(null);
@ -207,51 +252,7 @@ export default function OAuthModal({ isOpen, provider, providerInfo, onSuccess,
setError(err.message); setError(err.message);
setStep("error"); setStep("error");
} }
}; }, [provider, isLocalhost, startPolling]);
// 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);
};
// Handle manual URL input // Handle manual URL input
const handleManualSubmit = async () => { const handleManualSubmit = async () => {

View file

@ -1,6 +1,7 @@
"use client"; "use client";
import { useState } from "react"; import { useState } from "react";
import PropTypes from "prop-types";
import Link from "next/link"; import Link from "next/link";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { cn } from "@/shared/utils/cn"; import { cn } from "@/shared/utils/cn";
@ -38,7 +39,7 @@ export default function Sidebar({ onClose }) {
try { try {
await fetch("/api/shutdown", { method: "POST" }); await fetch("/api/shutdown", { method: "POST" });
} catch (e) { } catch (e) {
// Expected to fail as server shuts down // Expected to fail as server shuts down; ignore error
} }
setIsShuttingDown(false); setIsShuttingDown(false);
setShowShutdownModal(false); setShowShutdownModal(false);
@ -51,7 +52,7 @@ export default function Sidebar({ onClose }) {
{/* Logo */} {/* Logo */}
<div className="p-8"> <div className="p-8">
<Link href="/dashboard" className="flex items-center gap-3"> <Link href="/dashboard" className="flex items-center gap-3">
<div className="flex items-center justify-center size-9 rounded bg-gradient-to-br from-[#f97815] to-[#c2590a]"> <div className="flex items-center justify-center size-9 rounded bg-linear-to-br from-[#f97815] to-[#c2590a]">
<span className="material-symbols-outlined text-white text-[20px]">hub</span> <span className="material-symbols-outlined text-white text-[20px]">hub</span>
</div> </div>
<h1 className="text-lg font-semibold tracking-tight text-text-main"> <h1 className="text-lg font-semibold tracking-tight text-text-main">
@ -161,7 +162,7 @@ export default function Sidebar({ onClose }) {
</div> </div>
<h2 className="text-xl font-semibold text-white mb-2">Server Disconnected</h2> <h2 className="text-xl font-semibold text-white mb-2">Server Disconnected</h2>
<p className="text-text-muted mb-6">The proxy server has been stopped.</p> <p className="text-text-muted mb-6">The proxy server has been stopped.</p>
<Button variant="secondary" onClick={() => window.location.reload()}> <Button variant="secondary" onClick={() => globalThis.location.reload()}>
Reload Page Reload Page
</Button> </Button>
</div> </div>
@ -170,3 +171,7 @@ export default function Sidebar({ onClose }) {
</> </>
); );
} }
Sidebar.propTypes = {
onClose: PropTypes.func,
};

View file

@ -52,7 +52,7 @@ export default function UsageStats() {
router.replace(`?${params.toString()}`, { scroll: false }); router.replace(`?${params.toString()}`, { scroll: false });
}; };
const sortData = (dataMap, pendingMap = {}) => { const sortData = useCallback((dataMap, pendingMap = {}) => {
return Object.entries(dataMap || {}) return Object.entries(dataMap || {})
.map(([key, data]) => { .map(([key, data]) => {
const totalTokens = const totalTokens =
@ -91,11 +91,11 @@ export default function UsageStats() {
if (valA > valB) return sortOrder === "asc" ? 1 : -1; if (valA > valB) return sortOrder === "asc" ? 1 : -1;
return 0; return 0;
}); });
}; }, [sortBy, sortOrder]);
const sortedModels = useMemo( const sortedModels = useMemo(
() => sortData(stats?.byModel, stats?.pending?.byModel), () => sortData(stats?.byModel, stats?.pending?.byModel),
[stats?.byModel, stats?.pending?.byModel, sortBy, sortOrder] [stats?.byModel, stats?.pending?.byModel, sortData]
); );
const sortedAccounts = useMemo(() => { const sortedAccounts = useMemo(() => {
// For accounts, pendingMap is by connectionId, but dataMap is by accountKey // For accounts, pendingMap is by connectionId, but dataMap is by accountKey
@ -114,7 +114,7 @@ export default function UsageStats() {
}); });
} }
return sortData(stats?.byAccount, accountPendingMap); return sortData(stats?.byAccount, accountPendingMap);
}, [stats?.byAccount, stats?.pending?.byAccount, sortBy, sortOrder]); }, [stats?.byAccount, stats?.pending?.byAccount, sortData]);
const fetchStats = useCallback(async (showLoading = true) => { const fetchStats = useCallback(async (showLoading = true) => {
if (showLoading) setLoading(true); if (showLoading) setLoading(true);

View file

@ -88,5 +88,6 @@ async function handleResponse(response) {
return data; return data;
} }
export default { get, post, put, del }; const api = { get, post, put, del };
export default api;