This commit is contained in:
IGNY8 VPS (Salman)
2025-12-08 14:57:36 +00:00
parent 144e955b92
commit c09c6cf7eb
5 changed files with 201 additions and 86 deletions

View File

@@ -3,10 +3,12 @@
* Used across all Planner and Writer module pages
* Includes: Page title, last updated, site/sector info, and selectors
*/
import React, { ReactNode } from 'react';
import React, { ReactNode, useEffect, useRef } from 'react';
import { useSiteStore } from '../../store/siteStore';
import { useSectorStore } from '../../store/sectorStore';
import SiteAndSectorSelector from './SiteAndSectorSelector';
import { trackLoading } from './LoadingStateMonitor';
import { useErrorHandler } from '../../hooks/useErrorHandler';
interface PageHeaderProps {
title: string;
@@ -33,7 +35,58 @@ export default function PageHeader({
navigation,
}: PageHeaderProps) {
const { activeSite } = useSiteStore();
const { activeSector } = useSectorStore();
const { activeSector, loadSectorsForSite } = useSectorStore();
const { addError } = useErrorHandler('PageHeader');
const lastSiteId = useRef<number | null>(null);
const isLoadingSector = useRef(false);
// Load sectors when active site changes - only for pages that need site/sector context
useEffect(() => {
// Skip sector loading for pages that hide site/sector selector (account/billing pages)
if (hideSiteSector) return;
const currentSiteId = activeSite?.id ?? null;
// Only load if:
// 1. We have a site ID
// 2. The site is active (inactive sites can't have accessible sectors)
// 3. It's different from the last one we loaded
// 4. We're not already loading
if (currentSiteId && activeSite?.is_active && currentSiteId !== lastSiteId.current && !isLoadingSector.current) {
lastSiteId.current = currentSiteId;
isLoadingSector.current = true;
trackLoading('sector-loading', true);
// Add timeout to prevent infinite loading
const timeoutId = setTimeout(() => {
if (isLoadingSector.current) {
console.error('PageHeader: Sector loading timeout after 35 seconds');
trackLoading('sector-loading', false);
isLoadingSector.current = false;
addError(new Error('Sector loading timeout - check network connection'), 'PageHeader.loadSectorsForSite');
}
}, 35000);
loadSectorsForSite(currentSiteId)
.catch((error) => {
// Don't log 403/404 errors as they're expected for inactive sites
if (error.status !== 403 && error.status !== 404) {
console.error('PageHeader: Error loading sectors:', error);
addError(error, 'PageHeader.loadSectorsForSite');
}
})
.finally(() => {
clearTimeout(timeoutId);
trackLoading('sector-loading', false);
isLoadingSector.current = false;
});
} else if (currentSiteId && !activeSite?.is_active) {
// Site is inactive - clear sectors and reset lastSiteId
lastSiteId.current = null;
const { useSectorStore } = require('../../store/sectorStore');
useSectorStore.getState().clearActiveSector();
}
}, [activeSite?.id, activeSite?.is_active, hideSiteSector, loadSectorsForSite, addError]);
const badgeColors = {
blue: 'bg-blue-600 dark:bg-blue-500',

View File

@@ -5,7 +5,6 @@ import AppHeader from "./AppHeader";
import Backdrop from "./Backdrop";
import AppSidebar from "./AppSidebar";
import { useSiteStore } from "../store/siteStore";
import { useSectorStore } from "../store/sectorStore";
import { useAuthStore } from "../store/authStore";
import { useBillingStore } from "../store/billingStore";
import { useHeaderMetrics } from "../context/HeaderMetricsContext";
@@ -16,15 +15,12 @@ import ResourceDebugOverlay from "../components/debug/ResourceDebugOverlay";
const LayoutContent: React.FC = () => {
const { isExpanded, isHovered, isMobileOpen } = useSidebar();
const { loadActiveSite, activeSite } = useSiteStore();
const { loadSectorsForSite } = useSectorStore();
const { refreshUser, isAuthenticated } = useAuthStore();
const { balance, loadBalance } = useBillingStore();
const { setMetrics } = useHeaderMetrics();
const { addError } = useErrorHandler('AppLayout');
const hasLoadedSite = useRef(false);
const lastSiteId = useRef<number | null>(null);
const isLoadingSite = useRef(false);
const isLoadingSector = useRef(false);
const [debugEnabled, setDebugEnabled] = useState(false);
const lastUserRefresh = useRef<number>(0);
@@ -86,51 +82,10 @@ const LayoutContent: React.FC = () => {
checkTokenAndLoad();
}, [isAuthenticated]); // Run when authentication state changes
// Load sectors when active site changes (by ID, not object reference)
useEffect(() => {
const currentSiteId = activeSite?.id ?? null;
// Only load if:
// 1. We have a site ID
// 2. The site is active (inactive sites can't have accessible sectors)
// 3. It's different from the last one we loaded
// 4. We're not already loading
if (currentSiteId && activeSite?.is_active && currentSiteId !== lastSiteId.current && !isLoadingSector.current) {
lastSiteId.current = currentSiteId;
isLoadingSector.current = true;
trackLoading('sector-loading', true);
// Add timeout to prevent infinite loading
// Match API timeout (30s) + buffer for network delays
const timeoutId = setTimeout(() => {
if (isLoadingSector.current) {
console.error('AppLayout: Sector loading timeout after 35 seconds');
trackLoading('sector-loading', false);
isLoadingSector.current = false;
addError(new Error('Sector loading timeout - check network connection'), 'AppLayout.loadSectorsForSite');
}
}, 35000); // 35 seconds to match API timeout (30s) + buffer
loadSectorsForSite(currentSiteId)
.catch((error) => {
// Don't log 403/404 errors as they're expected for inactive sites
if (error.status !== 403 && error.status !== 404) {
console.error('AppLayout: Error loading sectors:', error);
addError(error, 'AppLayout.loadSectorsForSite');
}
})
.finally(() => {
clearTimeout(timeoutId);
trackLoading('sector-loading', false);
isLoadingSector.current = false;
});
} else if (currentSiteId && !activeSite?.is_active) {
// Site is inactive - clear sectors and reset lastSiteId
lastSiteId.current = null;
const { useSectorStore } = require('../store/sectorStore');
useSectorStore.getState().clearActiveSector();
}
}, [activeSite?.id, activeSite?.is_active]); // Depend on both ID and is_active
// Sector loading moved to PageHeader component
// This ensures sectors are only loaded when content pages (Planner/Writer/Optimizer) mount
// Account/billing pages don't use PageHeader with site/sector selector, so they won't trigger sector loading
// This prevents unnecessary 404 errors on /account/plans and similar routes
// Refresh user data on mount and when app version changes (after code updates)
// This ensures changes are reflected immediately without requiring re-login

View File

@@ -624,6 +624,7 @@ export default function PlansAndBillingPage() {
</div>
</div>
</div>
</div>
)}
{/* Billing History Tab */}
@@ -810,4 +811,4 @@ export default function PlansAndBillingPage() {
</div>
</div>
);
}
}

