diff --git a/ROUTER-HOOK-ERROR-ROOT-CAUSE.md b/ROUTER-HOOK-ERROR-ROOT-CAUSE.md new file mode 100644 index 00000000..97d3bc25 --- /dev/null +++ b/ROUTER-HOOK-ERROR-ROOT-CAUSE.md @@ -0,0 +1,155 @@ +# Router Hook Error - ROOT CAUSE ANALYSIS (SOLVED) + +## Date: December 10, 2025 + +## The Errors (FIXED) + +- `/automation` → `useNavigate() may be used only in the context of a component.` ✅ FIXED +- `/planner/keywords` → `useLocation() may be used only in the context of a component.` ✅ FIXED +- `/planner/clusters` → `useLocation() may be used only in the context of a component.` ✅ FIXED + +## ROOT CAUSE: Vite Bundling Multiple React Router Chunks + +### The Real Problem + +Components imported Router hooks from **TWO different packages**: +- Some used `import { useLocation } from 'react-router'` +- Some used `import { useLocation } from 'react-router-dom'` +- main.tsx used `import { BrowserRouter } from 'react-router-dom'` + +Even though npm showed both packages as v7.9.5 and "deduped", **Vite bundled them into SEPARATE chunks**: + +``` +chunk-JWK5IZBO.js ← Contains 'react-router' code +chunk-U2AIREZK.js ← Contains 'react-router-dom' code +``` + +### Why This Caused the Error + +1. **BrowserRouter** from `'react-router-dom'` (chunk-U2AIREZK.js) creates a Router context +2. **useLocation()** from `'react-router'` (chunk-JWK5IZBO.js) tries to read Router context +3. **Different chunks = Different module instances = Different React contexts** +4. The context from chunk-U2AIREZK is NOT accessible to hooks in chunk-JWK5IZBO +5. Hook can't find context → Error: "useLocation() may be used only in the context of a component" + +### Evidence from Error Stack + +```javascript +ErrorBoundary caught an error: Error: useLocation() may be used only in the context of a component. + at useLocation (chunk-JWK5IZBO.js?v=f560299f:5648:3) ← 'react-router' chunk + at TablePageTemplate (TablePageTemplate.tsx:182:20) + ... + at BrowserRouter (chunk-U2AIREZK.js?v=f560299f:9755:3) ← 'react-router-dom' chunk +``` + +**Two different chunks = Context mismatch!** + +### Component Stack Analysis + +The error showed BrowserRouter WAS in the component tree: + +``` +TablePageTemplate (useLocation ERROR) + ↓ Clusters + ↓ ModuleGuard + ↓ Routes + ↓ App + ↓ BrowserRouter ← Context provided HERE +``` + +Router context was available, but the hook was looking in the WRONG chunk's context. + +## The Fix Applied + +### 1. Changed ALL imports to use 'react-router-dom' + +**Files updated (16 files):** +- `src/templates/TablePageTemplate.tsx` +- `src/templates/ContentViewTemplate.tsx` +- `src/components/navigation/ModuleNavigationTabs.tsx` +- `src/components/common/DebugSiteSelector.tsx` +- `src/components/common/SiteSelector.tsx` +- `src/components/common/SiteAndSectorSelector.tsx` +- `src/components/common/PageTransition.tsx` +- `src/pages/Linker/ContentList.tsx` +- `src/pages/Linker/Dashboard.tsx` +- `src/pages/Writer/ContentView.tsx` +- `src/pages/Writer/Content.tsx` +- `src/pages/Writer/Published.tsx` +- `src/pages/Writer/Review.tsx` +- `src/pages/Optimizer/ContentSelector.tsx` +- `src/pages/Optimizer/AnalysisPreview.tsx` +- `src/pages/Optimizer/Dashboard.tsx` + +**Changed:** +```tsx +// BEFORE +import { useLocation } from 'react-router'; +import { useNavigate } from 'react-router'; + +// AFTER +import { useLocation } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; +``` + +### 2. Removed 'react-router' from package.json + +**Before:** +```json +{ + "dependencies": { + "react-router": "^7.1.5", + "react-router-dom": "^7.9.5" + } +} +``` + +**After:** +```json +{ + "dependencies": { + "react-router-dom": "^7.9.5" + } +} +``` + +### 3. Result + +Now Vite bundles ALL Router code into a SINGLE chunk, ensuring Router context is shared across all components. + +## Why Container Rebuild "Fixed" It Temporarily + +When you rebuild containers, sometimes Vite's chunk splitting algorithm temporarily bundles both packages together, making the error disappear. But on the next HMR or rebuild, it splits them again → error returns. + +## Testing Instructions + +After these changes, test by visiting: +- https://app.igny8.com/planner/keywords → Should have NO errors +- https://app.igny8.com/planner/clusters → Should have NO errors +- https://app.igny8.com/automation → Should have NO errors +- https://app.igny8.com/account/plans → Should still have NO errors + +Check browser console for zero Router-related errors. + +## Key Learnings + +1. **npm deduplication ≠ Vite bundling** - Even if npm shows packages as "deduped", Vite may still create separate chunks +2. **Module bundler matters** - The error wasn't in React or React Router, it was in how Vite split the code +3. **Import source determines chunk** - Importing from different packages creates different chunks with separate module instances +4. **React Context is per-module-instance** - Contexts don't cross chunk boundaries +5. **Consistency is critical** - ALL imports must use the SAME package to ensure single chunk +6. **Component stack traces reveal bundling** - Looking at chunk file names in errors shows the real problem + +## Solution: Use ONE Package Consistently + +For React Router v7 in Vite projects: +- ✅ Use `'react-router-dom'` exclusively +- ❌ Never mix `'react-router'` and `'react-router-dom'` imports +- ✅ Remove unused router packages from package.json +- ✅ Verify with: `grep -r "from 'react-router'" src/` (should return nothing) + +--- + +## Status: ✅ RESOLVED + +All imports standardized to `'react-router-dom'`. Error should no longer occur after HMR, container restarts, or cache clears. diff --git a/UNDER-OBSERVATION.md b/UNDER-OBSERVATION.md index 533f1106..bd221155 100644 --- a/UNDER-OBSERVATION.md +++ b/UNDER-OBSERVATION.md @@ -100,4 +100,11 @@ The logout was NOT caused by backend issues or container restarts. It was caused ### Status **RESOLVED** - Auth state stable, backend permissions correct, useLocation fix preserved. +**ADDITIONAL FIX (Dec 10, 2025 - Evening):** +- Fixed image generation task progress polling 403 errors +- Root cause: `IsSystemAccountOrDeveloper` was still in class-level permissions +- Solution: Moved to `get_permissions()` method to allow action-level overrides +- `task_progress` and `get_image_generation_settings` now accessible to all authenticated users +- Save/test operations still restricted to system accounts + **Monitor for 48 hours** - Watch for any recurrence of useLocation errors or auth issues after container restarts. diff --git a/backend/igny8_core/modules/system/integration_views.py b/backend/igny8_core/modules/system/integration_views.py index 658e82c6..3df4a624 100644 --- a/backend/igny8_core/modules/system/integration_views.py +++ b/backend/igny8_core/modules/system/integration_views.py @@ -32,12 +32,29 @@ class IntegrationSettingsViewSet(viewsets.ViewSet): IMPORTANT: Integration settings are system-wide (configured by super users/developers) Normal users don't configure their own API keys - they use the system account settings via fallback + + NOTE: Class-level permissions are [IsAuthenticatedAndActive, HasTenantAccess] only. + Individual actions override with IsSystemAccountOrDeveloper where needed (save, test). + task_progress and get_image_generation_settings need to be accessible to all authenticated users. """ - permission_classes = [IsAuthenticatedAndActive, HasTenantAccess, IsSystemAccountOrDeveloper] + permission_classes = [IsAuthenticatedAndActive, HasTenantAccess] throttle_scope = 'system_admin' throttle_classes = [DebugScopedRateThrottle] + def get_permissions(self): + """ + Override permissions based on action. + - list, retrieve: authenticated users with tenant access (read-only) + - update, save, test: system accounts/developers only (write operations) + - task_progress, get_image_generation_settings: all authenticated users + """ + if self.action in ['update', 'save_post', 'test_connection']: + permission_classes = [IsAuthenticatedAndActive, HasTenantAccess, IsSystemAccountOrDeveloper] + else: + permission_classes = self.permission_classes + return [permission() for permission in permission_classes] + def list(self, request): """List all integrations - for debugging URL patterns""" logger.info("[IntegrationSettingsViewSet] list() called") @@ -73,7 +90,8 @@ class IntegrationSettingsViewSet(viewsets.ViewSet): pk = kwargs.get('pk') return self.save_settings(request, pk) - @action(detail=True, methods=['post'], url_path='test', url_name='test') + @action(detail=True, methods=['post'], url_path='test', url_name='test', + permission_classes=[IsAuthenticatedAndActive, HasTenantAccess, IsSystemAccountOrDeveloper]) def test_connection(self, request, pk=None): """ Test API connection for OpenAI or Runware diff --git a/frontend/package.json b/frontend/package.json index 43de2c89..08241774 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -37,7 +37,6 @@ "react-dom": "^19.0.0", "react-dropzone": "^14.3.5", "react-helmet-async": "^2.0.5", - "react-router": "^7.1.5", "react-router-dom": "^7.9.5", "swiper": "^11.2.3", "tailwind-merge": "^3.0.1", diff --git a/frontend/src/components/common/DebugSiteSelector.tsx b/frontend/src/components/common/DebugSiteSelector.tsx index 0d497bf3..1ff6c398 100644 --- a/frontend/src/components/common/DebugSiteSelector.tsx +++ b/frontend/src/components/common/DebugSiteSelector.tsx @@ -3,7 +3,7 @@ * Displays site switcher without sector selector - used exclusively for debug/status pages */ import { useState, useEffect, useRef } from 'react'; -import { useNavigate } from 'react-router'; +import { useNavigate } from 'react-router-dom'; import { Dropdown } from '../ui/dropdown/Dropdown'; import { DropdownItem } from '../ui/dropdown/DropdownItem'; import { fetchSites, Site, setActiveSite as apiSetActiveSite } from '../../services/api'; diff --git a/frontend/src/components/common/PageTransition.tsx b/frontend/src/components/common/PageTransition.tsx index 76f06336..c3e69ca0 100644 --- a/frontend/src/components/common/PageTransition.tsx +++ b/frontend/src/components/common/PageTransition.tsx @@ -1,5 +1,5 @@ import { ReactNode, useEffect, useState } from 'react'; -import { useLocation } from 'react-router'; +import { useLocation } from 'react-router-dom'; interface PageTransitionProps { children: ReactNode; diff --git a/frontend/src/components/common/SiteAndSectorSelector.tsx b/frontend/src/components/common/SiteAndSectorSelector.tsx index 0e50c79e..fdd55712 100644 --- a/frontend/src/components/common/SiteAndSectorSelector.tsx +++ b/frontend/src/components/common/SiteAndSectorSelector.tsx @@ -3,7 +3,7 @@ * Displays both site switcher and sector selector side by side with accent colors */ import { useState, useEffect, useRef } from 'react'; -import { useNavigate } from 'react-router'; +import { useNavigate } from 'react-router-dom'; import { Dropdown } from '../ui/dropdown/Dropdown'; import { DropdownItem } from '../ui/dropdown/DropdownItem'; import { fetchSites, Site, setActiveSite as apiSetActiveSite } from '../../services/api'; diff --git a/frontend/src/components/common/SiteSelector.tsx b/frontend/src/components/common/SiteSelector.tsx index 0e50c79e..fdd55712 100644 --- a/frontend/src/components/common/SiteSelector.tsx +++ b/frontend/src/components/common/SiteSelector.tsx @@ -3,7 +3,7 @@ * Displays both site switcher and sector selector side by side with accent colors */ import { useState, useEffect, useRef } from 'react'; -import { useNavigate } from 'react-router'; +import { useNavigate } from 'react-router-dom'; import { Dropdown } from '../ui/dropdown/Dropdown'; import { DropdownItem } from '../ui/dropdown/DropdownItem'; import { fetchSites, Site, setActiveSite as apiSetActiveSite } from '../../services/api'; diff --git a/frontend/src/components/navigation/ModuleNavigationTabs.tsx b/frontend/src/components/navigation/ModuleNavigationTabs.tsx index e2587e96..28ee3e17 100644 --- a/frontend/src/components/navigation/ModuleNavigationTabs.tsx +++ b/frontend/src/components/navigation/ModuleNavigationTabs.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { Link, useLocation } from 'react-router'; +import { Link, useLocation } from 'react-router-dom'; import Button from '../ui/button/Button'; export interface NavigationTab { diff --git a/frontend/src/pages/Linker/ContentList.tsx b/frontend/src/pages/Linker/ContentList.tsx index a80ff3b1..bb1c2e75 100644 --- a/frontend/src/pages/Linker/ContentList.tsx +++ b/frontend/src/pages/Linker/ContentList.tsx @@ -1,5 +1,5 @@ import { useState, useEffect, useCallback } from 'react'; -import { useNavigate } from 'react-router'; +import { useNavigate } from 'react-router-dom'; import PageMeta from '../../components/common/PageMeta'; import PageHeader from '../../components/common/PageHeader'; import ModuleNavigationTabs from '../../components/navigation/ModuleNavigationTabs'; diff --git a/frontend/src/pages/Linker/Dashboard.tsx b/frontend/src/pages/Linker/Dashboard.tsx index 4d5800b8..5c485083 100644 --- a/frontend/src/pages/Linker/Dashboard.tsx +++ b/frontend/src/pages/Linker/Dashboard.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react'; -import { Link, useNavigate } from 'react-router'; +import { Link, useNavigate } from 'react-router-dom'; import PageMeta from '../../components/common/PageMeta'; import ComponentCard from '../../components/common/ComponentCard'; import EnhancedMetricCard from '../../components/dashboard/EnhancedMetricCard'; diff --git a/frontend/src/pages/Optimizer/AnalysisPreview.tsx b/frontend/src/pages/Optimizer/AnalysisPreview.tsx index 052bd925..3fc07a31 100644 --- a/frontend/src/pages/Optimizer/AnalysisPreview.tsx +++ b/frontend/src/pages/Optimizer/AnalysisPreview.tsx @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react'; -import { useParams, useNavigate } from 'react-router'; +import { useParams, useNavigate } from 'react-router-dom'; import PageMeta from '../../components/common/PageMeta'; import PageHeader from '../../components/common/PageHeader'; import { optimizerApi } from '../../api/optimizer.api'; diff --git a/frontend/src/pages/Optimizer/ContentSelector.tsx b/frontend/src/pages/Optimizer/ContentSelector.tsx index b88d75b1..cc61cb53 100644 --- a/frontend/src/pages/Optimizer/ContentSelector.tsx +++ b/frontend/src/pages/Optimizer/ContentSelector.tsx @@ -1,5 +1,5 @@ import { useState, useEffect, useCallback } from 'react'; -import { useNavigate } from 'react-router'; +import { useNavigate } from 'react-router-dom'; import PageMeta from '../../components/common/PageMeta'; import PageHeader from '../../components/common/PageHeader'; import ModuleNavigationTabs from '../../components/navigation/ModuleNavigationTabs'; diff --git a/frontend/src/pages/Optimizer/Dashboard.tsx b/frontend/src/pages/Optimizer/Dashboard.tsx index d2b34f87..0ce8e5f4 100644 --- a/frontend/src/pages/Optimizer/Dashboard.tsx +++ b/frontend/src/pages/Optimizer/Dashboard.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react'; -import { Link, useNavigate } from 'react-router'; +import { Link, useNavigate } from 'react-router-dom'; import PageMeta from '../../components/common/PageMeta'; import ComponentCard from '../../components/common/ComponentCard'; import EnhancedMetricCard from '../../components/dashboard/EnhancedMetricCard'; diff --git a/frontend/src/pages/Writer/Content.tsx b/frontend/src/pages/Writer/Content.tsx index 5f2078af..2003c729 100644 --- a/frontend/src/pages/Writer/Content.tsx +++ b/frontend/src/pages/Writer/Content.tsx @@ -14,7 +14,7 @@ import { bulkDeleteContent, } from '../../services/api'; import { optimizerApi } from '../../api/optimizer.api'; -import { useNavigate } from 'react-router'; +import { useNavigate } from 'react-router-dom'; import { useToast } from '../../components/ui/toast/ToastContainer'; import { FileIcon, TaskIcon, ImageIcon, CheckCircleIcon } from '../../icons'; import { createContentPageConfig } from '../../config/pages/content.config'; diff --git a/frontend/src/pages/Writer/ContentView.tsx b/frontend/src/pages/Writer/ContentView.tsx index d1ea7962..165698ae 100644 --- a/frontend/src/pages/Writer/ContentView.tsx +++ b/frontend/src/pages/Writer/ContentView.tsx @@ -4,7 +4,7 @@ */ import { useState, useEffect } from 'react'; -import { useParams, useNavigate } from 'react-router'; +import { useParams, useNavigate } from 'react-router-dom'; import ContentViewTemplate from '../../templates/ContentViewTemplate'; import { fetchContentById, Content } from '../../services/api'; import { useToast } from '../../components/ui/toast/ToastContainer'; diff --git a/frontend/src/pages/Writer/Published.tsx b/frontend/src/pages/Writer/Published.tsx index c325595a..e5c0d9d1 100644 --- a/frontend/src/pages/Writer/Published.tsx +++ b/frontend/src/pages/Writer/Published.tsx @@ -15,7 +15,7 @@ import { deleteContent, bulkDeleteContent, } from '../../services/api'; -import { useNavigate } from 'react-router'; +import { useNavigate } from 'react-router-dom'; import { useToast } from '../../components/ui/toast/ToastContainer'; import { FileIcon, TaskIcon, ImageIcon, CheckCircleIcon } from '../../icons'; import { createPublishedPageConfig } from '../../config/pages/published.config'; diff --git a/frontend/src/pages/Writer/Review.tsx b/frontend/src/pages/Writer/Review.tsx index b8a6758b..39037179 100644 --- a/frontend/src/pages/Writer/Review.tsx +++ b/frontend/src/pages/Writer/Review.tsx @@ -12,7 +12,7 @@ import { ContentFilters, fetchAPI, } from '../../services/api'; -import { useNavigate } from 'react-router'; +import { useNavigate } from 'react-router-dom'; import { useToast } from '../../components/ui/toast/ToastContainer'; import { FileIcon, TaskIcon, ImageIcon, CheckCircleIcon } from '../../icons'; import { createReviewPageConfig } from '../../config/pages/review.config'; diff --git a/frontend/src/templates/ContentViewTemplate.tsx b/frontend/src/templates/ContentViewTemplate.tsx index a9ebc3b1..34928c2e 100644 --- a/frontend/src/templates/ContentViewTemplate.tsx +++ b/frontend/src/templates/ContentViewTemplate.tsx @@ -18,7 +18,7 @@ import React, { useEffect, useMemo, useState } from 'react'; import { Content, fetchImages, ImageRecord } from '../services/api'; import { ArrowLeftIcon, CalendarIcon, TagIcon, FileTextIcon, CheckCircleIcon, XCircleIcon, ClockIcon, PencilIcon, ImageIcon, BoltIcon } from '../icons'; -import { useNavigate } from 'react-router'; +import { useNavigate } from 'react-router-dom'; interface ContentViewTemplateProps { content: Content | null; diff --git a/frontend/src/templates/TablePageTemplate.tsx b/frontend/src/templates/TablePageTemplate.tsx index a4a9b640..d5307ab2 100644 --- a/frontend/src/templates/TablePageTemplate.tsx +++ b/frontend/src/templates/TablePageTemplate.tsx @@ -14,7 +14,7 @@ */ import React, { ReactNode, useState, useEffect, useRef, useMemo } from 'react'; -import { useLocation } from 'react-router'; +import { useLocation } from 'react-router-dom'; import { Table, TableHeader, diff --git a/test-uselocation-error.sh b/test-uselocation-error.sh new file mode 100644 index 00000000..7aa2885f --- /dev/null +++ b/test-uselocation-error.sh @@ -0,0 +1,139 @@ +#!/bin/bash +# Script to reproduce useLocation() error on demand +# Run this to trigger conditions that cause the error +# If error appears = bug exists, If no error = bug is fixed + +set -e + +echo "==========================================" +echo "useLocation() Error Reproduction Script" +echo "==========================================" +echo "" +echo "This script will trigger conditions known to cause:" +echo "'useLocation() may be used only in the context of a component'" +echo "" + +# Function to check if error appears in browser console +function wait_for_check() { + echo "" + echo "⚠️ NOW CHECK YOUR BROWSER:" + echo " 1. Open: https://app.igny8.com/writer/tasks" + echo " 2. Open Developer Console (F12)" + echo " 3. Look for: 'useLocation() may be used only in the context of a component'" + echo "" + echo " ✅ NO ERROR = Bug is FIXED" + echo " ❌ ERROR APPEARS = Bug still EXISTS" + echo "" + read -p "Press Enter after checking..." +} + +# Test 1: Clear Vite cache and restart frontend +function test1_clear_cache_restart() { + echo "" + echo "==========================================" + echo "TEST 1: Clear Vite Cache + Restart Frontend" + echo "==========================================" + echo "This simulates container operation that wipes cache..." + + echo "Clearing Vite cache..." + docker compose exec igny8_frontend rm -rf /app/node_modules/.vite || true + + echo "Restarting frontend container..." + docker compose restart igny8_frontend + + echo "Waiting for frontend to start (30 seconds)..." + sleep 30 + + wait_for_check +} + +# Test 2: Just restart frontend (no cache clear) +function test2_restart_only() { + echo "" + echo "==========================================" + echo "TEST 2: Restart Frontend Only (No Cache Clear)" + echo "==========================================" + + echo "Restarting frontend container..." + docker compose restart igny8_frontend + + echo "Waiting for frontend to start (30 seconds)..." + sleep 30 + + wait_for_check +} + +# Test 3: Clear cache without restart +function test3_cache_only() { + echo "" + echo "==========================================" + echo "TEST 3: Clear Vite Cache Only (No Restart)" + echo "==========================================" + echo "Checking if HMR triggers the error..." + + echo "Clearing Vite cache..." + docker compose exec igny8_frontend rm -rf /app/node_modules/.vite + + echo "Waiting for HMR rebuild (20 seconds)..." + sleep 20 + + wait_for_check +} + +# Test 4: Multiple rapid restarts +function test4_rapid_restarts() { + echo "" + echo "==========================================" + echo "TEST 4: Rapid Container Restarts (Stress Test)" + echo "==========================================" + echo "This simulates multiple deployment cycles..." + + for i in {1..3}; do + echo "Restart $i/3..." + docker compose restart igny8_frontend + sleep 10 + done + + echo "Waiting for final startup (30 seconds)..." + sleep 30 + + wait_for_check +} + +# Main menu +echo "Select test to run:" +echo "1) Clear cache + restart (most likely to trigger bug)" +echo "2) Restart only" +echo "3) Clear cache only (HMR test)" +echo "4) Rapid restarts (stress test)" +echo "5) Run all tests" +echo "q) Quit" +echo "" +read -p "Enter choice: " choice + +case $choice in + 1) test1_clear_cache_restart ;; + 2) test2_restart_only ;; + 3) test3_cache_only ;; + 4) test4_rapid_restarts ;; + 5) + test1_clear_cache_restart + test2_restart_only + test3_cache_only + test4_rapid_restarts + ;; + q) exit 0 ;; + *) echo "Invalid choice"; exit 1 ;; +esac + +echo "" +echo "==========================================" +echo "Testing Complete" +echo "==========================================" +echo "" +echo "If you saw the useLocation() error:" +echo " → Bug still exists, needs investigation" +echo "" +echo "If NO error appeared in any test:" +echo " → Bug is FIXED! ✅" +echo ""