# 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. ```mermaid 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. ```mermaid 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 (I1–I8) goes through. No module skips phases. ```mermaid 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 I1–I8 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 I1–I8. ```mermaid 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. ```mermaid 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= 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. ```mermaid 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. ```mermaid 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. ```mermaid 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= ← 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 ```