Files
LynkedUpPro_CRM/docs/diagrams/lld_i0_integration.md
T
Satyam 7694788387 feat(ProCanvas): retro game overhaul with log actions, challenges & achievements modals
- Complete ProCanvas redesign with retro sports game aesthetic
- Card-game style photo frame with tilt, shimmer, corner star accents
- Daily Missions card with weekly challenge + individual quest progress bars
- Log Action card (Door Knocked, Lead Gained, Appointment Set, Client Meeting)
- Each log action opens themed modal with relevant input fields
- Challenges modal with 6 active challenges and progress tracking
- Achievements modal with all badges, unlock status, descriptions
- Nav bar tabs (Leaderboard, Challenges, Achievements) wired to modals
- Rewards & Checkpoints with named stages (Daily Grind to Legend Run)
- Smoother Hot Streak pulse animation (2.5s float + 3s pulse rings)
- Hit The Map button with smooth pulsing glow animation
- Grid pattern overlay in Leaderboard card
- Light mode support via dark: Tailwind variants throughout
- Top 3 badges displayed on profile card
- Fixed dropdown option visibility in dark mode
2026-02-26 01:36:36 +05:30

14 KiB
Raw Blame History

LLD — I0 Integration Overview

Scope: Frontend API layer architecture, 401 interceptor, mock-to-real migration, error handling, environment config Companion doc: docs/integration/00_integration_overview.md


Diagram 1 — Frontend API Layer Architecture

How the files in src/api/ relate to each other, to the React components, and to the backend.

graph TD
    subgraph Components["React Components / Pages"]
        C1["OwnerSnapshot.jsx"]
        C2["AdminDashboard.jsx"]
        C3["AgentDashboard.jsx"]
        C4["Map.jsx"]
        C5["VendorDashboard.jsx"]
        C6["Chatbot.jsx"]
    end

    subgraph Hooks["Custom Hooks (src/hooks/)"]
        H1["useProperties()"]
        H2["useMeetings()"]
        H3["useUsers()"]
        H4["useVendors()"]
        H5["useInvoices()"]
    end

    subgraph ApiLayer["API Layer (src/api/)"]
        CLIENT["client.js\naxios.create({\n  baseURL: VITE_API_URL,\n  withCredentials: true\n})"]
        INTER["interceptors.js\nResponse interceptor:\n401 → refresh → retry\nfailure → logout event"]
        AUTH_API["auth.js\nlogin()\nlogout()\ngetMe()"]
        PROP_API["properties.js\ngetProperties()\ngetProperty()\nassignAgent()"]
        MEET_API["meetings.js\ngetMeetings()\ncreateMeeting()\nupdateMeeting()"]
        VEND_API["vendors.js\ngetVendors()\nuploadComplianceDoc()"]
        INV_API["invoices.js\ngetInvoices()\napproveInvoice()"]
        CHAT_API["chatbot.js\nsendMessage() ← streaming"]
    end

    subgraph Config["Config (src/config/)"]
        ENV["env.js\nAPI_URL\nFEATURE_REAL_API\nGROQ_API_KEY"]
    end

    subgraph Backend["FastAPI Backend"]
        BE["/api/v1/*"]
    end

    C1 & C2 & C3 --> H1 & H2 & H3
    C4 --> H1
    C5 --> H4
    C6 --> CHAT_API

    H1 --> PROP_API
    H2 --> MEET_API
    H3 --> AUTH_API
    H4 --> VEND_API
    H5 --> INV_API

    AUTH_API & PROP_API & MEET_API & VEND_API & INV_API & CHAT_API --> CLIENT
    INTER -->|"wraps"| CLIENT
    CLIENT --> ENV

    CLIENT -->|"HTTP + cookies"| BE

    style CLIENT fill:#1e3a5f,color:#fff
    style INTER fill:#1e5f3a,color:#fff
    style ENV fill:#5f3a1e,color:#fff

Diagram 2 — 401 Interceptor State Machine

The queue-based pattern that handles concurrent requests all expiring at the same time. Only one refresh call is ever made.

