diff --git a/apps/provider-console/sentry.client.config.js b/apps/provider-console/sentry.client.config.js index b9fc2944a..cdcbe60b2 100644 --- a/apps/provider-console/sentry.client.config.js +++ b/apps/provider-console/sentry.client.config.js @@ -3,7 +3,21 @@ import * as Sentry from "@sentry/nextjs"; Sentry.init({ dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, enabled: !!process.env.NEXT_PUBLIC_SENTRY_DSN, - integrations: [Sentry.replayIntegration()], + integrations: [ + Sentry.replayIntegration(), + new Sentry.BrowserTracing({ + // Set sampling rate for performance monitoring + tracePropagationTargets: ["localhost", /^https:\/\/provider-console.akash.network/] + }) + ], + // Session replay sampling rates replaysSessionSampleRate: 0.1, - replaysOnErrorSampleRate: 1.0 + replaysOnErrorSampleRate: 1.0, + + // Performance monitoring sampling rate + tracesSampleRate: 0.2, + + // Enable automatic instrumentation for analytics + enableAutoSessionTracking: true, + sessionTrackingIntervalMillis: 30000 }); diff --git a/apps/provider-console/src/components/become-provider/ProviderAttributes.tsx b/apps/provider-console/src/components/become-provider/ProviderAttributes.tsx index b89b265c0..56f1a7202 100644 --- a/apps/provider-console/src/components/become-provider/ProviderAttributes.tsx +++ b/apps/provider-console/src/components/become-provider/ProviderAttributes.tsx @@ -40,6 +40,68 @@ const attributeKeys = Object.keys(providerAttributesFormValuesSchema.shape); const DEFAULT_ATTRIBUTES = ["host", "tier"]; +interface GpuConfig { + count: number; + vendor: string; + name: string; + memory_size: string; + interface: string; +} + +const createGpuAttributes = (gpuConfigs: GpuConfig[] | undefined) => { + // Return empty array if gpuConfigs is undefined, empty, or has no valid GPUs + if (!gpuConfigs || gpuConfigs.length === 0) return []; + + // Filter out configurations with count=0 or null values + const validGpuConfigs = gpuConfigs.filter( + gpu => gpu.count !== 0 && gpu.vendor !== null && gpu.name !== null && gpu.memory_size !== null && gpu.interface !== null + ); + + if (validGpuConfigs.length === 0) return []; + + // Get unique GPU configurations based on vendor, model, memory, and interface + const uniqueConfigs = validGpuConfigs.reduce( + (acc, gpu) => { + const key = `${gpu.vendor}-${gpu.name}-${gpu.memory_size}-${gpu.interface}`; + if (!acc[key]) { + acc[key] = gpu; + } + return acc; + }, + {} as Record + ); + + return Object.values(uniqueConfigs).flatMap(gpu => { + const vendor = gpu.vendor.toLowerCase(); + const model = gpu.name.toLowerCase(); + const memory = gpu.memory_size; + const iface = gpu.interface.toLowerCase(); + + return [ + { + key: "unknown-attributes", + value: "true", + customKey: `capabilities/gpu/vendor/${vendor}/model/${model}` + }, + { + key: "unknown-attributes", + value: "true", + customKey: `capabilities/gpu/vendor/${vendor}/model/${model}/ram/${memory}` + }, + { + key: "unknown-attributes", + value: "true", + customKey: `capabilities/gpu/vendor/${vendor}/model/${model}/ram/${memory}/interface/${iface}` + }, + { + key: "unknown-attributes", + value: "true", + customKey: `capabilities/gpu/vendor/${vendor}/model/${model}/interface/${iface}` + } + ]; + }); +}; + interface ProviderAttributesProps { existingAttributes?: ProviderAttribute[]; editMode?: boolean; @@ -62,6 +124,9 @@ export const ProviderAttributes: React.FunctionComponent machine.systemInfo?.gpu).filter((gpu): gpu is GpuConfig => !!gpu); + const form = useForm({ resolver: zodResolver(providerFormSchema), defaultValues: { @@ -74,7 +139,8 @@ export const ProviderAttributes: React.FunctionComponent = ({ acti const [openAccordions, setOpenAccordions] = useState([]); const [taskLogs, setTaskLogs] = useState({}); const [loadingLogs, setLoadingLogs] = useState<{ [taskId: string]: boolean }>({}); + const [elapsedTimes, setElapsedTimes] = useState<{ [taskId: string]: string }>({}); const logStreams = useRef<{ [taskId: string]: EventSourcePolyfill | null }>({}); const { data: actionDetails, isLoading } = useProviderActionStatus(actionId); useEffect(() => { - if (actionDetails) { + if (actionDetails && actionDetails.tasks) { const initialAccordions = actionDetails.tasks?.map(task => task.status === "in_progress"); setOpenAccordions(initialAccordions ?? []); } }, [actionDetails]); useEffect(() => { - if (actionDetails?.tasks) { - Object.values(logStreams.current).forEach(stream => stream?.close()); + if (actionDetails?.tasks?.length && actionDetails.tasks.length > 0) { + // Safely close existing streams + if (logStreams.current) { + Object.values(logStreams.current).forEach(stream => { + if (stream && typeof stream.close === "function") { + try { + stream.close(); + } catch (error) { + console.warn("Error closing stream:", error); + } + } + }); + } logStreams.current = {}; actionDetails.tasks.forEach((task, index) => { @@ -41,7 +53,11 @@ export const ActivityLogDetails: React.FC<{ actionId: string | null }> = ({ acti setupLogStream(task.id); } else { if (logStreams.current[task.id]) { - logStreams.current[task.id]?.close(); + try { + logStreams.current[task.id]?.close(); + } catch (error) { + console.warn("Error closing stream:", error); + } logStreams.current[task.id] = null; } } @@ -49,11 +65,42 @@ export const ActivityLogDetails: React.FC<{ actionId: string | null }> = ({ acti } return () => { - Object.values(logStreams.current).forEach(stream => stream?.close()); + // Cleanup function + if (logStreams.current) { + Object.values(logStreams.current).forEach(stream => { + if (stream && typeof stream.close === "function") { + try { + stream.close(); + } catch (error) { + console.warn("Error closing stream:", error); + } + } + }); + } logStreams.current = {}; }; }, [actionDetails?.tasks]); + useEffect(() => { + if (!actionDetails?.tasks) return; + + const interval = setInterval(() => { + const newElapsedTimes: { [taskId: string]: string } = {}; + + actionDetails?.tasks?.forEach(task => { + if (task.status === "in_progress" && task.start_time) { + newElapsedTimes[task.id] = formatTimeLapse(task.start_time, null); + } else if (task.start_time && task.end_time) { + newElapsedTimes[task.id] = formatTimeLapse(task.start_time, task.end_time); + } + }); + + setElapsedTimes(newElapsedTimes); + }, 1000); + + return () => clearInterval(interval); + }, [actionDetails?.tasks]); + const fetchTaskLogs = async (taskId: string) => { setLoadingLogs(prev => ({ ...prev, [taskId]: true })); try { @@ -79,6 +126,8 @@ export const ActivityLogDetails: React.FC<{ actionId: string | null }> = ({ acti eventSource.onmessage = event => { try { const logData = JSON.parse(event.data); + if (!logData || !logData.message) return; // Skip invalid messages + const formattedMessage = `${logData.message}`; setTaskLogs(prev => ({ @@ -125,7 +174,7 @@ export const ActivityLogDetails: React.FC<{ actionId: string | null }> = ({ acti ); } - if (!logs) { + if (!logs || typeof logs !== "string") { return (
No logs recorded for this task or this task is more than 7 days old. @@ -133,30 +182,44 @@ export const ActivityLogDetails: React.FC<{ actionId: string | null }> = ({ acti ); } - return ( -
- ( - - )} - /> -
- ); + try { + const sanitizedLogs = logs.trim(); + + return ( +
+ ( + + )} + /> +
+ ); + } catch (error) { + return ( +
+
{logs}
+
+ ); + } }; if (actionId === null) { @@ -205,15 +268,7 @@ export const ActivityLogDetails: React.FC<{ actionId: string | null }> = ({ acti {task.description}
- {task.start_time && ( -

- {task.status === "in_progress" - ? formatTimeLapse(task.start_time, null) - : task.end_time - ? formatTimeLapse(task.start_time, task.end_time) - : ""} -

- )} + {task.start_time &&

{elapsedTimes[task.id] || ""}

} {task.status === "completed" && } {task.status === "in_progress" && } {task.status === "not_started" &&
} diff --git a/apps/provider-console/src/components/shared/ActivityLogList.tsx b/apps/provider-console/src/components/shared/ActivityLogList.tsx index 992895015..26e518a73 100644 --- a/apps/provider-console/src/components/shared/ActivityLogList.tsx +++ b/apps/provider-console/src/components/shared/ActivityLogList.tsx @@ -1,8 +1,10 @@ import React, { useCallback } from "react"; -import { Separator } from "@akashnetwork/ui/components"; -import { CheckCircle, Play, XmarkCircle } from "iconoir-react"; +import { Separator, Spinner } from "@akashnetwork/ui/components"; +import { CheckCircle, XmarkCircle } from "iconoir-react"; import { useRouter } from "next/router"; +import { formatTimeLapse } from "@src/utils/dateUtils"; + interface ProviderAction { id: string; name: string; @@ -24,7 +26,7 @@ const StatusIcon: React.FC = ({ status }) => { case "completed": return ; case "in_progress": - return ; + return ; case "failed": return ; default: @@ -49,13 +51,6 @@ export const ActivityLogList: React.FC = ({ actions }) => }); }, []); - const calculateTimeLapse = (start: string, end?: string) => { - const startTime = new Date(start).getTime(); - const endTime = end ? new Date(end).getTime() : Date.now(); - const timeLapse = endTime - startTime; - return `${Math.floor(timeLapse / 1000)} seconds`; - }; - const handleRowClick = (actionId: string) => { router.push(`/activity-logs/${actionId}`); }; @@ -65,7 +60,7 @@ export const ActivityLogList: React.FC = ({ actions }) =>
Action
Duration
-
Timestamp
+
Start Time
Status
@@ -78,7 +73,7 @@ export const ActivityLogList: React.FC = ({ actions }) =>

{action.name}

-

{calculateTimeLapse(action.start_time, action.end_time)}

+

{formatTimeLapse(action.start_time, action.end_time || null)}

{formatDate(action.start_time)}

diff --git a/apps/provider-console/src/components/shared/withAuth.tsx b/apps/provider-console/src/components/shared/withAuth.tsx index 3497832ab..bd1c02416 100644 --- a/apps/provider-console/src/components/shared/withAuth.tsx +++ b/apps/provider-console/src/components/shared/withAuth.tsx @@ -16,55 +16,77 @@ export const withAuth = ({ WrappedComponent, authLevel = "wallet" }: WithAuthPro const [loading, setLoading] = useState(true); const [loadingMessage, setLoadingMessage] = useState("Checking wallet connection..."); + const delayedRedirect = (message: string) => { + setLoadingMessage(message); + return new Promise(resolve => setTimeout(resolve, 3000)); // 3 second delay + }; + + const delayedCheck = (message: string) => { + setLoadingMessage(message); + return new Promise(resolve => setTimeout(resolve, 5000)); // 3 second delay + }; + useEffect(() => { - if (authLevel === "wallet") { - if (!isWalletConnected) { - setLoadingMessage("Connecting to wallet..."); - router.push("/"); - return; - } - setLoading(false); - } - - if (authLevel === "provider") { - if (!isWalletConnected) { - setLoadingMessage("Connecting to wallet..."); - router.push("/"); - return; + const checkAuth = async () => { + // Wait for initial wallet connection check + while (!isWalletConnected) { + await delayedCheck("Checking wallet connection..."); + if (!isWalletConnected) continue; } - if (!isProviderStatusFetched) { - setLoadingMessage("Checking provider status..."); - return; + if (authLevel === "wallet") { + if (!isWalletConnected) { + await delayedRedirect("Wallet not connected, redirecting to home page..."); + router.push("/"); + return; + } + setLoading(false); } - if (!isProvider) { - router.push("/"); - return; - } + if (authLevel === "provider") { + if (!isWalletConnected) { + await delayedRedirect("Wallet not connected, redirecting to home page..."); + router.push("/"); + return; + } - setLoading(false); - } + if (!isProviderStatusFetched) { + setLoadingMessage("Checking provider status..."); + return; + } - if (authLevel === "onlineProvider") { - if (!isWalletConnected) { - setLoadingMessage("Wallet not connected, redirecting to home page..."); - router.push("/"); - return; - } + if (!isProvider) { + await delayedRedirect("Not a provider, redirecting to home page..."); + router.push("/"); + return; + } - if (!isProviderStatusFetched || !isProviderOnlineStatusFetched) { - setLoadingMessage("Checking provider status..."); - return; + setLoading(false); } - if (!isProvider && !isOnline) { - router.push("/"); - return; + if (authLevel === "onlineProvider") { + if (!isWalletConnected) { + await delayedRedirect("Wallet not connected, redirecting to home page..."); + router.push("/"); + return; + } + + if (!isProviderStatusFetched || !isProviderOnlineStatusFetched) { + setLoadingMessage("Checking provider status..."); + return; + } + + if (!isProvider && !isOnline) { + await delayedRedirect("Provider is offline, redirecting to home page..."); + router.push("/"); + return; + } + setLoading(false); } - setLoading(false); - } - }, [isWalletConnected, isProvider, isProviderStatusFetched, isProviderOnlineStatusFetched, router, authLevel]); + }; + + checkAuth(); + }, [isWalletConnected, isProvider, isProviderStatusFetched, isProviderOnlineStatusFetched, isOnline, router]); if (loading) { return ( diff --git a/apps/provider-console/src/context/CustomChainProvider/CustomChainProvider.tsx b/apps/provider-console/src/context/CustomChainProvider/CustomChainProvider.tsx index 9f818d3db..7f85b3f09 100644 --- a/apps/provider-console/src/context/CustomChainProvider/CustomChainProvider.tsx +++ b/apps/provider-console/src/context/CustomChainProvider/CustomChainProvider.tsx @@ -12,16 +12,30 @@ import { akash, assetLists } from "@src/chains"; import { useSelectedNetwork } from "@src/hooks/useSelectedNetwork"; import { customRegistry } from "@src/utils/customRegistry"; +declare global { + interface Window { + leap?: any; + } +} + type Props = { children: React.ReactNode; }; export function CustomChainProvider({ children }: Props) { + // Filter out Leap wallets if the extension is not detected + const availableWallets = [...keplr, ...leap].filter(wallet => { + if (wallet.walletInfo.name.toLowerCase().includes("leap")) { + return typeof window !== "undefined" && window.leap; + } + return true; + }); + return ( { diff --git a/apps/provider-console/src/context/ProviderContext/ProviderContext.tsx b/apps/provider-console/src/context/ProviderContext/ProviderContext.tsx index 215ba2146..b0249dcde 100644 --- a/apps/provider-console/src/context/ProviderContext/ProviderContext.tsx +++ b/apps/provider-console/src/context/ProviderContext/ProviderContext.tsx @@ -38,11 +38,6 @@ export const ProviderContextProvider = ({ children }) => { } finally { setIsProviderOnlineStatusFetched(true); } - } else { - setIsWalletProvider(false); - setIsWalletProviderOnline(false); - setIsProviderOnlineStatusFetched(true); - setIsProviderStatusFetched(true); } }; checkProviderStatus(); diff --git a/apps/provider-console/src/queries/useProviderQuery.ts b/apps/provider-console/src/queries/useProviderQuery.ts index 5164b6af9..18bf31f82 100644 --- a/apps/provider-console/src/queries/useProviderQuery.ts +++ b/apps/provider-console/src/queries/useProviderQuery.ts @@ -181,7 +181,7 @@ export const useProviderActionStatus = (actionId: string | null) => { enabled: !!actionId, refetchInterval: data => { if (data?.tasks?.some(task => task.status === "in_progress")) { - return 1000; + return 5000; } return false; },