mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 20:00:47 +00:00
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:
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
</Button>
|
||||
@@ -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
|
||||
</Button>
|
||||
@@ -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
|
||||
</Button>
|
||||
@@ -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
|
||||
</Button>
|
||||
@@ -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 ? (
|
||||
<span className="flex items-center gap-2">
|
||||
@@ -602,4 +598,8 @@ export default function APIPageClient({ machineId }) {
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
APIPageClient.propTypes = {
|
||||
machineId: import("prop-types").string.isRequired,
|
||||
};
|
||||
@@ -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` }}
|
||||
>
|
||||
<img
|
||||
<Image
|
||||
src={`/providers/${providerInfo.id}.png`}
|
||||
alt={providerInfo.name}
|
||||
className="size-12 object-contain rounded-lg"
|
||||
onError={(e) => { e.target.style.display = "none"; }}
|
||||
width={48}
|
||||
height={48}
|
||||
className="object-contain rounded-lg"
|
||||
onError={(e) => { e.currentTarget.style.display = "none"; }}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -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 ? (
|
||||
<img
|
||||
<Image
|
||||
src={`/providers/${provider.id}.png`}
|
||||
alt={provider.name}
|
||||
className="size-10 object-contain rounded-lg"
|
||||
width={40}
|
||||
height={40}
|
||||
className="object-contain rounded-lg"
|
||||
onError={() => setImgError(true)}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -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() {
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<Suspense fallback={<div className="text-text-muted">Loading...</div>}>
|
||||
{activeTab === "overview" ? <UsageStats /> : <RequestLogger />}
|
||||
</Suspense>
|
||||
{activeTab === "overview" ? (
|
||||
<Suspense fallback={<CardSkeleton />}>
|
||||
<UsageStats />
|
||||
</Suspense>
|
||||
) : (
|
||||
<RequestLogger />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 *));
|
||||
|
||||
@@ -13,7 +13,7 @@ export default function HowItWorks() {
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 relative">
|
||||
{/* 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 */}
|
||||
<div className="flex flex-col gap-6 relative group">
|
||||
|
||||
@@ -11,7 +11,7 @@ export default function Navigation() {
|
||||
<div className="max-w-7xl mx-auto px-6 h-16 flex items-center justify-between">
|
||||
{/* Logo */}
|
||||
<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>
|
||||
</div>
|
||||
<h2 className="text-white text-xl font-bold tracking-tight">9Router</h2>
|
||||
|
||||
@@ -47,7 +47,7 @@ export default function LandingPage() {
|
||||
|
||||
{/* CTA Section */}
|
||||
<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">
|
||||
<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">
|
||||
|
||||
+1
-4
@@ -17,10 +17,7 @@ export default function RootLayout({ children }) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<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" />
|
||||
</head>
|
||||
<body className={`${inter.variable} font-sans antialiased`}>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 }) {
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
{crumb.image && (
|
||||
<img
|
||||
<Image
|
||||
src={crumb.image}
|
||||
alt={crumb.label}
|
||||
className="size-7 object-contain rounded"
|
||||
onError={(e) => { e.target.style.display = "none"; }}
|
||||
width={28}
|
||||
height={28}
|
||||
className="object-contain rounded"
|
||||
onError={(e) => { e.currentTarget.style.display = "none"; }}
|
||||
/>
|
||||
)}
|
||||
<h1 className="text-2xl font-semibold text-text-main tracking-tight">
|
||||
|
||||
@@ -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 */}
|
||||
<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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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 */}
|
||||
<div className="p-8">
|
||||
<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>
|
||||
</div>
|
||||
<h1 className="text-lg font-semibold tracking-tight text-text-main">
|
||||
@@ -161,7 +162,7 @@ export default function Sidebar({ onClose }) {
|
||||
</div>
|
||||
<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>
|
||||
<Button variant="secondary" onClick={() => window.location.reload()}>
|
||||
<Button variant="secondary" onClick={() => globalThis.location.reload()}>
|
||||
Reload Page
|
||||
</Button>
|
||||
</div>
|
||||
@@ -170,3 +171,7 @@ export default function Sidebar({ onClose }) {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Sidebar.propTypes = {
|
||||
onClose: PropTypes.func,
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user