stateDiagram-v2
    [*] --> Idle : App initialised\ninterceptors.js imported

    Idle --> RequestInFlight : Any API call made

    RequestInFlight --> Success200 : Response 2xx
    Success200 --> Idle : Return response to caller

    RequestInFlight --> Error401 : Response 401

    Error401 --> AlreadyRetried : originalRequest._retry == true
    AlreadyRetried --> PropagateError : Reject — do not loop

    Error401 --> RefreshInProgress : isRefreshing == true\n(another request is already refreshing)
    RefreshInProgress --> Queued : Push {resolve, reject}\ninto failedQueue

    Error401 --> StartRefresh : isRefreshing == false
    StartRefresh --> RefreshCall : Set _retry=true\nSet isRefreshing=true\nPOST /auth/refresh

    RefreshCall --> RefreshSuccess : 200 — new cookies set\nby backend
    RefreshSuccess --> DrainQueue : processQueue(null)\nresume all queued requests
    DrainQueue --> RetryOriginal : apiClient(originalRequest)
    RetryOriginal --> Success200

    RefreshCall --> RefreshFailed : 401 — refresh token\ninvalid/expired/reused
    RefreshFailed --> DrainQueueError : processQueue(error)\nreject all queued requests
    DrainQueueError --> DispatchLogout : window.dispatchEvent(\n'auth:logout-required'\n)
    DispatchLogout --> ForceLogout : AuthContext listener\ncalls logout()\nredirects /login
    ForceLogout --> [*]

    note right of Queued
        Multiple components race
        to refresh. First one wins,
        others wait here.
    end note

    note right of RefreshFailed
        Refresh token expired
        or reuse attack detected.
        User must log in again.
    end note

Diagram 3 — Mock-to-Real Migration: Module Lifecycle

The three phases every integration module (I1I8) goes through. No module skips phases.

flowchart LR
    subgraph Phase1["Phase 1 — Feature Flagged (Default OFF)"]
        P1_ENV["VITE_FEATURE_REAL_API=false\nin .env.local"]
        P1_HOOK["useProperties() hook created\nwith flag branch:\nif (!FEATURE_REAL_API)\n  return mockStore data"]
        P1_TEST["All existing tests pass\nApp behaves identically\nto before integration started"]

        P1_ENV --> P1_HOOK --> P1_TEST
    end

    subgraph Phase2["Phase 2 — Real API Active"]
        P2_ENV["VITE_FEATURE_REAL_API=true\nin .env.local"]
        P2_BACKEND["Backend module running\n(e.g. B3 Properties)"]
        P2_TEST["QA: real data renders\nloading/error states work\nrole scoping correct"]

        P2_ENV --> P2_BACKEND --> P2_TEST
    end

    subgraph Phase3["Phase 3 — Mock Removed"]
        P3_CLEAN["Delete if (!FEATURE_REAL_API)\nbranch from hook"]
        P3_FLAG["Remove VITE_FEATURE_REAL_API\nfrom env files"]
        P3_MOCK["MockStoreProvider removal\nonly when ALL I1I8 complete"]

        P3_CLEAN --> P3_FLAG --> P3_MOCK
    end

    Phase1 -->|"backend module\nbecomes available"| Phase2
    Phase2 -->|"QA signed off"| Phase3

    style Phase1 fill:#1e3a5f,color:#fff
    style Phase2 fill:#1e5f3a,color:#fff
    style Phase3 fill:#5f1e3a,color:#fff

Diagram 4 — Feature Flag Decision Tree (Inside Each Hook)

The exact conditional logic inside every useXxx() hook created by I1I8.

flowchart TD
    HOOK["useProperties(filters)\nor any useXxx() hook called"]

    HOOK --> FLAG{FEATURE_REAL_API\n== true?}

    FLAG -->|false — mock path| MOCK_STORE["useMockStore()\nRead from MockStoreProvider\nApply filters in JS"]
    MOCK_STORE --> MOCK_SHAPE["Return mock-shaped object:\n{ data: [...], loading: false, error: null }"]

    FLAG -->|true — real API path| EFFECT["useEffect(() => {\n  fetchData()\n  return () => { cancelled = true }\n}, [deps])"]

    EFFECT --> LOADING["setLoading(true)\nsetError(null)"]
    LOADING --> API_CALL["await getProperties(filters)\nsrc/api/properties.js"]

    API_CALL --> OK{"Response\nok?"}
    OK -->|yes| SET_DATA["setData(result.data)\nsetLoading(false)"]
    OK -->|no| SET_ERROR["setError(\n  err.response?.data?.detail\n  ?? 'Failed to load'\n)\nsetLoading(false)"]

    SET_DATA --> REAL_SHAPE["Return real object:\n{ data: [...], loading: false, error: null }"]
    SET_ERROR --> ERROR_SHAPE["Return error object:\n{ data: [], loading: false, error: 'message' }"]

    subgraph ShapeContract["Shape Contract — Must Match"]
        MOCK_SHAPE2["Both paths return:\n{ data: T[], loading: boolean, error: string | null }\n\nComponents never know which path is active"]
    end

