Files
igny8/docs/90-REFERENCE/FIXES-KB.md
IGNY8 VPS (Salman) bc371e5482 Consolidate docs: move design/docs files to docs folder
- Moved DESIGN-GUIDE.md → docs/30-FRONTEND/DESIGN-GUIDE.md
- Moved frontend/DESIGN_SYSTEM.md → docs/30-FRONTEND/DESIGN-TOKENS.md
- Moved IGNY8-APP.md → docs/00-SYSTEM/IGNY8-APP.md
- Moved fixes-kb.md → docs/90-REFERENCE/FIXES-KB.md
- Moved FINAL_PRELAUNCH.md → docs/plans/FINAL-PRELAUNCH.md
- Updated all references in .rules, README.md, docs/INDEX.md
- Updated ESLint plugin documentation comments
- Root folder now only contains: .rules, CHANGELOG.md, README.md
2026-01-02 23:43:58 +00:00

16 KiB

Architecture Knowledge Base

Last Updated: December 14, 2025
Purpose: Critical architectural patterns, common issues, and solutions reference


🔥 CRITICAL FIXES - December 2025

PERMANENT FIX: Django Admin Custom Sidebar Not Showing on Subpages

ROOT CAUSE: Django's ModelAdmin view methods (changelist_view, change_view, etc.) do not call AdminSite.each_context(), so custom sidebar logic defined in site.py was bypassed on model list/detail/edit pages.

SOLUTION IMPLEMENTED:

  1. Created Igny8ModelAdmin base class extending UnfoldModelAdmin
  2. Overrides all view methods to inject extra_context with custom sidebar
  3. Applied to 46+ admin classes across all modules
  4. Sidebar now consistent on homepage, app index, and ALL model pages

Files Modified: backend/igny8_core/admin/base.py, all */admin.py files

PERMANENT FIX: User Swapping / Random Logout Issue

ROOT CAUSE: Django's database-backed sessions with in-memory user caching caused cross-request contamination at the process level.

