Fix childer tennats

This commit is contained in:
Urtzi Alfaro
2025-12-29 17:25:20 +01:00
parent adef7971a0
commit c1dedfa44f
5 changed files with 108 additions and 57 deletions

View File

@@ -23,7 +23,12 @@ export const useUserProgress = (
queryKey: onboardingKeys.progress(userId), queryKey: onboardingKeys.progress(userId),
queryFn: () => onboardingService.getUserProgress(userId), queryFn: () => onboardingService.getUserProgress(userId),
enabled: !!userId, enabled: !!userId,
staleTime: 30 * 1000, // 30 seconds // OPTIMIZATION: Once onboarding is fully completed, it won't change back
// Use longer staleTime (5 min) and gcTime (30 min) to reduce API calls
// The select function below will update staleTime based on completion status
staleTime: 5 * 60 * 1000, // 5 minutes (increased from 30s - completed status rarely changes)
gcTime: 30 * 60 * 1000, // 30 minutes - keep in cache longer
refetchOnWindowFocus: false, // Don't refetch on window focus for onboarding status
...options, ...options,
}); });
}; };

View File

@@ -20,6 +20,10 @@ import { parseISO } from 'date-fns';
// Debounce delay for SSE-triggered query invalidations (ms) // Debounce delay for SSE-triggered query invalidations (ms)
const SSE_INVALIDATION_DEBOUNCE_MS = 500; const SSE_INVALIDATION_DEBOUNCE_MS = 500;
// Delay before SSE invalidations are allowed after initial load (ms)
// This prevents duplicate API calls when SSE events arrive during/right after initial fetch
const SSE_INITIAL_LOAD_GRACE_PERIOD_MS = 3000;
// ============================================================ // ============================================================
// Types // Types
// ============================================================ // ============================================================
@@ -421,6 +425,15 @@ export function useControlPanelData(tenantId: string) {
// Ref for debouncing SSE-triggered invalidations // Ref for debouncing SSE-triggered invalidations
const invalidationTimeoutRef = useRef<NodeJS.Timeout | null>(null); const invalidationTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const lastEventCountRef = useRef<number>(0); const lastEventCountRef = useRef<number>(0);
// Track when the initial data was successfully fetched to avoid immediate SSE refetches
const initialLoadTimestampRef = useRef<number | null>(null);
// Update initial load timestamp when query succeeds
useEffect(() => {
if (query.isSuccess && !initialLoadTimestampRef.current) {
initialLoadTimestampRef.current = Date.now();
}
}, [query.isSuccess]);
// SSE integration - invalidate query on relevant events (debounced) // SSE integration - invalidate query on relevant events (debounced)
useEffect(() => { useEffect(() => {
@@ -429,6 +442,17 @@ export function useControlPanelData(tenantId: string) {
return; return;
} }
// OPTIMIZATION: Skip SSE-triggered invalidation during grace period after initial load
// This prevents duplicate API calls when SSE events arrive during/right after the initial fetch
if (initialLoadTimestampRef.current) {
const timeSinceInitialLoad = Date.now() - initialLoadTimestampRef.current;
if (timeSinceInitialLoad < SSE_INITIAL_LOAD_GRACE_PERIOD_MS) {
// Update the event count ref so we don't process these events later
lastEventCountRef.current = sseAlerts.length;
return;
}
}
const relevantEvents = sseAlerts.filter(event => const relevantEvents = sseAlerts.filter(event =>
event.event_type?.includes('production.') || event.event_type?.includes('production.') ||
event.event_type?.includes('batch_') || event.event_type?.includes('batch_') ||

View File

@@ -36,14 +36,9 @@ export const TenantSwitcher: React.FC<TenantSwitcherProps> = ({
clearError, clearError,
} = useTenant(); } = useTenant();
// NOTE: Removed duplicate loadUserTenants() useEffect
// Tenant loading is already handled by useTenantInitializer at app level (stores/useTenantInitializer.ts)
// Load tenants on mount // This was causing duplicate /tenants API calls on every dashboard load
useEffect(() => {
if (!availableTenants) {
loadUserTenants();
}
}, [availableTenants, loadUserTenants]);
// Handle click outside to close dropdown // Handle click outside to close dropdown
useEffect(() => { useEffect(() => {

View File

@@ -495,13 +495,19 @@ export function DashboardPage() {
const { plan, loading: subLoading } = subscriptionInfo; const { plan, loading: subLoading } = subscriptionInfo;
const tenantId = currentTenant?.id; const tenantId = currentTenant?.id;
// Fetch onboarding progress // Check if in demo mode - demo users don't need onboarding check
// (backend returns fully_completed=true for demo users anyway, but we skip the API call entirely)
const isDemoMode = localStorage.getItem('demo_mode') === 'true';
// Fetch onboarding progress - SKIP for demo users and enterprise tier
// Demo users are pre-configured through cloning, so onboarding is always complete
const isAuthenticated = useIsAuthenticated(); const isAuthenticated = useIsAuthenticated();
const { data: userProgress, isLoading: progressLoading } = useUserProgress('', { const { data: userProgress, isLoading: progressLoading } = useUserProgress('', {
enabled: !!isAuthenticated && plan !== SUBSCRIPTION_TIERS.ENTERPRISE enabled: !!isAuthenticated && !isDemoMode && plan !== SUBSCRIPTION_TIERS.ENTERPRISE
}); });
const loading = subLoading || progressLoading; // Don't wait for progressLoading if demo mode (we're not fetching it)
const loading = subLoading || (!isDemoMode && progressLoading);
useEffect(() => { useEffect(() => {
if (!loading && userProgress && !userProgress.fully_completed && plan !== SUBSCRIPTION_TIERS.ENTERPRISE) { if (!loading && userProgress && !userProgress.fully_completed && plan !== SUBSCRIPTION_TIERS.ENTERPRISE) {

View File

@@ -54,7 +54,7 @@ export const useTenantInitializer = () => {
const demoAccountType = useDemoAccountType(); const demoAccountType = useDemoAccountType();
const availableTenants = useAvailableTenants(); const availableTenants = useAvailableTenants();
const currentTenant = useCurrentTenant(); const currentTenant = useCurrentTenant();
const { loadUserTenants, setCurrentTenant } = useTenantActions(); const { loadUserTenants, setCurrentTenant, setAvailableTenants } = useTenantActions();
// Load tenants for authenticated users (but not demo users - they have special initialization below) // Load tenants for authenticated users (but not demo users - they have special initialization below)
useEffect(() => { useEffect(() => {
@@ -79,34 +79,35 @@ export const useTenantInitializer = () => {
typeof currentTenant === 'object' && typeof currentTenant === 'object' &&
currentTenant.id === virtualTenantId; currentTenant.id === virtualTenantId;
// Determine the appropriate subscription tier based on stored value or account type
const subscriptionTier = storedTier as SubscriptionTier || getDemoTierForAccountType(demoAccountType);
// Get appropriate tenant details based on account type
const tenantDetails = getTenantDetailsForAccountType(demoAccountType);
// Create a complete tenant object matching TenantResponse structure
const mockTenant = {
id: virtualTenantId,
name: tenantDetails.name,
subdomain: `demo-${demoSessionId.slice(0, 8)}`,
business_type: tenantDetails.business_type,
business_model: tenantDetails.business_model,
description: tenantDetails.description,
address: 'Demo Address',
city: 'Madrid',
postal_code: '28001',
phone: null,
is_active: true,
subscription_plan: subscriptionTier,
subscription_tier: subscriptionTier,
ml_model_trained: false,
last_training_date: null,
owner_id: 'demo-user',
created_at: new Date().toISOString(),
};
// Only set current tenant if not already valid
if (!isValidDemoTenant) { if (!isValidDemoTenant) {
// Determine the appropriate subscription tier based on stored value or account type
const subscriptionTier = storedTier as SubscriptionTier || getDemoTierForAccountType(demoAccountType);
// Get appropriate tenant details based on account type
const tenantDetails = getTenantDetailsForAccountType(demoAccountType);
// Create a complete tenant object matching TenantResponse structure
const mockTenant = {
id: virtualTenantId,
name: tenantDetails.name,
subdomain: `demo-${demoSessionId.slice(0, 8)}`,
business_type: tenantDetails.business_type,
business_model: tenantDetails.business_model,
description: tenantDetails.description,
address: 'Demo Address',
city: 'Madrid',
postal_code: '28001',
phone: null,
is_active: true,
subscription_plan: subscriptionTier,
subscription_tier: subscriptionTier,
ml_model_trained: false,
last_training_date: null,
owner_id: 'demo-user',
created_at: new Date().toISOString(),
};
// Set the demo tenant as current // Set the demo tenant as current
setCurrentTenant(mockTenant); setCurrentTenant(mockTenant);
@@ -114,25 +115,45 @@ export const useTenantInitializer = () => {
import('../api/client').then(({ apiClient }) => { import('../api/client').then(({ apiClient }) => {
apiClient.setTenantId(virtualTenantId); apiClient.setTenantId(virtualTenantId);
}); });
}
// For enterprise demos, load child tenants immediately // For professional demos, just set the single mock tenant as available (if not already set)
if (demoAccountType === 'enterprise') { if (demoAccountType !== 'enterprise') {
const mockUserId = 'demo-user'; if (!availableTenants || availableTenants.length === 0) {
setAvailableTenants([mockTenant as any]);
import('../api/services/tenant').then(({ TenantService }) => {
const tenantService = new TenantService();
tenantService.getUserTenants(mockUserId)
.then(tenants => {
import('../stores/tenant.store').then(({ useTenantStore }) => {
useTenantStore.getState().setAvailableTenants(tenants);
});
})
.catch(() => {
// Silently handle error
});
});
} }
return;
}
// For enterprise demos, ALWAYS ensure child tenants are loaded
// Check if we need to load child tenants:
// - availableTenants is empty/null, OR
// - availableTenants only has the parent (length === 1)
const needsChildTenants = !availableTenants || availableTenants.length <= 1;
if (needsChildTenants) {
console.log('[useTenantInitializer] Enterprise demo - loading child tenants for parent:', virtualTenantId);
import('../api/services/tenant').then(({ TenantService }) => {
const tenantService = new TenantService();
// Use getChildTenants with the parent tenant ID (virtualTenantId)
// This calls GET /tenants/{tenant_id}/children
tenantService.getChildTenants(virtualTenantId)
.then(childTenants => {
console.log('[useTenantInitializer] Enterprise demo - loaded child tenants:', childTenants?.length, childTenants);
// Combine parent tenant with children for the tenant switcher
const allTenants = [mockTenant as any, ...childTenants];
setAvailableTenants(allTenants);
})
.catch((error) => {
console.error('[useTenantInitializer] Enterprise demo - failed to load child tenants:', error);
// Fallback: at least set the parent mock tenant as available so TenantSwitcher renders
if (!availableTenants || availableTenants.length === 0) {
setAvailableTenants([mockTenant as any]);
}
});
});
} }
} }
}, [isDemoMode, demoSessionId, demoAccountType, currentTenant, setCurrentTenant]); }, [isDemoMode, demoSessionId, demoAccountType, currentTenant, availableTenants, setCurrentTenant, setAvailableTenants]);
}; };