Diagram 5 — Frontend Request Lifecycle

The complete path from a user action in a component all the way to the backend and back, including what each layer is responsible for.

sequenceDiagram
    actor User as 👤 User
    participant Comp as React Component
    participant Hook as useXxx() Hook
    participant ApiFile as src/api/properties.js
    participant Client as apiClient (client.js)
    participant Inter as interceptors.js
    participant BE as FastAPI /api/v1

    User->>Comp: Interacts (navigate / click / filter)
    Comp->>Hook: useProperties({ status: 'Hot Lead' })
    Note over Hook: checks FEATURE_REAL_API flag

    Hook->>ApiFile: getProperties({ status: 'Hot Lead' })
    ApiFile->>Client: apiClient.get('/properties', { params })
    Note over Client: Attaches withCredentials=true\nbrowser sends httpOnly cookie automatically

    Client->>Inter: Request passes through interceptor
    Note over Inter: Response interceptor registered\n(not triggered yet — on response)

    Client->>BE: GET /api/v1/properties?status=Hot+Lead\nCookie: access_token=<jwt>

    Note over BE: Middleware: CORS → Rate Limit → Logger\nDependencies: get_current_user() → require_role()\nService: property_service.get_properties(role, filters)

    BE-->>Client: 200 { data: [...], meta: { total, page, page_size } }
    Client-->>Inter: Response passes through interceptor\n(2xx → pass through unchanged)
    Inter-->>ApiFile: { data: [...], meta: {...} }

    Note over ApiFile: Optional: transform snake_case → camelCase\nassigned_agent_id → agentId
    ApiFile-->>Hook: Transformed data object
    Hook-->>Comp: { data: [...], loading: false, error: null }
    Comp-->>User: Render updated UI

Diagram 6 — HTTP Error Handling Decision Tree

How the frontend responds to each HTTP error code the backend can return.

flowchart TD
    RES["API response received\nerror.response.status"]

    RES --> S400{400\nBad Request}
    S400 --> H400["Validation error\n(bad request body)\nShow field-level errors\nfrom error.response.data.detail"]

    RES --> S401{401\nUnauthorized}
    S401 --> RETRY{originalRequest\n._retry?}
    RETRY -->|false — first 401| REFRESH["Trigger token refresh\nPOST /auth/refresh\n(interceptors.js queue)"]
    REFRESH --> RFAIL{refresh\nsucceeded?}
    RFAIL -->|yes| RETR["Retry original request\nUser sees nothing"]
    RFAIL -->|no| LOGOUT["dispatch('auth:logout-required')\nAuthContext.logout()\nRedirect /login"]
    RETRY -->|true — already retried| LOGOUT

    RES --> S403{403\nForbidden}
    S403 --> H403["User authenticated but wrong role\nShow 'Access denied' toast\nor redirect to own portal\n(do not retry)"]

    RES --> S404{404\nNot Found}
    S404 --> H404["Resource does not exist\nShow empty state or\n'Not found' message"]

    RES --> S422{422\nUnprocessable Entity}
    S422 --> H422["FastAPI schema validation failed\nShow detail array from\nerror.response.data.detail[]\nHighlight failing fields"]

    RES --> S429{429\nRate Limited}
    S429 --> H429["Show toast 'Too many requests'\nBackoff — do not auto-retry\nUser must wait"]

    RES --> S500{500\nServer Error}
    S500 --> H500["Show generic error toast\n'Something went wrong'\nLog error.response.data to console\n(dev only — never to user)"]

    subgraph ErrorShape["All error bodies follow this shape"]
        ES["{\n  detail: 'Human-readable message',\n  code: 'MACHINE_CODE'\n}\n\n422 uses FastAPI default:\n{\n  detail: [\n    { loc: ['body','field'], msg: '...', type: '...' }\n  ]\n}"]
    end