SOLUTION IMPLEMENTED:

  1. Redis-backed sessions (SESSION_ENGINE = 'django.contrib.sessions.backends.cache')
  2. Custom authentication backend without caching (NoCacheModelBackend)
  3. Session integrity validation (stores and verifies account_id/user_id on every request)
  4. Middleware never mutates request.user (uses Django's set value directly)

See: CRITICAL-BUG-FIXES-DEC-2025.md for complete details.

PERMANENT FIX: useNavigate / useLocation Errors During HMR

ROOT CAUSE: Individual Suspense boundaries per route lost React Router context during Hot Module Replacement.

SOLUTION IMPLEMENTED:

  1. Single top-level Suspense boundary around entire <Routes> component
  2. Removed 100+ individual Suspense wrappers from route elements
  3. Router context now persists through HMR automatically

See: CRITICAL-BUG-FIXES-DEC-2025.md for complete details.


Table of Contents

  1. Authentication & Session Management
  2. Site/Sector Architecture
  3. State Management & Race Conditions
  4. Permission System
  5. Frontend Component Dependencies
  6. Common Pitfalls & Solutions

Authentication & Session Management

Token Persistence Architecture

Problem Pattern:

  • Zustand persist middleware writes to localStorage asynchronously
  • API calls can happen before tokens are persisted
  • Results in 403 "Authentication credentials were not provided" errors

Solution Implemented:

// In authStore.ts login/register functions
// CRITICAL: Immediately persist tokens synchronously after setting state
const authState = {
  state: { user, token, refreshToken, isAuthenticated: true },
  version: 0
};
localStorage.setItem('auth-storage', JSON.stringify(authState));

Key Principle: Always write tokens to localStorage synchronously in auth actions, don't rely solely on persist middleware.


Logout & State Cleanup

WRONG APPROACH (causes race conditions):

logout: () => {
  localStorage.clear(); // ❌ BREAKS EVERYTHING
  set({ user: null, token: null });
}

CORRECT APPROACH:

logout: () => {
  // ✅ Selective removal - only auth-related keys
  const authKeys = ['auth-storage', 'site-storage', 'sector-storage', 'billing-storage'];
  authKeys.forEach(key => localStorage.removeItem(key));
  
  // ✅ Reset dependent stores explicitly
  useSiteStore.setState({ activeSite: null });
  useSectorStore.setState({ activeSector: null, sectors: [] });
  
  set({ user: null, token: null, isAuthenticated: false });
}

Key Principle: Never use localStorage.clear() - it breaks Zustand persist middleware initialization. Always selectively remove keys.


403 Error Handling

Problem Pattern:

  • 403 errors thrown before checking if it's an auth error
  • Token validation code becomes unreachable
  • Invalid tokens persist in localStorage

WRONG ORDER:

// In api.ts
if (response.status === 403) {
  throw new Error(response.statusText); // ❌ Thrown immediately
}

// This code NEVER runs (unreachable):
if (errorData?.detail?.includes('Authentication credentials')) {
  logout(); // Never called!
}

CORRECT ORDER:

// Check for auth errors FIRST, then throw
if (response.status === 403) {
  const errorData = JSON.parse(text);
  
  // ✅ Check authentication BEFORE throwing
  if (errorData?.detail?.includes('Authentication credentials')) {
    const authState = useAuthStore.getState();
    if (authState?.isAuthenticated) {
      authState.logout();
      window.location.href = '/signin';
    }
  }
  
  // Now throw the error
  throw new Error(errorMessage);
}

Key Principle: Handle authentication errors before throwing. Order matters in error handling logic.


Site/Sector Architecture

Data Hierarchy

Account (Tenant)
  └── Site (e.g., myblog.com)
      └── Sector (e.g., Technology, Health)
          └── Keywords
              └── Clusters
                  └── Ideas
                      └── Content

Where Sectors Are Used (Global Context)

USES SECTORS (requires site/sector selection):

  • Planner Module (Keywords, Clusters, Ideas)
  • Writer Module (Tasks, Content, Drafts, Published)
  • Linker Module (Internal linking)
  • Optimizer Module (Content optimization)
  • Setup/Add Keywords page
  • Seed Keywords reference data

DOES NOT USE SECTORS (account-level only):

  • Billing/Plans pages (/account/*)
  • Account Settings
  • Team Management
  • User Profile
  • Admin Dashboard
  • System Settings

Sector Loading Pattern

Architecture Decision:

  • Sectors loaded by PageHeader component (not AppLayout)
  • Only loads when hideSiteSector={false} prop is set
  • Account/billing pages pass hideSiteSector={true} to skip loading

Implementation:

// PageHeader.tsx
useEffect(() => {
  if (hideSiteSector) return; // Skip for account pages
  
  const currentSiteId = activeSite?.id ?? null;
  if (currentSiteId && activeSite?.is_active) {
    loadSectorsForSite(currentSiteId);
  }
}, [activeSite?.id, hideSiteSector]);

Key Principle: Lazy-load sectors only when components need them. Don't load globally for all pages.


Site/Sector Store Persistence

Storage Keys:

  • site-storage - Active site selection
  • sector-storage - Active sector selection

Reset Pattern:

// When site changes, reset sector if it belongs to different site
if (currentSector && currentSector.site_id !== newSiteId) {
  set({ activeSector: null });
  localStorage.setItem('sector-storage', JSON.stringify({
    state: { activeSector: null },
    version: 0
  }));
}

Key Principle: Sector selection is site-scoped. Always validate sector belongs to active site.


State Management & Race Conditions

Common Race Condition Patterns

1. User Switching

Problem: Rapid logout → login leaves stale state in stores

Solution:

logout: () => {
  // Reset ALL dependent stores explicitly
  import('./siteStore').then(({ useSiteStore }) => {
    useSiteStore.setState({ activeSite: null, loading: false, error: null });
  });
  import('./sectorStore').then(({ useSectorStore }) => {
    useSectorStore.setState({ activeSector: null, sectors: [], loading: false, error: null });
  });
}

2. API Calls Before Token Persistence

Problem: API calls happen before Zustand persist writes token

Solution: Synchronous localStorage write immediately after state update (see Authentication section)

3. Module Loading Failures

Problem: 404 errors during page navigation cause module loading to fail

Solution: Ensure API endpoints exist before pages try to load them. Use conditional rendering based on route.


Zustand Persist Middleware Gotchas

Issue 1: Version Mismatch

// Stored format
{ state: { user, token }, version: 0 }

// If version changes, persist middleware clears state

Issue 2: Async Hydration

  • State rehydration from localStorage is async
  • Can cause brief flash of "no user" state

Solution: Use loading states or check both store AND localStorage:

const getAuthToken = (): string | null => {
  // Try Zustand store first
  const authState = useAuthStore.getState();
  if (authState?.token) return authState.token;
  
  // Fallback to localStorage
  const stored = localStorage.getItem('auth-storage');
  return JSON.parse(stored)?.state?.token || null;
};

Permission System

Superuser/Developer Bypass Pattern

Critical Locations for Bypass:

  1. Middleware - auth/middleware.py
  2. Permission Classes - api/permissions.py
  3. ViewSet Querysets - api/base.py
  4. Validation Functions - auth/utils.py

Standard Bypass Check:

def check_bypass(user):
    return (
        user.is_superuser or
        user.role == 'developer' or
        is_system_account_user(user)
    )

Apply at ALL levels:

  • Middleware request validation
  • DRF permission has_permission()
  • ViewSet get_queryset() filtering
  • Custom validation functions

Key Principle: Bypass checks must be consistent across all permission layers. Missing one layer breaks superuser access.


System Account Pattern

Reserved Accounts:

  • aws-admin - System automation account
  • default-account - Default tenant fallback

Check Function:

def is_system_account_user(user):
    if not user or not user.account:
        return False
    return user.account.slug in ['aws-admin', 'default-account']

Usage: Always include in bypass checks alongside superuser/developer.


Frontend Component Dependencies

PageHeader Component

Dependencies:

  • useSiteStore - Active site
  • useSectorStore - Active sector
  • SiteAndSectorSelector - Dropdown component

Props:

  • hideSiteSector: boolean - Skip site/sector display and loading
  • title: string - Page title
  • navigation: ReactNode - Optional module tabs

Used By:

  • All Planner pages
  • All Writer pages
  • All Optimizer pages
  • Setup pages
  • Seed Keywords page

NOT Used By:

  • Account/billing pages (use plain headers instead)

Module Navigation Pattern

Component: ModuleNavigationTabs.tsx

CRITICAL: Must be wrapped in <Router> context

  • Uses useLocation() and useNavigate() hooks
  • Cannot be used outside <Routes> tree

Common Error:

Error: useLocation() may be used only in the context of a <Router> component

Cause: Component rendered outside React Router context

Solution: Ensure component is within <Route> element in App.tsx


Common Pitfalls & Solutions

Pitfall 1: Frontend 403 Errors After User Switch

Symptoms:

  • "Authentication credentials were not provided"
  • User appears logged in but API calls fail
  • Manually clearing cache fixes it

Root Cause: Invalid tokens persisting in localStorage after logout

Solution:

  1. Check 403 handler runs BEFORE throwing error
  2. Ensure logout clears specific auth keys (not localStorage.clear())
  3. Add immediate token persistence after login

Prevention: See "Authentication & Session Management" section


Pitfall 2: Sector 404 Errors on Billing Pages

Symptoms:

  • GET /v1/auth/sites/{id}/sectors/ returns 404
  • "Failed to fetch dynamically imported module" error
  • Billing pages don't load

Root Cause: AppLayout loading sectors for ALL pages globally

Solution: Move sector loading to PageHeader component (lazy loading)

Prevention: Only load data when components that need it are mounted


Pitfall 3: Module Loading Failures After Git Commits

Symptoms:

  • React Router context errors
  • "useLocation() may be used only in context of " errors
  • Pages work after rebuild but fail after git push

Root Cause: Docker build cache not invalidated properly

Solution:

# Force clean rebuild
docker compose -f docker-compose.app.yml down
docker compose -f docker-compose.app.yml build --no-cache igny8_frontend
docker compose -f docker-compose.app.yml up -d

Prevention: Use --no-cache flag when rebuilding after major changes


Pitfall 4: Plan Selection Issues in Pricing Page

Symptoms:

  • Monthly/Annual toggle missing
  • Pre-selected plan not highlighted
  • Discount calculation wrong

Root Cause:

  1. PricingTable component missing showToggle prop
  2. Backend missing is_featured and annual_discount_percent fields
  3. Frontend not calculating annual price from discount

Solution:

  1. Add fields to Plan model with migration
  2. Pass annualDiscountPercent to PricingTable
  3. Calculate: annualPrice = monthlyPrice * 12 * (1 - discount/100)

Files Modified:

  • backend/igny8_core/auth/models.py
  • backend/igny8_core/auth/serializers.py
  • frontend/src/services/billing.api.ts
  • frontend/src/components/ui/pricing-table/PricingTable.tsx

Pitfall 5: Adjacent JSX Elements Error

Symptoms:

  • "Adjacent JSX elements must be wrapped in an enclosing tag"
  • Build fails but line numbers don't help

Root Cause: Mismatched opening/closing tags (usually missing </div>)

Debugging Strategy:

  1. Use TypeScript compiler: npx tsc --noEmit <file>
  2. Count opening vs closing tags: grep -c "<div" vs grep -c "</div>"
  3. Check conditionals have matching closing parens/braces

Common Pattern:

{condition && (
  <div>
    {/* Content */}
  </div>
  {/* Missing closing parenthesis causes "adjacent elements" error */}
}