View File

@@ -154,8 +154,56 @@ export async function fetchAPI(endpoint: string, options?: RequestInit & { timeo
// Read response body once (can only be consumed once)
const text = await response.text();
// Handle 402/403 for plan/limits gracefully by tagging error
if (response.status === 402 || response.status === 403) {
// Handle 403 Forbidden - check for authentication errors FIRST before throwing
if (response.status === 403) {
try {
const errorData = text ? JSON.parse(text) : null;
const errorMessage = errorData?.detail || errorData?.message || errorData?.error || response.statusText;
// Check if it's an authentication credentials error
if (errorMessage?.includes?.('Authentication credentials') ||
errorMessage?.includes?.('not authenticated') ||
errorMessage?.includes?.('Invalid token') ||
errorMessage?.includes?.('Token has expired')) {
// CRITICAL: Authentication token is invalid/missing - force logout
const authState = useAuthStore.getState();
if (authState?.isAuthenticated || authState?.token) {
console.warn('Authentication token invalid - forcing logout');
const { logout } = useAuthStore.getState();
logout();
// Redirect to login page
if (typeof window !== 'undefined') {
window.location.href = '/signin';
}
}
// Throw authentication error
let err: any = new Error(errorMessage);
err.status = 403;
err.data = errorData;
throw err;
}
// Not an auth error - could be permissions/plan issue
let err: any = new Error(errorMessage);
err.status = 403;
err.data = errorData;
throw err;
} catch (e: any) {
// If it's the error we just threw, re-throw it
if (e.status === 403) throw e;
// Parsing failed - throw generic 403 error
let err: any = new Error(text || response.statusText);
err.status = 403;
throw err;
}
}
// Handle 402 Payment Required - plan/limits issue
if (response.status === 402) {
let err: any = new Error(response.statusText);
err.status = response.status;
try {
@@ -168,29 +216,6 @@ export async function fetchAPI(endpoint: string, options?: RequestInit & { timeo
throw err;
}
// Handle 403 Forbidden with authentication error - clear invalid tokens
if (response.status === 403) {
try {
const errorData = text ? JSON.parse(text) : null;
// Check if it's an authentication credentials error
if (errorData?.detail?.includes('Authentication credentials') ||
errorData?.message?.includes('Authentication credentials') ||
errorData?.error?.includes('Authentication credentials')) {
// Only logout if we actually have a token stored (means it's invalid)
// If no token, it might be a race condition after login - don't logout
const authState = useAuthStore.getState();
if (authState?.token || authState?.isAuthenticated) {
// Token exists but is invalid - clear auth state and force re-login
const { logout } = useAuthStore.getState();
logout();
}
// Don't throw here - let the error handling below show the error
}
} catch (e) {
// If parsing fails, continue with normal error handling
}
}
// Handle 401 Unauthorized - try to refresh token
if (response.status === 401) {
const refreshToken = getRefreshToken();

View File

@@ -92,13 +92,35 @@ export const useAuthStore = create<AuthState>()(
throw createAuthError('Active subscription required. Visit igny8.com/pricing to subscribe.', 'PLAN_REQUIRED');
}
const newToken = responseData.access || tokens.access || data.access || null;
const newRefreshToken = responseData.refresh || tokens.refresh || data.refresh || null;
// CRITICAL: Set auth state AND immediately persist to localStorage
// This prevents race conditions where API calls happen before persist middleware writes
set({
user: userData,
token: responseData.access || tokens.access || data.access || null,
refreshToken: responseData.refresh || tokens.refresh || data.refresh || null,
token: newToken,
refreshToken: newRefreshToken,
isAuthenticated: true,
loading: false
});
// Force immediate persist to localStorage (don't wait for Zustand middleware)
try {
const authState = {
state: {
user: userData,
token: newToken,
refreshToken: newRefreshToken,
isAuthenticated: true,
loading: false
},
version: 0
};
localStorage.setItem('auth-storage', JSON.stringify(authState));
} catch (e) {
console.warn('Failed to persist auth state to localStorage:', e);
}
} catch (error: any) {
// ALWAYS reset loading on error - critical to prevent stuck state
set({ loading: false });
@@ -125,12 +147,49 @@ export const useAuthStore = create<AuthState>()(
document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/;domain=" + window.location.hostname;
document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT;path=/;domain=." + window.location.hostname;
}
// Clear all localStorage to prevent state contamination
localStorage.clear();
// Clear sessionStorage as well
// IMPORTANT: Selectively clear auth-related localStorage items
// DO NOT use localStorage.clear() as it breaks Zustand persist middleware
const authKeys = ['auth-storage', 'auth-store', 'site-storage', 'sector-storage', 'billing-storage'];
authKeys.forEach(key => {
try {
localStorage.removeItem(key);
} catch (e) {
console.warn(`Failed to remove ${key}:`, e);
}
});
// Clear sessionStorage
sessionStorage.clear();
// Reset auth state
set({ user: null, token: null, refreshToken: null, isAuthenticated: false, loading: false });
// Reset auth state to initial values
set({
user: null,
token: null,
refreshToken: null,
isAuthenticated: false,
loading: false
});
// Reset other stores that depend on auth
try {
// Dynamically import and reset site store
import('./siteStore').then(({ useSiteStore }) => {
useSiteStore.setState({ activeSite: null, loading: false, error: null });
});
// Dynamically import and reset sector store
import('./sectorStore').then(({ useSectorStore }) => {
useSectorStore.setState({ activeSector: null, sectors: [], loading: false, error: null });
});
// Dynamically import and reset billing store
import('./billingStore').then(({ useBillingStore }) => {
useBillingStore.setState({ balance: null, loading: false, error: null });
});
} catch (e) {
console.warn('Failed to reset stores on logout:', e);
}
},
register: async (registerData) => {
@@ -166,13 +225,35 @@ export const useAuthStore = create<AuthState>()(
const tokens = responseData.tokens || {};
const userData = responseData.user || data.user;
const newToken = tokens.access || responseData.access || data.access || null;
const newRefreshToken = tokens.refresh || responseData.refresh || data.refresh || null;
// CRITICAL: Set auth state AND immediately persist to localStorage
set({
user: userData,
token: tokens.access || responseData.access || data.access || null,
refreshToken: tokens.refresh || responseData.refresh || data.refresh || null,
token: newToken,
refreshToken: newRefreshToken,
isAuthenticated: true,
loading: false
});
// Force immediate persist to localStorage (don't wait for Zustand middleware)
try {
const authState = {
state: {
user: userData,
token: newToken,
refreshToken: newRefreshToken,
isAuthenticated: true,
loading: false
},
version: 0
};
localStorage.setItem('auth-storage', JSON.stringify(authState));
} catch (e) {
console.warn('Failed to persist auth state to localStorage:', e);
}
return userData;
} catch (error: any) {
// ALWAYS reset loading on error - critical to prevent stuck state