Diagram 7 — Frontend Environment Variable Flow

How VITE_API_URL travels from .env.local through the build system into every API call.

graph LR
    subgraph Files["Source Files"]
        ENV_LOCAL[".env.local\n(never committed)\nVITE_API_URL=http://localhost:8000/api/v1"]
        ENV_PROD[".env.production\n(Vercel env vars)\nVITE_API_URL=https://api.lynkeduppro.com/api/v1"]
        ENV_EXAMPLE[".env.example\n(committed template)\nVITE_API_URL="]
    end

    subgraph Build["Vite Build Process"]
        VITE["Vite reads .env.local\nDuring vite dev / vite build\nOnly VITE_ prefix exposed to bundle\nAll others stripped"]
    end

    subgraph Runtime["Browser Runtime"]
        META["import.meta.env.VITE_API_URL\n(string | undefined)\nbaked into JS bundle at build time"]
    end

    subgraph Config["src/config/env.js"]
        EXPORT["export const API_URL =\n  import.meta.env.VITE_API_URL\n  ?? 'http://localhost:8000/api/v1'\n\nexport const FEATURE_REAL_API =\n  import.meta.env.VITE_FEATURE_REAL_API === 'true'\n\nexport const GROQ_API_KEY =\n  import.meta.env.VITE_GROQ_API_KEY"]
    end

    subgraph ApiClient["src/api/client.js"]
        CLIENT["axios.create({\n  baseURL: API_URL,\n  withCredentials: true\n})"]
    end

    ENV_LOCAL -->|"vite dev"| VITE
    ENV_PROD -->|"vite build (Vercel)"| VITE
    ENV_EXAMPLE -.->|"template for"| ENV_LOCAL

    VITE --> META
    META --> EXPORT
    EXPORT -->|"API_URL"| CLIENT
    EXPORT -->|"FEATURE_REAL_API"| HOOKS["All useXxx() hooks\nmock vs real branch"]
    EXPORT -->|"GROQ_API_KEY"| CHATBOT["Chatbot.jsx\n(pre-integration direct calls)"]

    subgraph Warning["⚠️ Security Note"]
        W1["VITE_ vars are PUBLIC\nThey appear in the JS bundle\nNever put SECRET_KEY, DB passwords,\nor admin tokens here\nThose belong in backend .env only"]
    end

Diagram 8 — Axios withCredentials + CORS Handshake

Why withCredentials: true is required and what happens at the browser + server level.

sequenceDiagram
    participant Browser as Browser
    participant Axios as Axios (withCredentials: true)
    participant CORS as FastAPI CORS Middleware
    participant Handler as Route Handler

    Note over Browser,Handler: Preflight (first cross-origin request)
    Browser->>CORS: OPTIONS /api/v1/properties\nOrigin: https://lynkeduppro.vercel.app\nAccess-Control-Request-Method: GET\nAccess-Control-Request-Headers: Content-Type

    Note over CORS: Check ALLOWED_ORIGINS list:\n['https://lynkeduppro.vercel.app',\n 'http://localhost:5173']
    CORS-->>Browser: 200\nAccess-Control-Allow-Origin: https://lynkeduppro.vercel.app\nAccess-Control-Allow-Credentials: true ← REQUIRED\nAccess-Control-Allow-Methods: GET, POST, PATCH, DELETE\nAccess-Control-Allow-Headers: Content-Type

    Note over Browser,Handler: Actual request (cookies attached automatically)
    Axios->>CORS: GET /api/v1/properties\nOrigin: https://lynkeduppro.vercel.app\nCookie: access_token=<jwt> ← browser attaches because withCredentials=true

    CORS->>Handler: Pass through (origin allowed)
    Handler-->>CORS: 200 { data: [...] }
    CORS-->>Browser: 200 { data: [...] }\nAccess-Control-Allow-Origin: https://lynkeduppro.vercel.app\nAccess-Control-Allow-Credentials: true

    Note over Browser: Without withCredentials: true\nthe Cookie header would not be sent\neven for same-site API on different port (localhost:8000)

    subgraph BackendConfig["Backend: main.py CORS config"]
        BC["CORSMiddleware(\n  allow_origins=settings.backend_cors_origins,\n  allow_credentials=True,  ← REQUIRED for cookies\n  allow_methods=['*'],\n  allow_headers=['*'],\n)"]
    end