Solution: Ensure every opening bracket has matching close bracket


Best Practices Summary

State Management

DO: Immediately persist auth tokens synchronously
DO: Selectively remove localStorage keys
DO: Reset dependent stores on logout
DON'T: Use localStorage.clear()
DON'T: Rely solely on Zustand persist middleware timing

Error Handling

DO: Check authentication errors BEFORE throwing
DO: Force logout on invalid tokens
DO: Redirect to login after logout
DON'T: Throw errors before checking auth status
DON'T: Leave invalid tokens in storage

Component Architecture

DO: Lazy-load data at component level
DO: Skip unnecessary data loading (hideSiteSector pattern)
DO: Keep components in Router context
DON'T: Load data globally in AppLayout
DON'T: Use Router hooks outside Router context

Permission System

DO: Implement bypass at ALL permission layers
DO: Include system accounts in bypass checks
DO: Use consistent bypass logic everywhere
DON'T: Forget middleware layer bypass
DON'T: Mix permission approaches

Docker Builds

DO: Use --no-cache after major changes
DO: Restart containers after rebuilds
DO: Check logs for module loading errors
DON'T: Trust build cache after git commits
DON'T: Deploy without testing fresh build


Quick Reference: File Locations

Authentication

  • Token handling: frontend/src/services/api.ts
  • Auth store: frontend/src/store/authStore.ts
  • Middleware: backend/igny8_core/auth/middleware.py

Permissions

  • Permission classes: backend/igny8_core/api/permissions.py
  • Base viewsets: backend/igny8_core/api/base.py
  • Validation utils: backend/igny8_core/auth/utils.py

Site/Sector

  • Site store: frontend/src/store/siteStore.ts
  • Sector store: frontend/src/store/sectorStore.ts
  • PageHeader: frontend/src/components/common/PageHeader.tsx

Billing

  • Billing API: frontend/src/services/billing.api.ts
  • Plans page: frontend/src/pages/account/PlansAndBillingPage.tsx
  • Plan model: backend/igny8_core/auth/models.py

End of Knowledge Base
Update this document when architectural patterns change or new common issues are discovered.