Repository: chain4travel/camino-suite-voting
Pull request #58: Fix data race condition in DAC proposal tabs
Author: login: aeddaqqa
Created: 2025-04-08T11:28:21Z | Merged: 2025-04-29T11:10:58Z

# Fix Data Race in DAC Proposals Tab Switching

## Problem
When rapidly switching between "Upcoming", "Ongoing", and "Completed" proposal tabs in the DAC section, a data race condition occurs that causes incorrect display of proposals. This includes:
- Ongoing proposals showing in the Completed tab
- No data loading until manually refreshing
- Incorrect filtering when switching tabs quickly

## Root Cause
The issue was caused by several factors:
1. Shared React Query cache keys between different tabs
2. Missing request cancellation for in-flight API calls
3. Timing issues between tab navigation and data fetching
4. Query state not properly reset between tab switches


Diff:
diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx
index 3107a10..9c4b5a0 100644
--- a/src/components/Navbar.tsx
+++ b/src/components/Navbar.tsx
@@ -2,8 +2,10 @@ import { useWalletStore } from '@/store';
 import { PlatformVMConstants } from '@c4tplatform/caminojs/dist/apis/platformvm';
 import { Box, Tab, Tabs, useTheme } from '@mui/material';
 import { filter } from 'lodash';
-import React, { useEffect, useMemo, useState } from 'react';
-import { useNavigate } from 'react-router-dom';
+import React, { useEffect, useMemo, useState, useRef } from 'react';
+import { useNavigate, useLocation } from 'react-router-dom';
+import { useQueryClient } from '@tanstack/react-query';
+import { QUERY_KEYS } from '@/hooks/useProposals';
 
 function a11yProps(index: number) {
   return {
@@ -12,12 +14,27 @@ function a11yProps(index: number) {
   };
 }
 
-//TODO: notification is removed need to be added
 const ProposalNavbar = () => {
   const [value, setValue] = useState(0);
-  const handleChange = (event: React.SyntheticEvent, newValue: number) => {
-    setValue(newValue);
-  };
+  const [isNavigating, setIsNavigating] = useState(false);
+  const lastNavigationTime = useRef(Date.now());
+  const initialFetchDone = useRef(false);
+  const location = useLocation();
+  const navigate = useNavigate();
+  const queryClient = useQueryClient();
+
+  useEffect(() => {
+    if (!initialFetchDone.current) {
+      Promise.all([queryClient.invalidateQueries([QUERY_KEYS.ACTIVE])])
+        .then(() => {
+          initialFetchDone.current = true;
+        })
+        .catch(err => {
+          console.error('Error prefetching proposal data:', err);
+        });
+    }
+  }, []);
+
   useEffect(() => {
     if (location.pathname.includes('active')) {
       setValue(0);
@@ -29,38 +46,56 @@ const ProposalNavbar = () => {
       setValue(3);
     }
   }, [location.pathname]);
+
   const theme = useTheme();
-  const { addressState, currentWalletAddress, pendingMultisigTxs } =
-    useWalletStore(state => ({
-      addressState: state.addressState,
-      currentWalletAddress: state.currentWalletAddress,
-      pendingMultisigTxs: state.pendingMultisigTxs,
-    }));
+  const { addressState, currentWalletAddress } = useWalletStore(state => ({
+    addressState: state.addressState,
+    currentWalletAddress: state.currentWalletAddress,
+    pendingMultisigTxs: state.pendingMultisigTxs,
+  }));
+
   const { isKycVerified, isConsortiumAdminProposer } = addressState;
-  const { pendingAddProposals, pendingAddVotes } = useMemo(() => {
-    const pendingForCurrentAlias = filter(pendingMultisigTxs, {
-      alias: currentWalletAddress,
-    });
-    const pendingAddProposalCount = filter(
-      pendingForCurrentAlias,
-      tx => tx.typeId === PlatformVMConstants.ADDPROPOSALTX
-    ).length;
-    const pendingAddVoteCount = filter(
-      pendingForCurrentAlias,
-      tx => tx.typeId === PlatformVMConstants.ADDVOTETX
-    ).length;
-    return {
-      pendingAddProposals:
-        pendingAddProposalCount > 0
-          ? `${pendingAddProposalCount} pending`
-          : undefined,
-      pendingAddVotes:
-        pendingAddVoteCount > 0 ? `${pendingAddVoteCount} pending` : undefined,
-    };
-  }, [pendingMultisigTxs]);
-  const navigate = useNavigate();
+
   const isCreateProposalAllowed = isKycVerified || isConsortiumAdminProposer;
   const enableCreateButton = currentWalletAddress && isCreateProposalAllowed;
+
+  const handleTabClick = (path: string, tabValue: number) => {
+    if (isNavigating || value === tabValue) return;
+
+    const now = Date.now();
+    if (now - lastNavigationTime.current < 300) {
+      return;
+    }
+
+    setValue(tabValue);
+    setIsNavigating(true);
+    lastNavigationTime.current = now;
+
+    switch (tabValue) {
+      case 0: // active
+        queryClient.invalidateQueries([QUERY_KEYS.ACTIVE]);
+        break;
+      case 1: // upcoming
+        queryClient.invalidateQueries([
+          QUERY_KEYS.ACTIVE,
+          undefined,
+          0,
+          'upcoming',
+        ]);
+        break;
+      case 2: // completed
+        queryClient.invalidateQueries([QUERY_KEYS.COMPLETED]);
+        break;
+    }
+
+    setTimeout(() => {
+      navigate(path);
+      setTimeout(() => {
+        setIsNavigating(false);
+      }, 100);
+    }, 50);
+  };
+
   return (
     <Box
       sx={{
@@ -71,7 +106,6 @@ const ProposalNavbar = () => {
     >
       <Tabs
         value={value}
-        onChange={handleChange}
         textColor="secondary"
         sx={{
           '& .MuiTabs-indicator': { display: 'none' },
@@ -79,6 +113,9 @@ const ProposalNavbar = () => {
           '& .Mui-selected': {
             color: `${theme.palette.text.primary} !important`,
           },
+          '& .Mui-disabled': {
+            opacity: 0.6,
+          },
         }}
         scrollButtons="auto"
         variant="scrollable"
@@ -91,7 +128,8 @@ const ProposalNavbar = () => {
           {...a11yProps(0)}
           key={0}
           sx={{ '&::after': { display: value === 0 ? 'block' : 'none' } }}
-          onClick={() => navigate('/dac/active')}
+          onClick={() => handleTabClick('/dac/active', 0)}
+          disabled={isNavigating}
         />
         <Tab
           className="tab"
@@ -100,7 +138,8 @@ const ProposalNavbar = () => {
           {...a11yProps(1)}
           key={1}
           sx={{ '&::after': { display: value === 1 ? 'block' : 'none' } }}
-          onClick={() => navigate('/dac/upcoming')}
+          onClick={() => handleTabClick('/dac/upcoming', 1)}
+          disabled={isNavigating}
         />
         <Tab
           className="tab"
@@ -109,7 +148,8 @@ const ProposalNavbar = () => {
           {...a11yProps(2)}
           key={2}
           sx={{ '&::after': { display: value === 2 ? 'block' : 'none' } }}
-          onClick={() => navigate('/dac/completed')}
+          onClick={() => handleTabClick('/dac/completed', 2)}
+          disabled={isNavigating}
         />
         {enableCreateButton && (
           <Tab
@@ -119,7 +159,8 @@ const ProposalNavbar = () => {
             {...a11yProps(3)}
             key={3}
             sx={{ '&::after': { display: value === 3 ? 'block' : 'none' } }}
-            onClick={() => navigate('/dac/creating')}
+            onClick={() => handleTabClick('/dac/creating', 3)}
+            disabled={isNavigating}
           />
         )}
       </Tabs>
diff --git a/src/hooks/useProposals.ts b/src/hooks/useProposals.ts
index 5577533..f8c0642 100644
--- a/src/hooks/useProposals.ts
+++ b/src/hooks/useProposals.ts
@@ -15,35 +15,46 @@ import {
 } from '@/types';
 import { BinTools, Buffer, platformvm } from '@c4tplatform/caminojs/dist';
 import { Serialization } from '@c4tplatform/caminojs/dist/utils/serialization';
-import { useMutation, useQuery } from '@tanstack/react-query';
+import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
 import { filter, find, orderBy } from 'lodash';
 import { DateTime } from 'luxon';
 import { useMultisig } from './useMultisig';
 import useToast from './useToast';
 import useWallet from './useWallet';
 import { GeneralProposal } from '@c4tplatform/caminojs/dist/apis/platformvm';
-
-const bintools: BinTools = BinTools.getInstance();
+import { useRef, useEffect, useState, useMemo } from 'react';
+import { useLocation } from 'react-router-dom';
 
 const serialization = Serialization.getInstance();
+
+export const QUERY_KEYS = {
+  ACTIVE: 'activeProposals',
+  UPCOMING: 'upcomingProposals',
+  COMPLETED: 'completedProposals',
+  DETAIL: 'proposalDetail',
+};
+
 const parseAPIProposal = (proposal?: APIProposal) => {
-  if (proposal) {
+  if (!proposal) return undefined;
+
+  try {
     const optionsBuf = serialization.typeToBuffer(proposal.options, 'base64');
     const options = JSON.parse(optionsBuf.toString());
+
     let outcome;
     if (proposal.outcome) {
       const outcomeBuf = serialization.typeToBuffer(proposal.outcome, 'base64');
       outcome = JSON.parse(outcomeBuf.toString());
     }
+
     const proposalType =
       Object.values(ProposalTypes)[proposal.type === 3 ? 5 : proposal.type];
+
     let target;
     if (proposal.data) {
       const data = serialization.typeToBuffer(proposal.data, 'base64');
       switch (proposalType) {
         case ProposalTypes.NewMember:
-          target = JSON.parse(data.toString());
-          break;
         case ProposalTypes.ExcludeMember:
           target = JSON.parse(data.toString());
           break;
@@ -51,6 +62,7 @@ const parseAPIProposal = (proposal?: APIProposal) => {
           console.warn('Unknown data of proposal to process', data);
       }
     }
+
     const now = DateTime.now();
     const startTime = DateTime.fromISO(proposal.startTime);
     const endTime = DateTime.fromISO(proposal.endTime);
@@ -59,6 +71,7 @@ const parseAPIProposal = (proposal?: APIProposal) => {
       (Object.values(ProposalStatuses).indexOf(ProposalStatuses.Completed) &
         (proposal.status ?? 0)) >
       0;
+
     return {
       ...proposal,
       outcome,
@@ -78,42 +91,72 @@ const parseAPIProposal = (proposal?: APIProposal) => {
         .typeToBuffer(proposal.memo, 'base64')
         .toString(),
     };
+  } catch (error) {
+    console.error('Error parsing API proposal:', error);
+    return undefined;
   }
-  return;
 };
+
 const parseAPIProposals = (
   proposals?: APIPRoposalWrapper[],
   currentWalletAddress?: string
 ) => {
   if (!proposals) return [];
-  return proposals.map(proposalWrapper => {
-    const proposal = parseAPIProposal(proposalWrapper.dacProposal);
-    const votes = proposalWrapper.dacVotes.map(vote => parseAPIVote(vote));
-    let voted;
-    if (currentWalletAddress) {
-      voted = find(votes, { voterAddr: currentWalletAddress });
-      voted = voted?.votedOptions.map((v: number) => ({ option: v }));
-    }
+
+  return proposals
+    .map(proposalWrapper => {
+      try {
+        const proposal = parseAPIProposal(proposalWrapper.dacProposal);
+        if (!proposal) return null;
+
+        const votes = proposalWrapper.dacVotes
+          .map(vote => parseAPIVote(vote))
+          .filter(Boolean);
+
+        let voted;
+        if (currentWalletAddress) {
+          voted = find(votes, { voterAddr: currentWalletAddress });
+          voted = voted?.votedOptions.map((v: number) => ({ option: v }));
+        }
+
+        return {
+          ...proposal,
+          votes,
+          voted,
+        };
+      } catch (error) {
+        console.error('Error parsing proposal wrapper:', error);
+        return null;
+      }
+    })
+    .filter(Boolean) as Proposal[];
+};
+
+const parseAPIVote = (vote: APIVote) => {
+  try {
+    const votedOptionsBuf = serialization.typeToBuffer(
+      vote.votedOptions,
+      'base64'
+    );
+    const votedOptions = JSON.parse(votedOptionsBuf.toString());
+    const votedDateTime = DateTime.fromISO(vote.votedAt).toUnixInteger();
 
     return {
-      ...proposal,
-      votes,
-      voted,
+      ...vote,
+      votedDateTime,
+      votedOptions,
     };
-  });
+  } catch (error) {
+    console.error('Error parsing API vote:', error);
+    return null;
+  }
 };
-const parseAPIVote = (vote: APIVote) => {
-  const votedOptionsBuf = serialization.typeToBuffer(
-    vote.votedOptions,
-    'base64'
-  );
-  const votedOptions = JSON.parse(votedOptionsBuf.toString());
-  const votedDateTime = DateTime.fromISO(vote.votedAt).toUnixInteger();
-  return {
-    ...vote,
-    votedDateTime,
-    votedOptions,
-  };
+
+export const getViewTypeFromPath = (path: string) => {
+  if (path.includes('/dac/active')) return 'ongoing';
+  if (path.includes('/dac/upcoming')) return 'upcoming';
+  if (path.includes('/dac/completed')) return 'completed';
+  return null;
 };
 
 export const useActiveVotings = (
@@ -121,57 +164,177 @@ export const useActiveVotings = (
   currentWalletAddress?: string,
   page = 0
 ) => {
+  const queryClient = useQueryClient();
+  const location = useLocation();
+  const abortControllerRef = useRef<AbortController | null>(null);
+  const [forceRefetch, setForceRefetch] = useState(0);
+  const isCurrentView = location.pathname.includes('/dac/active');
+
+  useEffect(() => {
+    abortControllerRef.current = new AbortController();
+
+    return () => {
+      if (abortControllerRef.current) {
+        abortControllerRef.current.abort();
+        abortControllerRef.current = null;
+      }
+    };
+  }, []);
+
+  useEffect(() => {
+    if (isCurrentView) {
+      setForceRefetch(prev => prev + 1);
+    }
+  }, [location.pathname, isCurrentView]);
+
   const { data, isInitialLoading, isFetching, refetch, error } = useQuery(
-    ['getActiveVotings', currentWalletAddress, page],
+    [QUERY_KEYS.ACTIVE, currentWalletAddress, page, forceRefetch],
     async () => {
-      const result = await fetchActiveVotings(page);
-      let proposals = parseAPIProposals(
-        result?.data.dacProposals,
-        currentWalletAddress
-      );
-      if (pchainAPI && currentWalletAddress) {
-        proposals = await Promise.all(
-          proposals.map(async proposal => {
-            const response = await pchainAPI?.getValidatorsAt(
-              proposal.blockHeight
-            );
-            const canVote = !!find(
-              response?.validators,
-              validator =>
-                validator.consortiumMemberAddress === currentWalletAddress
-            );
-            return {
-              ...proposal,
-              canVote: canVote && !proposal.voted,
-            };
-          })
+      if (abortControllerRef.current) {
+        abortControllerRef.current.abort();
+        abortControllerRef.current = new AbortController();
+      }
+
+      try {
+        const result = await fetchActiveVotings(page);
+
+        let proposals = parseAPIProposals(
+          result?.data.dacProposals,
+          currentWalletAddress
         );
+
+        if (pchainAPI && currentWalletAddress) {
+          proposals = await Promise.all(
+            proposals.map(async proposal => {
+              try {
+                if (abortControllerRef.current?.signal.aborted) {
+                  throw new Error('Request aborted');
+                }
+
+                const response = await pchainAPI?.getValidatorsAt(
+                  proposal.blockHeight
+                );
+
+                const canVote = !!find(
+                  response?.validators,
+                  validator =>
+                    validator.consortiumMemberAddress === currentWalletAddress
+                );
+                return {
+                  ...proposal,
+                  canVote: canVote && !proposal.voted,
+                };
+              } catch (error) {
+                return proposal;
+              }
+            })
+          );
+        }
+
+        return proposals.filter(p => !p.inactive);
+      } catch (error) {
+        if ((error as any)?.name === 'AbortError') {
+          return [];
+        }
+        throw error;
       }
-      return proposals;
+    },
+    {
+      refetchOnWindowFocus: false,
+      staleTime: 10000, // 10 seconds
+      enabled: isCurrentView,
+      retry: false,
+      onError: err => {
+        console.error('Error fetching active votings:', err);
+        queryClient.setQueryData(
+          [QUERY_KEYS.ACTIVE, currentWalletAddress, page],
+          []
+        );
+      },
     }
   );
 
+  const customRefetch = () => {
+    setForceRefetch(prev => prev + 1);
+    return refetch();
+  };
+
   return {
     isFetching,
     isInitialLoading,
     error,
-    refetch,
-    proposals: data ?? [],
+    refetch: customRefetch,
+    proposals: isCurrentView ? data || [] : [],
   };
 };
 
 export const useUpcomingVotings = (page = 0) => {
-  const { proposals, isFetching, error, refetch } = useActiveVotings(
-    undefined,
-    undefined,
-    page
+  const queryClient = useQueryClient();
+  const location = useLocation();
+  const abortControllerRef = useRef<AbortController | null>(null);
+  const [forceRefetch, setForceRefetch] = useState(0);
+  const isCurrentView = location.pathname.includes('/dac/upcoming');
+
+  useEffect(() => {
+    abortControllerRef.current = new AbortController();
+
+    return () => {
+      if (abortControllerRef.current) {
+        abortControllerRef.current.abort();
+        abortControllerRef.current = null;
+      }
+    };
+  }, []);
+
+  useEffect(() => {
+    if (isCurrentView) {
+      setForceRefetch(prev => prev + 1);
+    }
+  }, [location.pathname, isCurrentView]);
+
+  const { data, isInitialLoading, isFetching, refetch, error } = useQuery(
+    [QUERY_KEYS.UPCOMING, page, forceRefetch],
+    async () => {
+      if (abortControllerRef.current) {
+        abortControllerRef.current.abort();
+        abortControllerRef.current = new AbortController();
+      }
+
+      try {
+        const result = await fetchActiveVotings(page);
+
+        const proposals = parseAPIProposals(result?.data.dacProposals);
+        return proposals.filter(p => p.inactive);
+      } catch (error) {
+        if ((error as any)?.name === 'AbortError') {
+          return [];
+        }
+        throw error;
+      }
+    },
+    {
+      refetchOnWindowFocus: false,
+      staleTime: 10000,
+      enabled: isCurrentView,
+      retry: false,
+      onError: err => {
+        console.error('Error fetching upcoming votings:', err);
+        queryClient.setQueryData([QUERY_KEYS.UPCOMING, page], []);
+      },
+    }
   );
-  const upcomings = filter(proposals, proposal => proposal.inactive);
+
+  const customRefetch = () => {
+    setForceRefetch(prev => prev + 1);
+    return refetch();
+  };
+
   return {
-    proposals: upcomings,
     isFetching,
+    isInitialLoading,
     error,
-    refetch,
+    refetch: customRefetch,
+    proposals: isCurrentView ? data || [] : [],
   };
 };
 
@@ -181,27 +344,106 @@ export const useCompletedVotes = (
   endTime?: string,
   page = 0
 ) => {
+  const queryClient = useQueryClient();
+  const location = useLocation();
+  const abortControllerRef = useRef<AbortController | null>(null);
+  const [forceRefetch, setForceRefetch] = useState(0);
+  const isCurrentView = location.pathname.includes('/dac/completed');
+
+  useEffect(() => {
+    abortControllerRef.current = new AbortController();
+
+    return () => {
+      if (abortControllerRef.current) {
+        abortControllerRef.current.abort();
+        abortControllerRef.current = null;
+      }
+    };
+  }, []);
+
+  useEffect(() => {
+    if (isCurrentView) {
+      setForceRefetch(prev => prev + 1);
+    }
+  }, [location.pathname, isCurrentView, type, startTime, endTime, page]);
+
   const { data, isFetching, error, refetch } = useQuery(
-    ['getCompletedVotes', type === 5 ? 3 : type, startTime, endTime, page],
-    async () => fetchCompletedVotes(type === 5 ? 3 : type, startTime, endTime),
+    [
+      QUERY_KEYS.COMPLETED,
+      type === 5 ? 3 : type,
+      startTime,
+      endTime,
+      page,
+      forceRefetch,
+    ],
+    async () => {
+      if (abortControllerRef.current) {
+        abortControllerRef.current.abort();
+        abortControllerRef.current = new AbortController();
+      }
+
+      try {
+        const result = await fetchCompletedVotes(
+          type === 5 ? 3 : type,
+          startTime,
+          endTime
+        );
+
+        return result;
+      } catch (error) {
+        if ((error as any)?.name === 'AbortError') {
+          return { data: { dacProposals: [] } };
+        }
+        throw error;
+      }
+    },
     {
       refetchOnWindowFocus: false,
-      // notifyOnChangeProps: ['data', 'error'],
+      staleTime: 10000,
+      enabled: isCurrentView,
+      retry: false,
+      onError: err => {
+        console.error('Error fetching completed votes:', err);
+        queryClient.setQueryData(
+          [
+            QUERY_KEYS.COMPLETED,
+            type === 5 ? 3 : type,
+            startTime,
+            endTime,
+            page,
+          ],
+          { data: { dacProposals: [] } }
+        );
+      },
     }
   );
-  const proposals = parseAPIProposals(data?.data.dacProposals);
-  const sortedProposals = orderBy(proposals, ['startTimestamp'], ['desc']);
+
+  const proposals = useMemo(() => {
+    if (!isCurrentView || !data || error) return [];
+
+    const parsedProposals = parseAPIProposals(data?.data.dacProposals);
+    const sortedProposals = orderBy(
+      parsedProposals,
+      ['startTimestamp'],
+      ['desc']
+    );
+
+    return sortedProposals.map((p, idx) => ({
+      ...p,
+      seq: sortedProposals.length - idx,
+    }));
+  }, [data, error, isCurrentView]);
+
+  const customRefetch = () => {
+    setForceRefetch(prev => prev + 1);
+    return refetch();
+  };
 
   return {
-    proposals: error
-      ? []
-      : sortedProposals.map((p, idx) => ({
-          ...p,
-          seq: sortedProposals.length - idx,
-        })),
+    proposals,
     error,
     isFetching,
-    refetch,
+    refetch: customRefetch,
   };
 };
 
diff --git a/src/pages/proposal/active/index.tsx b/src/pages/proposal/active/index.tsx
index a569be4..34df935 100644
--- a/src/pages/proposal/active/index.tsx
+++ b/src/pages/proposal/active/index.tsx
@@ -17,7 +17,7 @@ import { ProposalType } from '@/types';
 import { ExpandMore } from '@mui/icons-material';
 import { FormControlLabel, Stack, useTheme } from '@mui/material';
 import { filter, find } from 'lodash';
-import React, { useMemo, useState } from 'react';
+import React, { useMemo, useState, useEffect, useRef } from 'react';
 import { useLoaderData } from 'react-router-dom';
 import GroupHeader from './GroupHeader';
 import NoProposals from './NoProposals';
@@ -34,6 +34,8 @@ const ActiveVotings = () => {
   const isDark = theme.palette.mode === 'dark';
   const { isConsortiumMember } = addressState;
   const [onlyTodo, setOnlyTodo] = useState(false);
+  const initialFetchCompleted = useRef(false);
+
   const { proposals, error, refetch, isFetching } = useActiveVotings(
     wallet.pchainAPI,
     currentWalletAddress
@@ -47,6 +49,32 @@ const ActiveVotings = () => {
     currentNetwork: state.currentNetwork,
   }));
 
+  useEffect(() => {
+    if (!initialFetchCompleted.current) {
+      const timer = setTimeout(() => {
+        refetch()
+          .then(() => {
+            initialFetchCompleted.current = true;
+          })
+          .catch(err => {
+            console.error('Error during initial fetch:', err);
+          });
+
+        // Also fetch pending multisig transactions
+        refetchPendingMultisigTxs();
+      }, 100);
+
+      return () => clearTimeout(timer);
+    }
+  }, []);
+
+  useEffect(() => {
+    if (initialFetchCompleted.current) {
+      refetch();
+      refetchPendingMultisigTxs();
+    }
+  }, [currentWalletAddress, currentNetwork]);
+
   const groupedProposals = useMemo(() => {
     if (error) return {};
     let filteredProposals = filter(proposals, proposal => !proposal.inactive);
@@ -88,6 +116,11 @@ const ActiveVotings = () => {
     }, {});
   }, [proposals, onlyTodo, pendingMultisigAddVoteTxs, currentNetwork, error]);
 
+  const handleRefresh = () => {
+    refetch();
+    refetchPendingMultisigTxs();
+  };
+
   return (
     <Paper sx={{ p: 2 }}>
       <Header headline="Ongoing Proposals" variant="h6">
@@ -106,10 +139,7 @@ const ActiveVotings = () => {
           )}
           <RefreshButton
             loading={isFetching || isFetchingPendingMultisigTxs}
-            onRefresh={() => {
-              refetch();
-              refetchPendingMultisigTxs();
-            }}
+            onRefresh={handleRefresh}
           />
         </Stack>
       </Header>


Reviews:
user: login: VjeraTurk
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-15T08:54:37Z
user: login: VjeraTurk
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-15T08:58:09Z
user: login: aeddaqqa
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T09:18:05Z
user: login: aeddaqqa
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T09:20:54Z
user: login: VjeraTurk
state: DISMISSED
author_association: NONE
submitted_at: 2025-04-16T13:14:15Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 3**

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>src/pages/proposal/active/index.tsx (1)</summary><blockquote>

`52-70`: **Timer-based initial fetch is fragile**

1. If `refetch` throws synchronously the `initialFetchCompleted` flag is never set, causing later refreshes to be skipped.  
2. `refetchPendingMultisigTxs()` is fired even when `refetch()` fails – the two calls should succeed/fail together to keep views in sync.  
3. Using `setTimeout` for a 100 ms “debounce” provides little benefit and complicates reasoning; `React Query` already batches renders.

Consider replacing the whole effect with `useEffect(refetch, [])` or using `queryClient.invalidateQueries` in the navbar.

</blockquote></details>
<details>
<summary>src/components/Navbar.tsx (2)</summary><blockquote>

`140-155`: **Duplicate hard-coded query keys**

The same hard-coded arrays appear in `handleChange` and `handleTabClick`.
Extract them to a helper that uses the `QUERY_KEYS` enum to stay
type-safe and DRY.

```ts
const invalidateForTab = (tab: number) => {
  switch (tab) {
    case 0:
      queryClient.invalidateQueries([QUERY_KEYS.ACTIVE]);
      break;
    case 1:
      queryClient.invalidateQueries([QUERY_KEYS.UPCOMING]);
      break;
    case 2:
      queryClient.invalidateQueries([QUERY_KEYS.COMPLETED]);
      break;
  }
};
```

---

`103-123`: **`useMemo` misses `currentWalletAddress` dependency**

`pendingForCurrentAlias` relies on `currentWalletAddress` but the memo
only depends on `pendingMultisigTxs`.  
Switching wallets without reloading leaves stale badge counts.

```diff
-}, [pendingMultisigTxs]);
+}, [pendingMultisigTxs, currentWalletAddress]);
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between c79d287a210e7b2943f155b1c221e9ca2ef7bae0 and 0e43bec8484b11788681717c0860c04ca2c71326.

</details>

<details>
<summary>📒 Files selected for processing (4)</summary>

* `src/components/Navbar.tsx` (9 hunks)
* `src/hooks/useProposals.ts` (4 hunks)
* `src/pages/proposal/active/index.tsx` (5 hunks)
* `src/pages/proposal/detail/ProposalStatus.tsx` (1 hunks)

</details>

<details>
<summary>🔇 Additional comments (2)</summary><blockquote>

<details>
<summary>src/pages/proposal/active/index.tsx (1)</summary>

`71-76`: **Missing dependencies may skip refetches**

The second effect ignores `onlyTodo`, `page`, or other props that change the underlying query key.  If any of these change while the view is open the list will stay stale.

Add them to the dependency array or call `queryClient.invalidateQueries` instead of manual `refetch` calls.

</details>
<details>
<summary>src/hooks/useProposals.ts (1)</summary>

`303-307`: **`useUpcomingVotings` fetches the wrong endpoint**

`fetchActiveVotings()` returns ongoing proposals; upcoming data should
come from a dedicated endpoint (e.g. `fetchUpcomingVotings`) or from the
same endpoint but with a different server-side filter flag.

Confirm with the backend team and swap the call if necessary — otherwise
upcoming tab may be empty or stale.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-29T07:14:20Z
user: login: aeddaqqa
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T09:56:01Z
user: login: aeddaqqa
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T09:56:16Z
user: login: aeddaqqa
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T09:56:23Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-29T09:56:34Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-29T09:56:51Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-29T09:56:54Z
user: login: mo-c4t
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-29T10:42:19Z

Review comments:
diff_hunk: @@ -78,100 +91,250 @@ const parseAPIProposal = (proposal?: APIProposal) => {
         .typeToBuffer(proposal.memo, 'base64')
         .toString(),
     };
+  } catch (error) {
+    console.error('Error parsing API proposal:', error);
+    return undefined;
   }
-  return;
 };
+
 const parseAPIProposals = (
   proposals?: APIPRoposalWrapper[],
   currentWalletAddress?: string
 ) => {
   if (!proposals) return [];
-  return proposals.map(proposalWrapper => {
-    const proposal = parseAPIProposal(proposalWrapper.dacProposal);
-    const votes = proposalWrapper.dacVotes.map(vote => parseAPIVote(vote));
-    let voted;
-    if (currentWalletAddress) {
-      voted = find(votes, { voterAddr: currentWalletAddress });
-      voted = voted?.votedOptions.map((v: number) => ({ option: v }));
-    }
+
+  return proposals
+    .map(proposalWrapper => {
+      try {
+        const proposal = parseAPIProposal(proposalWrapper.dacProposal);
+        if (!proposal) return null;
+
+        const votes = proposalWrapper.dacVotes
+          .map(vote => parseAPIVote(vote))
+          .filter(Boolean);
+
+        let voted;
+        if (currentWalletAddress) {
+          voted = find(votes, { voterAddr: currentWalletAddress });
+          voted = voted?.votedOptions.map((v: number) => ({ option: v }));
+        }
+
+        return {
+          ...proposal,
+          votes,
+          voted,
+        };
+      } catch (error) {
+        console.error('Error parsing proposal wrapper:', error);
+        return null;
+      }
+    })
+    .filter(Boolean) as Proposal[];
+};
+
+const parseAPIVote = (vote: APIVote) => {
+  try {
+    const votedOptionsBuf = serialization.typeToBuffer(
+      vote.votedOptions,
+      'base64'
+    );
+    const votedOptions = JSON.parse(votedOptionsBuf.toString());
+    const votedDateTime = DateTime.fromISO(vote.votedAt).toUnixInteger();
 
     return {
-      ...proposal,
-      votes,
-      voted,
+      ...vote,
+      votedDateTime,
+      votedOptions,
     };
-  });
+  } catch (error) {
+    console.error('Error parsing API vote:', error);
+    return null;
+  }
 };
-const parseAPIVote = (vote: APIVote) => {
-  const votedOptionsBuf = serialization.typeToBuffer(
-    vote.votedOptions,
-    'base64'
-  );
-  const votedOptions = JSON.parse(votedOptionsBuf.toString());
-  const votedDateTime = DateTime.fromISO(vote.votedAt).toUnixInteger();
-  return {
-    ...vote,
-    votedDateTime,
-    votedOptions,
-  };
+
+export const getViewTypeFromPath = (path: string) => {
+  if (path.includes('/dac/active')) return 'ongoing';
+  if (path.includes('/dac/upcoming')) return 'upcoming';
+  if (path.includes('/dac/completed')) return 'completed';
+  return null;
 };
 
 export const useActiveVotings = (
   pchainAPI?: platformvm.PlatformVMAPI,
   currentWalletAddress?: string,
   page = 0
 ) => {
+  const queryClient = useQueryClient();
+  const location = useLocation();
+  const abortControllerRef = useRef<AbortController | null>(null);
+  const [forceRefetch, setForceRefetch] = useState(0);
+  const isCurrentView = location.pathname.includes('/dac/active');
+
+  useEffect(() => {
+    abortControllerRef.current = new AbortController();
+
+    return () => {
+      if (abortControllerRef.current) {
+        abortControllerRef.current.abort();
+        abortControllerRef.current = null;
+      }
+    };
+  }, []);
+
+  useEffect(() => {
+    if (isCurrentView) {
+      setForceRefetch(prev => prev + 1);
+    }
+  }, [location.pathname, isCurrentView]);
+
   const { data, isInitialLoading, isFetching, refetch, error } = useQuery(
-    ['getActiveVotings', currentWalletAddress, page],
+    [QUERY_KEYS.ACTIVE, currentWalletAddress, page, forceRefetch],
     async () => {
-      const result = await fetchActiveVotings(page);
-      let proposals = parseAPIProposals(
-        result?.data.dacProposals,
-        currentWalletAddress
-      );
-      if (pchainAPI && currentWalletAddress) {
-        proposals = await Promise.all(
-          proposals.map(async proposal => {
-            const response = await pchainAPI?.getValidatorsAt(
-              proposal.blockHeight
-            );
-            const canVote = !!find(
-              response?.validators,
-              validator =>
-                validator.consortiumMemberAddress === currentWalletAddress
-            );
-            return {
-              ...proposal,
-              canVote: canVote && !proposal.voted,
-            };
-          })
+      if (abortControllerRef.current) {
+        abortControllerRef.current.abort();
+        abortControllerRef.current = new AbortController();
+      }
+
+      try {
+        const result = await fetchActiveVotings(page);
+
+        let proposals = parseAPIProposals(
+          result?.data.dacProposals,
+          currentWalletAddress
         );
+
+        if (pchainAPI && currentWalletAddress) {
+          proposals = await Promise.all(
+            proposals.map(async proposal => {
+              try {
+                if (abortControllerRef.current?.signal.aborted) {
+                  throw new Error('Request aborted');
+                }
+
+                const response = await pchainAPI?.getValidatorsAt(
+                  proposal.blockHeight
path: src/hooks/useProposals.ts
user: login: VjeraTurk
body: _Get the validators and their weights of a subnet or the Primary Network at a given P-Chain height._

What happens if the Validator is excluded at a later height? 
created_at: 2025-04-15T08:54:37Z
author_association: NONE
diff_hunk: @@ -58,9 +121,47 @@ const ProposalNavbar = () => {
         pendingAddVoteCount > 0 ? `${pendingAddVoteCount} pending` : undefined,
     };
   }, [pendingMultisigTxs]);
-  const navigate = useNavigate();
+
   const isCreateProposalAllowed = isKycVerified || isConsortiumAdminProposer;
   const enableCreateButton = currentWalletAddress && isCreateProposalAllowed;
+
+  const handleTabClick = (path: string, tabValue: number) => {
+    if (isNavigating || value === tabValue) return;
+
+    const now = Date.now();
+    if (now - lastNavigationTime.current < 300) {
+      return;
+    }
path: src/components/Navbar.tsx
user: login: VjeraTurk
body: Why is this check necessaey?
created_at: 2025-04-15T08:58:09Z
author_association: NONE
diff_hunk: @@ -58,9 +121,47 @@ const ProposalNavbar = () => {
         pendingAddVoteCount > 0 ? `${pendingAddVoteCount} pending` : undefined,
     };
   }, [pendingMultisigTxs]);
-  const navigate = useNavigate();
+
   const isCreateProposalAllowed = isKycVerified || isConsortiumAdminProposer;
   const enableCreateButton = currentWalletAddress && isCreateProposalAllowed;
+
+  const handleTabClick = (path: string, tabValue: number) => {
+    if (isNavigating || value === tabValue) return;
+
+    const now = Date.now();
+    if (now - lastNavigationTime.current < 300) {
+      return;
+    }
path: src/components/Navbar.tsx
user: login: aeddaqqa
body: This check implements a debounce mechanism to prevent double-clicks or multiple rapid clicks from triggering multiple actions
created_at: 2025-04-15T09:18:05Z
author_association: CONTRIBUTOR
diff_hunk: @@ -78,100 +91,250 @@ const parseAPIProposal = (proposal?: APIProposal) => {
         .typeToBuffer(proposal.memo, 'base64')
         .toString(),
     };
+  } catch (error) {
+    console.error('Error parsing API proposal:', error);
+    return undefined;
   }
-  return;
 };
+
 const parseAPIProposals = (
   proposals?: APIPRoposalWrapper[],
   currentWalletAddress?: string
 ) => {
   if (!proposals) return [];
-  return proposals.map(proposalWrapper => {
-    const proposal = parseAPIProposal(proposalWrapper.dacProposal);
-    const votes = proposalWrapper.dacVotes.map(vote => parseAPIVote(vote));
-    let voted;
-    if (currentWalletAddress) {
-      voted = find(votes, { voterAddr: currentWalletAddress });
-      voted = voted?.votedOptions.map((v: number) => ({ option: v }));
-    }
+
+  return proposals
+    .map(proposalWrapper => {
+      try {
+        const proposal = parseAPIProposal(proposalWrapper.dacProposal);
+        if (!proposal) return null;
+
+        const votes = proposalWrapper.dacVotes
+          .map(vote => parseAPIVote(vote))
+          .filter(Boolean);
+
+        let voted;
+        if (currentWalletAddress) {
+          voted = find(votes, { voterAddr: currentWalletAddress });
+          voted = voted?.votedOptions.map((v: number) => ({ option: v }));
+        }
+
+        return {
+          ...proposal,
+          votes,
+          voted,
+        };
+      } catch (error) {
+        console.error('Error parsing proposal wrapper:', error);
+        return null;
+      }
+    })
+    .filter(Boolean) as Proposal[];
+};
+
+const parseAPIVote = (vote: APIVote) => {
+  try {
+    const votedOptionsBuf = serialization.typeToBuffer(
+      vote.votedOptions,
+      'base64'
+    );
+    const votedOptions = JSON.parse(votedOptionsBuf.toString());
+    const votedDateTime = DateTime.fromISO(vote.votedAt).toUnixInteger();
 
     return {
-      ...proposal,
-      votes,
-      voted,
+      ...vote,
+      votedDateTime,
+      votedOptions,
     };
-  });
+  } catch (error) {
+    console.error('Error parsing API vote:', error);
+    return null;
+  }
 };
-const parseAPIVote = (vote: APIVote) => {
-  const votedOptionsBuf = serialization.typeToBuffer(
-    vote.votedOptions,
-    'base64'
-  );
-  const votedOptions = JSON.parse(votedOptionsBuf.toString());
-  const votedDateTime = DateTime.fromISO(vote.votedAt).toUnixInteger();
-  return {
-    ...vote,
-    votedDateTime,
-    votedOptions,
-  };
+
+export const getViewTypeFromPath = (path: string) => {
+  if (path.includes('/dac/active')) return 'ongoing';
+  if (path.includes('/dac/upcoming')) return 'upcoming';
+  if (path.includes('/dac/completed')) return 'completed';
+  return null;
 };
 
 export const useActiveVotings = (
   pchainAPI?: platformvm.PlatformVMAPI,
   currentWalletAddress?: string,
   page = 0
 ) => {
+  const queryClient = useQueryClient();
+  const location = useLocation();
+  const abortControllerRef = useRef<AbortController | null>(null);
+  const [forceRefetch, setForceRefetch] = useState(0);
+  const isCurrentView = location.pathname.includes('/dac/active');
+
+  useEffect(() => {
+    abortControllerRef.current = new AbortController();
+
+    return () => {
+      if (abortControllerRef.current) {
+        abortControllerRef.current.abort();
+        abortControllerRef.current = null;
+      }
+    };
+  }, []);
+
+  useEffect(() => {
+    if (isCurrentView) {
+      setForceRefetch(prev => prev + 1);
+    }
+  }, [location.pathname, isCurrentView]);
+
   const { data, isInitialLoading, isFetching, refetch, error } = useQuery(
-    ['getActiveVotings', currentWalletAddress, page],
+    [QUERY_KEYS.ACTIVE, currentWalletAddress, page, forceRefetch],
     async () => {
-      const result = await fetchActiveVotings(page);
-      let proposals = parseAPIProposals(
-        result?.data.dacProposals,
-        currentWalletAddress
-      );
-      if (pchainAPI && currentWalletAddress) {
-        proposals = await Promise.all(
-          proposals.map(async proposal => {
-            const response = await pchainAPI?.getValidatorsAt(
-              proposal.blockHeight
-            );
-            const canVote = !!find(
-              response?.validators,
-              validator =>
-                validator.consortiumMemberAddress === currentWalletAddress
-            );
-            return {
-              ...proposal,
-              canVote: canVote && !proposal.voted,
-            };
-          })
+      if (abortControllerRef.current) {
+        abortControllerRef.current.abort();
+        abortControllerRef.current = new AbortController();
+      }
+
+      try {
+        const result = await fetchActiveVotings(page);
+
+        let proposals = parseAPIProposals(
+          result?.data.dacProposals,
+          currentWalletAddress
         );
+
+        if (pchainAPI && currentWalletAddress) {
+          proposals = await Promise.all(
+            proposals.map(async proposal => {
+              try {
+                if (abortControllerRef.current?.signal.aborted) {
+                  throw new Error('Request aborted');
+                }
+
+                const response = await pchainAPI?.getValidatorsAt(
+                  proposal.blockHeight
path: src/hooks/useProposals.ts
user: login: aeddaqqa
body: We are checking if the validatorsAt are part of the current validators in the following lines 
created_at: 2025-04-15T09:20:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -78,100 +91,250 @@ const parseAPIProposal = (proposal?: APIProposal) => {
         .typeToBuffer(proposal.memo, 'base64')
         .toString(),
     };
+  } catch (error) {
+    console.error('Error parsing API proposal:', error);
+    return undefined;
   }
-  return;
 };
+
 const parseAPIProposals = (
   proposals?: APIPRoposalWrapper[],
   currentWalletAddress?: string
 ) => {
   if (!proposals) return [];
-  return proposals.map(proposalWrapper => {
-    const proposal = parseAPIProposal(proposalWrapper.dacProposal);
-    const votes = proposalWrapper.dacVotes.map(vote => parseAPIVote(vote));
-    let voted;
-    if (currentWalletAddress) {
-      voted = find(votes, { voterAddr: currentWalletAddress });
-      voted = voted?.votedOptions.map((v: number) => ({ option: v }));
-    }
+
+  return proposals
+    .map(proposalWrapper => {
+      try {
+        const proposal = parseAPIProposal(proposalWrapper.dacProposal);
+        if (!proposal) return null;
+
+        const votes = proposalWrapper.dacVotes
+          .map(vote => parseAPIVote(vote))
+          .filter(Boolean);
+
+        let voted;
+        if (currentWalletAddress) {
+          voted = find(votes, { voterAddr: currentWalletAddress });
+          voted = voted?.votedOptions.map((v: number) => ({ option: v }));
+        }
+
+        return {
+          ...proposal,
+          votes,
+          voted,
+        };
+      } catch (error) {
+        console.error('Error parsing proposal wrapper:', error);
+        return null;
+      }
+    })
+    .filter(Boolean) as Proposal[];
+};
+
+const parseAPIVote = (vote: APIVote) => {
+  try {
+    const votedOptionsBuf = serialization.typeToBuffer(
+      vote.votedOptions,
+      'base64'
+    );
+    const votedOptions = JSON.parse(votedOptionsBuf.toString());
+    const votedDateTime = DateTime.fromISO(vote.votedAt).toUnixInteger();
 
     return {
-      ...proposal,
-      votes,
-      voted,
+      ...vote,
+      votedDateTime,
+      votedOptions,
     };
-  });
+  } catch (error) {
+    console.error('Error parsing API vote:', error);
+    return null;
+  }
 };
-const parseAPIVote = (vote: APIVote) => {
-  const votedOptionsBuf = serialization.typeToBuffer(
-    vote.votedOptions,
-    'base64'
-  );
-  const votedOptions = JSON.parse(votedOptionsBuf.toString());
-  const votedDateTime = DateTime.fromISO(vote.votedAt).toUnixInteger();
-  return {
-    ...vote,
-    votedDateTime,
-    votedOptions,
-  };
+
+export const getViewTypeFromPath = (path: string) => {
+  if (path.includes('/dac/active')) return 'ongoing';
+  if (path.includes('/dac/upcoming')) return 'upcoming';
+  if (path.includes('/dac/completed')) return 'completed';
+  return null;
 };
 
 export const useActiveVotings = (
   pchainAPI?: platformvm.PlatformVMAPI,
   currentWalletAddress?: string,
   page = 0
 ) => {
+  const queryClient = useQueryClient();
+  const location = useLocation();
+  const abortControllerRef = useRef<AbortController | null>(null);
+  const [forceRefetch, setForceRefetch] = useState(0);
+  const isCurrentView = location.pathname.includes('/dac/active');
+
+  useEffect(() => {
+    abortControllerRef.current = new AbortController();
+
+    return () => {
+      if (abortControllerRef.current) {
+        abortControllerRef.current.abort();
+        abortControllerRef.current = null;
+      }
+    };
+  }, []);
+
+  useEffect(() => {
+    if (isCurrentView) {
+      setForceRefetch(prev => prev + 1);
+    }
+  }, [location.pathname, isCurrentView]);
+
   const { data, isInitialLoading, isFetching, refetch, error } = useQuery(
-    ['getActiveVotings', currentWalletAddress, page],
+    [QUERY_KEYS.ACTIVE, currentWalletAddress, page, forceRefetch],
     async () => {
-      const result = await fetchActiveVotings(page);
-      let proposals = parseAPIProposals(
-        result?.data.dacProposals,
-        currentWalletAddress
-      );
-      if (pchainAPI && currentWalletAddress) {
-        proposals = await Promise.all(
-          proposals.map(async proposal => {
-            const response = await pchainAPI?.getValidatorsAt(
-              proposal.blockHeight
-            );
-            const canVote = !!find(
-              response?.validators,
-              validator =>
-                validator.consortiumMemberAddress === currentWalletAddress
-            );
-            return {
-              ...proposal,
-              canVote: canVote && !proposal.voted,
-            };
-          })
+      if (abortControllerRef.current) {
+        abortControllerRef.current.abort();
+        abortControllerRef.current = new AbortController();
+      }
+
+      try {
+        const result = await fetchActiveVotings(page);
+
+        let proposals = parseAPIProposals(
+          result?.data.dacProposals,
+          currentWalletAddress
         );
+
+        if (pchainAPI && currentWalletAddress) {
+          proposals = await Promise.all(
+            proposals.map(async proposal => {
+              try {
+                if (abortControllerRef.current?.signal.aborted) {
+                  throw new Error('Request aborted');
+                }
+
+                const response = await pchainAPI?.getValidatorsAt(
+                  proposal.blockHeight
path: src/hooks/useProposals.ts
user: login: VjeraTurk
body: This is the check, right?
https://github.com/chain4travel/camino-suite-voting/pull/58/files#diff-d5379945a014e26455722e94c063ae8fd81b33727d1b28b5bfae19b11e2ababdR63-R73

Should the same check be performed here, and then only the intersection of the validators can vote? 
created_at: 2025-04-15T11:44:10Z
author_association: NONE
diff_hunk: @@ -78,100 +91,250 @@ const parseAPIProposal = (proposal?: APIProposal) => {
         .typeToBuffer(proposal.memo, 'base64')
         .toString(),
     };
+  } catch (error) {
+    console.error('Error parsing API proposal:', error);
+    return undefined;
   }
-  return;
 };
+
 const parseAPIProposals = (
   proposals?: APIPRoposalWrapper[],
   currentWalletAddress?: string
 ) => {
   if (!proposals) return [];
-  return proposals.map(proposalWrapper => {
-    const proposal = parseAPIProposal(proposalWrapper.dacProposal);
-    const votes = proposalWrapper.dacVotes.map(vote => parseAPIVote(vote));
-    let voted;
-    if (currentWalletAddress) {
-      voted = find(votes, { voterAddr: currentWalletAddress });
-      voted = voted?.votedOptions.map((v: number) => ({ option: v }));
-    }
+
+  return proposals
+    .map(proposalWrapper => {
+      try {
+        const proposal = parseAPIProposal(proposalWrapper.dacProposal);
+        if (!proposal) return null;
+
+        const votes = proposalWrapper.dacVotes
+          .map(vote => parseAPIVote(vote))
+          .filter(Boolean);
+
+        let voted;
+        if (currentWalletAddress) {
+          voted = find(votes, { voterAddr: currentWalletAddress });
+          voted = voted?.votedOptions.map((v: number) => ({ option: v }));
+        }
+
+        return {
+          ...proposal,
+          votes,
+          voted,
+        };
+      } catch (error) {
+        console.error('Error parsing proposal wrapper:', error);
+        return null;
+      }
+    })
+    .filter(Boolean) as Proposal[];
+};
+
+const parseAPIVote = (vote: APIVote) => {
+  try {
+    const votedOptionsBuf = serialization.typeToBuffer(
+      vote.votedOptions,
+      'base64'
+    );
+    const votedOptions = JSON.parse(votedOptionsBuf.toString());
+    const votedDateTime = DateTime.fromISO(vote.votedAt).toUnixInteger();
 
     return {
-      ...proposal,
-      votes,
-      voted,
+      ...vote,
+      votedDateTime,
+      votedOptions,
     };
-  });
+  } catch (error) {
+    console.error('Error parsing API vote:', error);
+    return null;
+  }
 };
-const parseAPIVote = (vote: APIVote) => {
-  const votedOptionsBuf = serialization.typeToBuffer(
-    vote.votedOptions,
-    'base64'
-  );
-  const votedOptions = JSON.parse(votedOptionsBuf.toString());
-  const votedDateTime = DateTime.fromISO(vote.votedAt).toUnixInteger();
-  return {
-    ...vote,
-    votedDateTime,
-    votedOptions,
-  };
+
+export const getViewTypeFromPath = (path: string) => {
+  if (path.includes('/dac/active')) return 'ongoing';
+  if (path.includes('/dac/upcoming')) return 'upcoming';
+  if (path.includes('/dac/completed')) return 'completed';
+  return null;
 };
 
 export const useActiveVotings = (
   pchainAPI?: platformvm.PlatformVMAPI,
   currentWalletAddress?: string,
   page = 0
 ) => {
+  const queryClient = useQueryClient();
+  const location = useLocation();
+  const abortControllerRef = useRef<AbortController | null>(null);
+  const [forceRefetch, setForceRefetch] = useState(0);
+  const isCurrentView = location.pathname.includes('/dac/active');
+
+  useEffect(() => {
+    abortControllerRef.current = new AbortController();
+
+    return () => {
+      if (abortControllerRef.current) {
+        abortControllerRef.current.abort();
+        abortControllerRef.current = null;
+      }
+    };
+  }, []);
+
+  useEffect(() => {
+    if (isCurrentView) {
+      setForceRefetch(prev => prev + 1);
+    }
+  }, [location.pathname, isCurrentView]);
+
   const { data, isInitialLoading, isFetching, refetch, error } = useQuery(
-    ['getActiveVotings', currentWalletAddress, page],
+    [QUERY_KEYS.ACTIVE, currentWalletAddress, page, forceRefetch],
     async () => {
-      const result = await fetchActiveVotings(page);
-      let proposals = parseAPIProposals(
-        result?.data.dacProposals,
-        currentWalletAddress
-      );
-      if (pchainAPI && currentWalletAddress) {
-        proposals = await Promise.all(
-          proposals.map(async proposal => {
-            const response = await pchainAPI?.getValidatorsAt(
-              proposal.blockHeight
-            );
-            const canVote = !!find(
-              response?.validators,
-              validator =>
-                validator.consortiumMemberAddress === currentWalletAddress
-            );
-            return {
-              ...proposal,
-              canVote: canVote && !proposal.voted,
-            };
-          })
+      if (abortControllerRef.current) {
+        abortControllerRef.current.abort();
+        abortControllerRef.current = new AbortController();
+      }
+
+      try {
+        const result = await fetchActiveVotings(page);
+
+        let proposals = parseAPIProposals(
+          result?.data.dacProposals,
+          currentWalletAddress
         );
+
+        if (pchainAPI && currentWalletAddress) {
+          proposals = await Promise.all(
+            proposals.map(async proposal => {
+              try {
+                if (abortControllerRef.current?.signal.aborted) {
+                  throw new Error('Request aborted');
+                }
+
+                const response = await pchainAPI?.getValidatorsAt(
+                  proposal.blockHeight
path: src/hooks/useProposals.ts
user: login: VjeraTurk
body: Node does the check https://github.com/chain4travel/caminogo/blob/chain4travel/vms/platformvm/txs/executor/camino_tx_executor.go#L1950-L1952

_hard criteria checked by the node for casting a vote._
created_at: 2025-04-16T13:14:00Z
author_association: NONE
diff_hunk: @@ -253,7 +242,9 @@ const ProposalStatus = ({
                   <Typography variant="body2" color="text.secondary">
                     Did not participate
                   </Typography>
-                ) : hasPendingMultisigVote ? (
+                ) : isLoggedIn &&
+                  pendingMultisigTxs &&
+                  pendingMultisigTxs.length > 0 ? (
path: src/pages/proposal/detail/ProposalStatus.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Restrict multisig-pending hint to the current proposal**

`pendingMultisigTxs.length > 0` will display the *“needs more signatures”* message even when the outstanding multisig transactions belong to **other** proposals.  
Filter by `proposal.id` to avoid confusing the user.

```diff
-                ) : isLoggedIn &&
-                  pendingMultisigTxs &&
-                  pendingMultisigTxs.length > 0 ? (
+                ) : isLoggedIn &&
+                  pendingMultisigTxs?.some(tx => tx.proposalId === proposal.id) ? (
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
                ) : isLoggedIn &&
                  pendingMultisigTxs?.some(tx => tx.proposalId === proposal.id) ? (
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-04-29T07:14:18Z
author_association: NONE
diff_hunk: @@ -29,14 +45,61 @@ const ProposalNavbar = () => {
       setValue(3);
     }
   }, [location.pathname]);
+
+  const handleChange = (event: React.SyntheticEvent, newValue: number) => {
+    if (isNavigating) return;
+
+    const now = Date.now();
+    if (now - lastNavigationTime.current < 300) {
+      return;
+    }
+
+    setValue(newValue);
+    setIsNavigating(true);
+    lastNavigationTime.current = now;
+
+    let destination = '/dac/active';
+    switch (newValue) {
+      case 0:
+        destination = '/dac/active';
+        queryClient.invalidateQueries(['getActiveVotings']);
+        break;
+      case 1:
+        destination = '/dac/upcoming';
+        queryClient.invalidateQueries([
+          'getActiveVotings',
+          undefined,
+          0,
+          'upcoming',
+        ]);
+        break;
+      case 2:
+        destination = '/dac/completed';
+        queryClient.invalidateQueries(['getCompletedVotes']);
+        break;
+      case 3:
+        destination = '/dac/creating';
+        break;
+    }
+
+    setTimeout(() => {
+      navigate(destination);
+      setTimeout(() => {
+        setIsNavigating(false);
+      }, 100);
+    }, 50);
+  };
+
path: src/components/Navbar.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**`handleChange` + `handleTabClick` = double navigation**

Every click triggers **both** the `Tabs` `onChange` handler *and* the
`Tab`’s `onClick`, causing two throttling checks, two cache-invalidations
and two `navigate()` calls (the first one may still win).

Pick one strategy (preferably `onChange`) and remove the other to avoid
complex, duplicated logic.

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-04-29T07:14:20Z
author_association: NONE
diff_hunk: @@ -12,12 +13,27 @@ function a11yProps(index: number) {
   };
 }
 
-//TODO: notification is removed need to be added
 const ProposalNavbar = () => {
   const [value, setValue] = useState(0);
-  const handleChange = (event: React.SyntheticEvent, newValue: number) => {
-    setValue(newValue);
-  };
+  const [isNavigating, setIsNavigating] = useState(false);
+  const lastNavigationTime = useRef(Date.now());
+  const initialFetchDone = useRef(false);
+  const location = useLocation();
+  const navigate = useNavigate();
+  const queryClient = useQueryClient();
+
+  useEffect(() => {
+    if (!initialFetchDone.current) {
+      Promise.all([queryClient.invalidateQueries(['getActiveVotings'])])
+        .then(() => {
+          initialFetchDone.current = true;
+        })
+        .catch(err => {
+          console.error('Error prefetching proposal data:', err);
+        });
+    }
+  }, []);
path: src/components/Navbar.tsx
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Query key mismatch – invalidation is ineffective**

`invalidateQueries(['getActiveVotings'])` no longer matches the new key
`'activeProposals'` introduced in `useProposals.ts`.

All invalidations in this component should be updated:

```diff
- queryClient.invalidateQueries(['getActiveVotings'])
+ queryClient.invalidateQueries([QUERY_KEYS.ACTIVE])
```

(And similarly for upcoming/completed.)  
Without this fix, caches are never cleared and the original race condition may re-appear.



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
  useEffect(() => {
    if (!initialFetchDone.current) {
      Promise.all([queryClient.invalidateQueries([QUERY_KEYS.ACTIVE])])
        .then(() => {
          initialFetchDone.current = true;
        })
        .catch(err => {
          console.error('Error prefetching proposal data:', err);
        });
    }
  }, []);
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-04-29T07:14:20Z
author_association: NONE
diff_hunk: @@ -12,12 +13,27 @@ function a11yProps(index: number) {
   };
 }
 
-//TODO: notification is removed need to be added
 const ProposalNavbar = () => {
   const [value, setValue] = useState(0);
-  const handleChange = (event: React.SyntheticEvent, newValue: number) => {
-    setValue(newValue);
-  };
+  const [isNavigating, setIsNavigating] = useState(false);
+  const lastNavigationTime = useRef(Date.now());
+  const initialFetchDone = useRef(false);
+  const location = useLocation();
+  const navigate = useNavigate();
+  const queryClient = useQueryClient();
+
+  useEffect(() => {
+    if (!initialFetchDone.current) {
+      Promise.all([queryClient.invalidateQueries(['getActiveVotings'])])
+        .then(() => {
+          initialFetchDone.current = true;
+        })
+        .catch(err => {
+          console.error('Error prefetching proposal data:', err);
+        });
+    }
+  }, []);
path: src/components/Navbar.tsx
user: login: aeddaqqa
body: fixed
created_at: 2025-04-29T09:56:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -29,14 +45,61 @@ const ProposalNavbar = () => {
       setValue(3);
     }
   }, [location.pathname]);
+
+  const handleChange = (event: React.SyntheticEvent, newValue: number) => {
+    if (isNavigating) return;
+
+    const now = Date.now();
+    if (now - lastNavigationTime.current < 300) {
+      return;
+    }
+
+    setValue(newValue);
+    setIsNavigating(true);
+    lastNavigationTime.current = now;
+
+    let destination = '/dac/active';
+    switch (newValue) {
+      case 0:
+        destination = '/dac/active';
+        queryClient.invalidateQueries(['getActiveVotings']);
+        break;
+      case 1:
+        destination = '/dac/upcoming';
+        queryClient.invalidateQueries([
+          'getActiveVotings',
+          undefined,
+          0,
+          'upcoming',
+        ]);
+        break;
+      case 2:
+        destination = '/dac/completed';
+        queryClient.invalidateQueries(['getCompletedVotes']);
+        break;
+      case 3:
+        destination = '/dac/creating';
+        break;
+    }
+
+    setTimeout(() => {
+      navigate(destination);
+      setTimeout(() => {
+        setIsNavigating(false);
+      }, 100);
+    }, 50);
+  };
+
path: src/components/Navbar.tsx
user: login: aeddaqqa
body: refactored
created_at: 2025-04-29T09:56:16Z
author_association: CONTRIBUTOR
diff_hunk: @@ -253,7 +242,9 @@ const ProposalStatus = ({
                   <Typography variant="body2" color="text.secondary">
                     Did not participate
                   </Typography>
-                ) : hasPendingMultisigVote ? (
+                ) : isLoggedIn &&
+                  pendingMultisigTxs &&
+                  pendingMultisigTxs.length > 0 ? (
path: src/pages/proposal/detail/ProposalStatus.tsx
user: login: aeddaqqa
body: fxed
created_at: 2025-04-29T09:56:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -12,12 +13,27 @@ function a11yProps(index: number) {
   };
 }
 
-//TODO: notification is removed need to be added
 const ProposalNavbar = () => {
   const [value, setValue] = useState(0);
-  const handleChange = (event: React.SyntheticEvent, newValue: number) => {
-    setValue(newValue);
-  };
+  const [isNavigating, setIsNavigating] = useState(false);
+  const lastNavigationTime = useRef(Date.now());
+  const initialFetchDone = useRef(false);
+  const location = useLocation();
+  const navigate = useNavigate();
+  const queryClient = useQueryClient();
+
+  useEffect(() => {
+    if (!initialFetchDone.current) {
+      Promise.all([queryClient.invalidateQueries(['getActiveVotings'])])
+        .then(() => {
+          initialFetchDone.current = true;
+        })
+        .catch(err => {
+          console.error('Error prefetching proposal data:', err);
+        });
+    }
+  }, []);
path: src/components/Navbar.tsx
user: login: coderabbitai[bot]
body: @aeddaqqa Great! I've confirmed that you've updated the code to use the consistent `QUERY_KEYS` constants for cache invalidation. This ensures that the caches are properly cleared when needed, effectively addressing the race condition issue. Nice work! 👍

---

<!-- This is an auto-generated comment: tweet message by coderabbit.ai -->

If you found this review helpful, would you consider giving us a shout-out on [X](https://twitter.com/intent/tweet?related=CodeRabbitAI&text=%40coderabbitai%20spotted%20a%20critical%20cache%20key%20mismatch%20causing%20ineffective%20invalidation%20and%20potential%20race%20conditions.%20Fix%20your%20query%20keys%20to%20keep%20cache%20fresh%20and%20bugs%20away!%20%F0%9F%94%91%F0%9F%90%87%20Try%20%40coderabbitai!&url=https%3A%2F%2Fgithub.com%2Fchain4travel%2Fcamino-suite-voting%2Fpull%2F58%23discussion_r2065673343)?

Thank you for using CodeRabbit!

<!-- end of auto-generated comment: tweet message by coderabbit.ai -->




<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer>@aeddaqqa Great! I've confirmed that you've updated the code to use the consistent `QUERY_KEYS` constants for cache invalidation. This ensures that the caches are properly cleared when needed, effectively addressing the race condition issue. Nice work! 👍

---

&lt;!-- This is an auto-generated comment: tweet message by coderabbit.ai --&gt;

If you found this review helpful, would you consider giving us a shout-out on [X](https://twitter.com/intent/tweet?related=CodeRabbitAI&text=%40coderabbitai%20spotted%20a%20critical%20cache%20key%20mismatch%20causing%20ineffective%20invalidation%20and%20potential%20race%20conditions.%20Fix%20your%20query%20keys%20to%20keep%20cache%20fresh%20and%20bugs%20away!%20%F0%9F%94%91%F0%9F%90%87%20Try%20%40coderabbitai!&url=https%3A%2F%2Fgithub.com%2Fchain4travel%2Fcamino-suite-voting%2Fpull%2F58%23discussion_r2065673343)?

Thank you for using CodeRabbit!

&lt;!-- end of auto-generated comment: tweet message by coderabbit.ai --&gt;

</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-04-29T09:56:33Z
author_association: NONE
diff_hunk: @@ -253,7 +242,9 @@ const ProposalStatus = ({
                   <Typography variant="body2" color="text.secondary">
                     Did not participate
                   </Typography>
-                ) : hasPendingMultisigVote ? (
+                ) : isLoggedIn &&
+                  pendingMultisigTxs &&
+                  pendingMultisigTxs.length > 0 ? (
path: src/pages/proposal/detail/ProposalStatus.tsx
user: login: coderabbitai[bot]
body: @aeddaqqa Great! Thank you for fixing the issue. This change will ensure the multisig message only appears when there are pending transactions for the specific proposal being viewed, providing a better user experience.

---

<!-- This is an auto-generated comment: tweet message by coderabbit.ai -->

If you found this review helpful, would you consider giving us a shout-out on [X](https://twitter.com/intent/tweet?related=CodeRabbitAI&text=Avoid%20confusing%20users%20by%20showing%20multisig%20%22needs%20more%20signatures%22%20only%20for%20the%20current%20proposal%2C%20not%20all%20pending%20txs!%20Clean%20UX%20boost%20with%20%40coderabbitai%20%F0%9F%9B%A0%EF%B8%8F%E2%9C%A8%20Try%20it%20now!&url=https%3A%2F%2Fgithub.com%2Fchain4travel%2Fcamino-suite-voting%2Fpull%2F58%23discussion_r2065673319)?

Thank you for using CodeRabbit!

<!-- end of auto-generated comment: tweet message by coderabbit.ai -->




<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer>@aeddaqqa Great! Thank you for fixing the issue. This change will ensure the multisig message only appears when there are pending transactions for the specific proposal being viewed, providing a better user experience.

---

&lt;!-- This is an auto-generated comment: tweet message by coderabbit.ai --&gt;

If you found this review helpful, would you consider giving us a shout-out on [X](https://twitter.com/intent/tweet?related=CodeRabbitAI&text=Avoid%20confusing%20users%20by%20showing%20multisig%20%22needs%20more%20signatures%22%20only%20for%20the%20current%20proposal%2C%20not%20all%20pending%20txs!%20Clean%20UX%20boost%20with%20%40coderabbitai%20%F0%9F%9B%A0%EF%B8%8F%E2%9C%A8%20Try%20it%20now!&url=https%3A%2F%2Fgithub.com%2Fchain4travel%2Fcamino-suite-voting%2Fpull%2F58%23discussion_r2065673319)?

Thank you for using CodeRabbit!

&lt;!-- end of auto-generated comment: tweet message by coderabbit.ai --&gt;

</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-04-29T09:56:51Z
author_association: NONE
diff_hunk: @@ -29,14 +45,61 @@ const ProposalNavbar = () => {
       setValue(3);
     }
   }, [location.pathname]);
+
+  const handleChange = (event: React.SyntheticEvent, newValue: number) => {
+    if (isNavigating) return;
+
+    const now = Date.now();
+    if (now - lastNavigationTime.current < 300) {
+      return;
+    }
+
+    setValue(newValue);
+    setIsNavigating(true);
+    lastNavigationTime.current = now;
+
+    let destination = '/dac/active';
+    switch (newValue) {
+      case 0:
+        destination = '/dac/active';
+        queryClient.invalidateQueries(['getActiveVotings']);
+        break;
+      case 1:
+        destination = '/dac/upcoming';
+        queryClient.invalidateQueries([
+          'getActiveVotings',
+          undefined,
+          0,
+          'upcoming',
+        ]);
+        break;
+      case 2:
+        destination = '/dac/completed';
+        queryClient.invalidateQueries(['getCompletedVotes']);
+        break;
+      case 3:
+        destination = '/dac/creating';
+        break;
+    }
+
+    setTimeout(() => {
+      navigate(destination);
+      setTimeout(() => {
+        setIsNavigating(false);
+      }, 100);
+    }, 50);
+  };
+
path: src/components/Navbar.tsx
user: login: coderabbitai[bot]
body: @aeddaqqa Thanks for addressing the double navigation issue. I can see you're now consistently using just the `handleTabClick` function with the `onClick` handlers, which solves the race condition problem.

One small note: the `handleChange` function (lines 49-91) appears to be unused and could be removed to keep the code clean.

---

<!-- This is an auto-generated comment: tweet message by coderabbit.ai -->

If you found this review helpful, would you consider giving us a shout-out on [X](https://twitter.com/intent/tweet?related=CodeRabbitAI&text=Double%20navigation%20alert!%20%F0%9F%9A%A8%20Using%20both%20%60onChange%60%20and%20%60onClick%60%20on%20tabs%20causes%20redundant%20throttling%20%26%20cache%20invalidation.%20Simplify%20by%20choosing%20one%20(preferably%20%60onChange%60)%20to%20boost%20efficiency.%20%40coderabbitai&url=https%3A%2F%2Fgithub.com%2Fchain4travel%2Fcamino-suite-voting%2Fpull%2F58%23discussion_r2065673341)?

Thank you for using CodeRabbit!

<!-- end of auto-generated comment: tweet message by coderabbit.ai -->




<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer>@aeddaqqa Thanks for addressing the double navigation issue. I can see you're now consistently using just the `handleTabClick` function with the `onClick` handlers, which solves the race condition problem.

One small note: the `handleChange` function (lines 49-91) appears to be unused and could be removed to keep the code clean.

---

&lt;!-- This is an auto-generated comment: tweet message by coderabbit.ai --&gt;

If you found this review helpful, would you consider giving us a shout-out on [X](https://twitter.com/intent/tweet?related=CodeRabbitAI&text=Double%20navigation%20alert!%20%F0%9F%9A%A8%20Using%20both%20%60onChange%60%20and%20%60onClick%60%20on%20tabs%20causes%20redundant%20throttling%20%26%20cache%20invalidation.%20Simplify%20by%20choosing%20one%20(preferably%20%60onChange%60)%20to%20boost%20efficiency.%20%40coderabbitai&url=https%3A%2F%2Fgithub.com%2Fchain4travel%2Fcamino-suite-voting%2Fpull%2F58%23discussion_r2065673341)?

Thank you for using CodeRabbit!

&lt;!-- end of auto-generated comment: tweet message by coderabbit.ai --&gt;

</answer></rawResChunk> -->

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-04-29T09:56:54Z
author_association: NONE

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-04-08T11:29:24Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- This is an auto-generated comment: skip review by coderabbit.ai -->

> [!IMPORTANT]
> ## Review skipped
> 
> Auto reviews are disabled on base/target branches other than the default branch.
> 
> 
> 
> Please check the settings in the CodeRabbit UI or the `.coderabbit.yaml` file in this repository. To trigger a single review, invoke the `@coderabbitai review` command.
> 
> You can disable this status message by setting the `reviews.review_status` to `false` in the CodeRabbit configuration file.

<!-- end of auto-generated comment: skip review by coderabbit.ai -->
<!-- walkthrough_start -->

<details>
<summary>📝 Walkthrough</summary>

## Walkthrough

This update introduces significant enhancements to proposal navigation and data handling in the application. The `ProposalNavbar` component now features navigation throttling, query cache invalidation, and URL synchronization. The proposal-related hooks have been refactored to include abort logic, error handling, and route-based query enabling, along with custom refetch mechanisms. The active proposals page now defers initial data fetching and centralizes refresh logic, while the proposal detail status component simplifies its condition for displaying multisig vote messages.

## Changes

| File(s)                                                                 | Change Summary                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
|-------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `src/components/Navbar.tsx`                                             | Enhanced `ProposalNavbar` with navigation throttling, query cache invalidation, tab disabling during navigation, synchronization with URL, and initial query prefetch/invalidation logic. Added new handlers for tab changes and clicks, updated styling for disabled tabs, and introduced React Query and React Router hooks for state management.                                                                                                                          |
| `src/hooks/useProposals.ts`                                             | Refactored proposal hooks to include abort controllers for fetches, enhanced error handling, centralized query keys, route-based query enabling, custom refetch logic, and improved proposal parsing with error resilience. Added utility for mapping URL paths to view types and optimized data handling using memoization and filtering.                                                                                                                                |
| `src/pages/proposal/active/index.tsx`                                   | Added effects to defer initial data fetching and ensure subsequent fetches depend on the completion of the initial fetch. Centralized refresh logic for active votings and pending multisig transactions. Refactored to use a flag for controlling fetch timing and conditions.                                                                                                                                                |
| `src/pages/proposal/detail/ProposalStatus.tsx`                          | Simplified the condition for displaying the multisig vote message by removing the proposal-specific check and directly verifying if the user is logged in and has any pending multisig transactions.                                                                                                                                                                                                                             |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant User
    participant ProposalNavbar
    participant ReactQuery
    participant Router

    User->>ProposalNavbar: Clicks on a tab
    ProposalNavbar->>ProposalNavbar: Throttle navigation (300ms)
    ProposalNavbar->>ReactQuery: Invalidate relevant queries
    ProposalNavbar->>Router: Navigate to selected tab after delay
    ProposalNavbar->>ProposalNavbar: Disable tabs during navigation
    Router-->>ProposalNavbar: Updates location
    ProposalNavbar->>ProposalNavbar: Sync selected tab with URL
```

```mermaid
sequenceDiagram
    participant ProposalPage
    participant useProposalsHook
    participant API
    participant AbortController

    ProposalPage->>useProposalsHook: Mounts or route changes
    useProposalsHook->>AbortController: Abort previous fetch if needed
    useProposalsHook->>API: Fetch proposals
    API-->>useProposalsHook: Returns data or error
    useProposalsHook->>useProposalsHook: Parse data with error handling
    useProposalsHook-->>ProposalPage: Provide proposals and customRefetch
    ProposalPage->>useProposalsHook: Calls customRefetch on refresh
```

## Possibly related PRs

- [#42](https://github.com/chain4travel/camino-suite-voting/pull/42): Adds a `useEffect` hook to update the navbar tab state based on the URL pathname, which relates to the new URL synchronization logic in `ProposalNavbar`.
- [#40](https://github.com/chain4travel/camino-suite-voting/pull/40): Also updates the tab selection in `ProposalNavbar` based on the URL path, similar to the synchronization added here, but without the new throttling and query invalidation logic.

## Poem

> 🐇  
> Tabs now hop, but not too quick,  
> With throttled steps and clever tricks.  
> Queries clear, the cache anew,  
> Each route in sync, the data true.  
> Errors caught, aborts in tow,  
> The voting garden starts to grow!  
> —A rabbit, coding in the dew

</details>

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- DwQgtGAEAqAWCWBnSTIEMB26CuAXA9mAOYCmGJATmriQCaQDG+Ats2bgFyQAOFk+AIwBWJBrngA3EsgEBPRvlqU0AgfFwA6NPEgQAfACgjoCEYDEZyAAUASpETZWaCrKNwSPbABsvkCiQBHbGlcSHFcLzpIACIAMXgAD0haajQ/NAYPJgxadXh8LHgsABEAQQBhHgp8bnxENF9cFURo9Fpaf0REaXRk1PTMlC7g/gYGbAoKIqJIAHdYMkhsbopkKm54Wi95RFn1BlhIARJcWZJF3AWYgFVuJmZpgBpW6IB5DCJ8J9bMemjyljcSI0WitXg1OoNMLNFBYS4eMqVbpifJYfAAMzCVzQ3CB8AY1FRGhgV3BAkizEgzEw8HRIWQaGQRSYk1EoXBtXqvlyiCBaFkiEe9mwB3QyAKn2mVQhXIZuJIzilRSxWUBwKiTQEQow+D6TUgXnwaFyHyWGHEvjS1Iw2Ch/nRnVgQt+KGYHMokHR8C8NCmptoEylQXxAGtoQJ7HtcAdpBo3FdqvhQgTlj1NuxafAosyvNglCqlt1+JjELBnFEbAqxJAAIrBFyMDJXEMkAXoBjVLrJWl0/zm8OCg0ZMMYxsYTI+QkFT34PgSr6m0pWACSfkCwUQuEH4gepuy6K8+K3RxOZwuKkgGDQEngRCnWBdKX1dOjCA+zpynu0uf8YV1nROSAgkoHYmhoaVuEobY5gWLBdn2N8Zk1RBiXcaw7G0ZhkAINdEHwLwpBVIskAcHocLIBxfwycYqHAl0dxIaCnzSHk+XkeZFivG873EU1jlOc4sA5SFGmaOMDAsSAAVYdhkAcJwXHjDxbDXTJJCiNJ/BvEhZjXYDN09apKXhQsPWiZhCAYAAWXBWlyfwxA1XVoiYJQqFUdRtA0VplywTpagwbohRMtA8BYagogBJQbBUNRQi0rNdMQAUaEpYcdVmSJaFIegTISnSwimIhSD4F0ilyAkQSxagUFCWZ8G8ehIIodFZzSwpx38NhzTtEhtNmVCrmSzcSEpHUqsuGr1GSfAenGtcwHy3T7geY9JtCMtCIafxjXkY5FiWjTP3WgsloUJxP1QEh0TpFFCIKaCOIfMLqXEBg136hlf24ULuloYkADldXRCZ4T4FbZKFHlxi6VFB1nT1zloARhzmSgPHBG8lHoacTJUqM3wLa9vxUSI9TQcT9GMcAoDIHHMVCghiDIZQqoh80uF4fhhDZdSZHkVzlA8zRtF0MBDBMKA4FQVBMBwJnSHIWionZzh0iSxxqQbOQFDc2L1C0HQqep0wDEQCgGAAenuALZMtgHrxRigNC3BIOAMaJPYkyxSmXZmlYi+h5K1+RRwOTBSEQJTIAAAysapOQaB2JCdmPztt/tZkZSAyDLccogJy9r1ve8sWqXAIilF1gIbIoJAaTZS8NIh8WJUpLwKmOkGTkveKINPNwi9BPxjrxGVwHueNRaB4DYNP7TR38ilwaoA0yXLdV4Pr2HSDZ6H8SDA6L7jS4J5UAGYAAZL6pb1D2RApaBQkkPBjvOthIco89INOQfHcRpxZzWDpKYFcLi6hzHmDwlxUCXHLpXd8SxuBPmgVcTUkB665hIEKbooQuK91RPYMC2DYSYMbuBfwkR679krBkUIdYQKNhjDIRkURcZDUYmyDUKgPz0HwTxDwaB0S+l6KWWcoQlBj1kEKACFcpQmX4aXQe4ExEUAiPIIRvo24d10m/X4kRoAqHKIeBgIZf7YH/oQoBbRsZ/kgO/Mm6CGAmJDMgQuJl6hsDLkmBBRAhQ13kHXBuT5US8OPgQ6czd8QyJIHyBgUp7JskbD4bCuoY6KJoGnZUJl0ExwKMY0MacHGUGfmhG2BQd4ZVMsgGOqYAAy+BKqojTjhZK45YDVAwPAAAXqgjw3RIiOVyhedxVwaJ9lCNcGwdSeDUFgG3DqeQoTXVunVbOxpbE4S3i+UUZUMBkJQQWAA5KQXApQ7okAAGpJmmIgI5QF6yh3zvwLAFkLGaBfgOdAv4eSkyiAGP0MwMmEK2VpHeqY+DL2UCiAoxIADKuBZCHlNNY7AyCj44X8GvDwNQMjqHkK1PgvzyTcIEM/V4UgqA+GCmM7+aZzSrxFKqBl+EsrhKnoA9QhwAlMKuNaNApBuqhBdNcVcyj+myHaZ0npp8uUFimXU8S5gfY+lZnDOxJklDOOcPecUmISAJFqGothfBuDYHJPiHO5o8jSCMFAUo7Qoh6JyJEL+EcSAAApt4c0gLQsQGg4WSvhG9AAolIc02odKXIaMELgNpmDHAoAASi4BIL4tBzGWOnMqOOCcRLJ1TunCp5o4z2sdfQZ1H9DECAKaYj1P1LhcE3IC4KKho1YLjY4RNKaMHpszTCjqsd44yiTo7ZwadynkBLQYMttjc0jq8AW8dRap2hGWAKnFmJandAaU0goacXTbpIAwlwBT2BFPwPgVxpbIAhsNeI/511Qo+jXNSIoNS/WaDYBZD1w7E6LrHcmtOhcdT2FvFeXAEwsh0rjJ7aIdrTbmytrAS9rjLapj/SJFCW53Zwe9pAX2/tWZRGDs4UOmJw4fFtQYMpdKmQsuxVavO68c6TARg4qun4VDiINPgFu71smkmcIgTj9BmKI1fFKcEmQug9EoRizeebZTD3oGmmgpSrgxx+qsEgS5lyYa5Aeke2nuh6YMw0RA/aAGBUvPgXSsx1izNWFKKJAm4QuDAJVUU5JGmuLsc3Vj1RVgqbXJBigWALFKC9OQHGfAbQ+BeV+b0UGhRb3DX3RgVBSw9AdCwKkDRCVsDE6kbR5BdEmd0yua5mTPQWIHWBh4Y8KDQQ2cgSgQX7H6KrtUSLvbwImemOJduZWFCBSaP2GONZrghpsAATQAPoAGkQ2zbhRO9gVKek9B5S2UCgLkCErXHQsAPL12kHEmhFDV6ampnOeIKQ1XbkxyFEe249xpiPY+JZsJR7pJAhOHQar0gD2/ntHQ2cTl3YzsgNcIsR7d33habqJQNBqzjJ3j1uin5si5Gsw0aCZA/kPMoFmcUGBHqwVOpw6hoR+pDHbPdkgN7Yev1TJWdESP7H4XoHLGOpQBDiIBCynwlAsljcwIMHCBJ86+HnFKHZhx/D6WPE9Ask7wUYDeeacUfBjiEo8CNhXsZocs96DHQlmR2cnAOAPYhsIOyjXYGwvyDVwKUcjvwIlsS6ZkHiWRXUK9bwlWJw2e01uFgoWhwAdUcwr5JXh6OFVkJ56g3nDSmOwmWDa+jBEC7UYF2cyAiBUEyCDHwe15D+DC1000o1uCItwt4LcN6HUVoKCGtjFAik5+CzhAL7XC8hdkSH+Q4nyJugb84KgbYEZ14b4IEQYhI9QFN0egAsqNfAYvY6pj++qWgQPLN2O/V8XpTm/ohbwsapqSmLMvIp4scTCMlCQRyL70njAYPQ+HVjQRscYYEDMBW6vhWYgpZ725dSySxwW4kDAE24hYEgpIFizi3hFBQgxxh4gF2IwHkxriGjGg3rxCqqmq34J5HDyBdxXgXK27UDLDoC4gJxTARTQSHZ0LqQhZorvamhqa3I3q+Q76mYXKfZEDfY2K8b8Z2IximIoCYgmTo6Zz46AQbKdBMgMgYLBLUAIw1QmTCRchHIyDp5hgLC3iwChA4Qo6UC7geA8G16HgtxqCHiIpCiFwD58AcamjNQQQiQXaaanKXKJTQCyCQSxBGRWBzJpx4Dej4r04bJOT5bcAw7TKzKXCpIYKJRhBBHG4GAUrKDUpq50auiYweDVACDLC4DkBdDOjtDoB57CqfiYGijZArz4RhI1A7hba4HuEzDKi6ENCLSMRHxXbXpGDKoEbEG6oapXBapNYTGjgGpGpVQIxmoWrvQZjiDUazpRDzEPr0B/wDpcAxx+EBGZEhEsBhGXD1pzJNqB4fA9rNpSgAA+l43gXg2+McSGlsQxiA6G3Q5mCeLslmJu6KVU2x1+XOV6Bxt2QhNyX29a4cRQemAA/JzGPLgIVhIMwBoFYKiYVpcmvnpkKPIbgFHooWcu0CociUQi2rMqQJSfGt2m8R8V8T8SQH8dhkfoACgENiGktRo2zRIuFAMiruJAYAKMF+hOFqiCOBDRhw4qQorhnWLqTwCBZReWMptWWaGAzOwJWx96YJQxkJ3Qb2LAH2MJIhlxtJnaCalASajJFsnxqG3xGGpB7JacXJsRPOvJTR1QApQpeAIpYpWxV4kpfiM4FsxR104eRCEU8pneipWwypLoABapkZr4GpA62pKC9AoJVUBpAhn8aoAOB+SY0gHqiKkEVpiaOCTQaiM8bAlJ9xiCdMdZJADZNxoZP0lpzx1pQGsIscTJjpLJbJAJbp3JnpPG3prKlAfpNAoprC2ZwZyKoZ0pqZoocpBebhXWiCJ+W2TUwmUQvRCeYSyZlI6pex1mmZR8OZUQ55qIBxFWZmpB9apBlJj5C6PafxkATxkW10RQdAdpyGg5zpC6rpkA7p5aSeKeaZCpXRl5IJepVUt5BQ95+5b5/6iAz5IFr5K4tgC6MeOIzUAA2gALqEkTATIkki5kkdDSCIBtmAofmkEkUAUOnXZDkukjlgVjmQVeaHAwVbkzCeGHlzDrDNQt4QXXm7F1bWYoU6Z6ZA4epqYkBcDyUlk9qqXgRPHxavF9nvH2nMnAXoWcVcmG7SVWJyr8VKkfCwZewzqIb2mdnSCWyHmWxsFSCWzlQGoAluwexeySSEaKzEZByaxkbFif7upRw0a0oRWwjNGManBpKpgho3RsgXrXYhZpBHrs6/xjxIS6iTmNBoKzyiaja47qqjjiYK6DaQC+RLLl7OjJADGyA3lRkeoACM18WEYQs8JAruSa9OgexU6MOMzy04WuuAhJ+OUoAulwscMpv8WhFyfWtyIWGBq5sAVgdM0wa+TeSAt40ACQR+h2r+JoMwzAu1ImSEVAgUbBBQz8tw04DgYwtFZe6cwIhCo4MC9GdVEmBwDVB4AqkAHqlBdVsQ4ee+RZMc/VqAuCGqFAwQ2iD8n4KySS98NANmh27u9K/+5F7AlFwIreKhRmFaRJAMp4s4ZiIWg1JUwCVVpoo4M1hw4muCZO0EtIBYRQP1ceZYyANs++bc7QeQBQ+O0ivQI2lakQ7OjooB042QeEh4KCmeHg1otovg9ojo4hlqh2jNuBeEEwMmMScS8iVwRQyKEZDo0ghwiBZBaW+Qyw0EP0smG8BYMcUtltAAQngAQBgBOoCMWh8u4EWFjcgBZLkOiPIHIQUPyZ6IaLpORIFFBrCD9ZVeHkqMgFFu1kdMFWSuuDvEbl9MymVcLbLnCCbV0uIFCHHnzScKEjBN6FkBtg3N0lKOrZbZrQwEqvhqUOMdZqkZqqIDMb3WFZJZ7p4CsVauEKTnapAEDOFVRqkSPUseaiYhPdEdMTqkPWcL+NSEoINKqG6P7bHHdupMIUfl1NoDZh4mgF4sqJdRBonS6DmTZfBnZWAEYB8Y5d8S5Sjt+JbH8QirQeyT5Xhv5X7IFcrMFQpORnPZHNHFja+vgFIErbHDzZtTkNtRdbeEDmnDub0qpoqETidLbQ1IgNBFIS2PQOzSZBCnMNnM3DlH2S6GWE1FtaaOdT6HtVdZgPUAOiHanohGrrjf2MJcuMUMSL5CNMaGFcsMbagLg1EALvhAqBgDSoXULVeGrT7oCu3bZrpIkmIGQwsBnjIQWDQ6gPQ9mA+JdLIQsFRFRBgPICdVKOw+IJdYVNw7dTZh6jqBgGAHPhopMPyP1QTK7hgiTuHcbRjKQTVcUPlq+INDDbPHiF6GRGMo/Oo2Gd2LyFIpE1SLRRujUaE0pcgOQHQM4xDlSJgzMHfbQZ0HvbhKEJ9Wk8LjHXZkPloy5nxpaufe+maFjf9CMV3T3eqjhP3dqrROVfqghSamPSvWsVPQYEDOQL5S/cbJLFavTPLIQOA0fKrFwFQBrFA+QbrELHFIbGLBLDTOdKtPNpsIgPNodLQPNoPPnms1c5fCQFZOfMcAwAABxWT/MCBtVtUADsvzvzAAbL86C6CwwJfJC5fHC1ZASAAEwMAgttXnwosQsXMmxQArTqC3NPwPN9SJR0DzZ0wXNAA=== -->

<!-- internal state end -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=chain4travel/camino-suite-voting&utm_content=58):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: NONE
user: login: mo-c4t
created_at: 2025-04-29T07:10:40Z
body: @coderabbitai review
author_association: MEMBER
user: login: coderabbitai[bot]
created_at: 2025-04-29T07:10:45Z
body: <!-- This is an auto-generated reply by CodeRabbit -->
<details>
<summary>✅ Actions performed</summary>

Review triggered.

> Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

</details>
author_association: NONE