# I0 — Integration Overview **Module:** I0 — Integration Team Foundation **Companion backend doc:** `docs/backend/00_project_overview.md` (B0) **Status:** Integration Team reads this first, before touching any frontend code. --- ## What This Module Covers This document establishes the shared foundation every integration module (`I1`–`I8`) will build on: 1. API client setup — Axios instance, base URL, default headers 2. Auth token strategy — httpOnly cookies, no localStorage 3. Global 401 interceptor — automatic token refresh + request retry 4. Environment variable setup 5. Loading and error state conventions 6. The mock-to-real migration pattern (how to replace mockStore piece by piece) 7. Role-redirect logic (how to stay in sync with `Login.jsx`) Read this document fully before working on any `I1`–`I8` module. --- ## 1. Environment Variables The frontend uses Vite. All client-side environment variables must be prefixed with `VITE_`. **Add to `.env.local` (never commit this file):** ```env # Backend API VITE_API_URL=http://localhost:8000/api/v1 # External (already present — do not change) VITE_GROQ_API_KEY=your_groq_key_here VITE_OPENWEATHER_API_KEY=your_openweather_key_here ``` **Add to `.env.example` (commit this as a template):** ```env # Backend API base URL VITE_API_URL=http://localhost:8000/api/v1 ``` **Access in code:** ```js // src/config/env.js (add alongside existing exports) export const API_URL = import.meta.env.VITE_API_URL ?? 'http://localhost:8000/api/v1'; ``` > The `?? fallback` prevents crashes during local dev if `.env.local` is missing. In production on Vercel, `VITE_API_URL` must be set as an environment variable in the Vercel dashboard. --- ## 2. API Client Setup Create one shared Axios instance. Every integration module imports from this file — never call `axios.create()` again elsewhere. **File to create:** `src/api/client.js` ```js import axios from 'axios'; import { API_URL } from '../config/env'; const apiClient = axios.create({ baseURL: API_URL, withCredentials: true, // sends httpOnly cookies automatically headers: { 'Content-Type': 'application/json', }, }); export default apiClient; ``` ### Why `withCredentials: true`? The backend sets the access token and refresh token as `httpOnly` cookies (see B1). The browser will not include these cookies on cross-origin requests unless `withCredentials: true` is set. This is the only auth mechanism — there is no `Authorization: Bearer` header and no `localStorage`. ### What NOT to do ```js // ❌ Never do this localStorage.setItem('access_token', token); axios.defaults.headers.common['Authorization'] = `Bearer ${token}`; // ✅ Correct — the cookie is set by the backend, browser sends it automatically ``` --- ## 3. Global 401 Interceptor — Token Refresh and Retry When the access token expires, the backend returns `401`. The client must silently refresh the token and retry the original request. This logic lives in one place: `src/api/interceptors.js`. **File to create:** `src/api/interceptors.js` ```js import apiClient from './client'; let isRefreshing = false; let failedQueue = []; // requests that arrived while refresh was in-flight function processQueue(error, token = null) { failedQueue.forEach(({ resolve, reject }) => { if (error) { reject(error); } else { resolve(token); } }); failedQueue = []; } apiClient.interceptors.response.use( (response) => response, async (error) => { const originalRequest = error.config; // Only handle 401s that haven't already been retried if (error.response?.status !== 401 || originalRequest._retry) { return Promise.reject(error); } // If a refresh is already in progress, queue this request if (isRefreshing) { return new Promise((resolve, reject) => { failedQueue.push({ resolve, reject }); }).then(() => apiClient(originalRequest)); } originalRequest._retry = true; isRefreshing = true; try { // POST /auth/refresh — backend rotates the cookie automatically await apiClient.post('/auth/refresh'); processQueue(null); return apiClient(originalRequest); // retry original request } catch (refreshError) { processQueue(refreshError); // Refresh failed → force logout window.dispatchEvent(new CustomEvent('auth:logout-required')); return Promise.reject(refreshError); } finally { isRefreshing = false; } } ); export default apiClient; ``` **Register interceptors in your app entry point.** Import this file once at the top of `src/main.jsx` (or `src/App.jsx`): ```js // src/main.jsx — add this import at the top import './api/interceptors'; ``` > The `failedQueue` pattern prevents a thunderstorm of refresh requests when multiple concurrent API calls all get 401 at once. Only one refresh call is made; all others wait in the queue and retry together. ### Listening for the forced-logout event In `AuthContext.jsx`, add a listener for the custom event dispatched when refresh fails: ```js // Inside AuthProvider useEffect useEffect(() => { const handleForceLogout = () => { logout(); // existing logout function }; window.addEventListener('auth:logout-required', handleForceLogout); return () => window.removeEventListener('auth:logout-required', handleForceLogout); }, []); ``` --- ## 4. API Module Files Each integration module (`I1`–`I8`) will create one `src/api/*.js` file. These are thin wrappers around `apiClient` — no business logic, no state. Example structure: ``` src/api/ ├── client.js ← Axios instance (shared) ├── interceptors.js ← 401 handler + refresh (imported once in main.jsx) ├── auth.js ← I1: login, logout, refresh, getMe ├── properties.js ← I3: getProperties, getProperty, assignAgent, updateStatus ├── users.js ← I4: getUsers, getUser, updateUser ├── meetings.js ← I5: getMeetings, createMeeting, updateMeeting, requestChange ├── vendors.js ← I6: getVendors, getVendor, uploadComplianceDoc ├── invoices.js ← I7: getInvoices, createInvoice, approveInvoice └── chatbot.js ← I8: sendMessage (streaming) ``` **Convention for every API file:** ```js // src/api/properties.js — example pattern import apiClient from './client'; export async function getProperties(params = {}) { const { data } = await apiClient.get('/properties', { params }); return data; // { data: [...], meta: { total, page, page_size } } } export async function getProperty(id) { const { data } = await apiClient.get(`/properties/${id}`); return data; } export async function assignAgent(propertyId, agentId) { const { data } = await apiClient.patch(`/properties/${propertyId}`, { assigned_agent_id: agentId, }); return data; } ``` Always destructure `{ data }` from the Axios response — the actual payload is in `response.data`, not `response` itself. --- ## 5. Loading and Error State Conventions Every component that fetches data should follow this pattern. Use React's built-in `useState` + `useEffect` — no new libraries needed at this stage. ### Standard fetch pattern ```js const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { let cancelled = false; async function fetchData() { try { setLoading(true); setError(null); const result = await getProperties({ status: 'Hot Lead' }); if (!cancelled) setData(result.data); } catch (err) { if (!cancelled) setError(err.response?.data?.detail ?? 'Failed to load data'); } finally { if (!cancelled) setLoading(false); } } fetchData(); return () => { cancelled = true; }; }, []); ``` The `cancelled` flag prevents state updates on unmounted components (React strict mode issue). ### Error shape from the backend ```js // All backend errors follow this shape (from B0): // { detail: "Human-readable message", code: "MACHINE_CODE" } // Access in catch block: const message = err.response?.data?.detail ?? 'An unexpected error occurred'; const code = err.response?.data?.code; // e.g. "PERMISSION_DENIED" // HTTP status codes: // 400 — Validation error (bad request body) // 401 — Not authenticated / token expired // 403 — Authenticated but forbidden (wrong role) // 404 — Resource not found // 422 — FastAPI schema validation failed // 429 — Rate limited // 500 — Server error ``` ### Loading UI Use the existing `LoadingSpinner` component (or the role-appropriate skeleton) that is already present in the codebase. Do not add new spinner libraries. --- ## 6. Mock-to-Real Migration Pattern The frontend currently runs entirely off `src/data/mockStore.jsx` through `MockStoreProvider`. The migration approach is **module by module** — never rip out the whole mock layer at once. ### The three phases of each module swap **Phase 1 — Feature-flagged (default OFF)** Each integration module starts with a feature flag so the mock data remains the fallback during development: ```js // src/config/env.js — add this export const FEATURE_REAL_API = import.meta.env.VITE_FEATURE_REAL_API === 'true'; ``` ```env # .env.local — set to true when you're ready to test a module VITE_FEATURE_REAL_API=false ``` **Phase 2 — Swap one data source at a time** For example, when doing I3 (Properties), update only the properties data hook: ```js // src/hooks/useProperties.js — new file created by I3 import { useState, useEffect } from 'react'; import { FEATURE_REAL_API } from '../config/env'; import { getProperties } from '../api/properties'; import { useMockStore } from '../context/MockStoreContext'; export function useProperties(filters = {}) { const mockStore = useMockStore(); if (!FEATURE_REAL_API) { // Return mock data in the same shape as the real API return { data: mockStore.properties.filter(/* apply filters */), loading: false, error: null, }; } // Real API path (only when flag is true) const [data, setData] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); // ... useEffect fetch ... return { data, loading, error }; } ``` > Each integration module's spec (I1–I8) will define the exact hook shape and what filters to support. **Phase 3 — Remove the mock branch** Once QA confirms the real API works correctly, delete the mock branch and the feature flag check. The `MockStoreProvider` can be removed only when all modules are migrated (I1 → I8 all complete). ### Shape matching rule The real API response shape must always match the mock data shape that components already consume. If the backend returns a field name differently (e.g., `agent_id` vs `agentId`), transform it in the API layer (`src/api/*.js`), not in the component. ```js // src/api/properties.js — transform snake_case to camelCase if needed export async function getProperties(params = {}) { const { data } = await apiClient.get('/properties', { params }); return { ...data, data: data.data.map((p) => ({ ...p, agentId: p.assigned_agent_id, // normalize for components marketValue: p.estimated_market_value, })), }; } ``` --- ## 7. Role → Redirect Map The backend returns a `role` field in the `/auth/me` and `/auth/login` responses. The frontend uses this to redirect users to their portal. This map must stay in sync with the switch statement in `src/pages/Login.jsx`. | Role | Redirect path | |------|--------------| | `OWNER` | `/owner/snapshot` | | `ADMIN` | `/admin/dashboard` | | `FIELD_AGENT` | `/emp/fa/dashboard` | | `CONTRACTOR` | `/contractor/dashboard` | | `SUBCONTRACTOR` | `/subcontractor/dashboard` | | `VENDOR` | `/vendor/dashboard` | | `CUSTOMER` | `/portal/profile` | These exact strings are used in `src/context/AuthContext.jsx` (the `ROLE_ROUTES` map) after I1 replaces the login flow. --- ## 8. Dependency Installation Only one new package is needed: Axios. Everything else (React Query, SWR, etc.) is explicitly out of scope for the integration phase — keep the state management approach simple and consistent with the existing codebase. ```bash npm install axios ``` Verify it was added to `package.json` `dependencies` (not `devDependencies`). --- ## 9. File Creation Checklist for I0 Before moving to I1, confirm all of the following are created or updated: | File | Action | |------|--------| | `.env.local` | Add `VITE_API_URL=http://localhost:8000/api/v1` | | `.env.example` | Add `VITE_API_URL=` (no value) | | `src/config/env.js` | Export `API_URL` and `FEATURE_REAL_API` | | `src/api/client.js` | Create Axios instance with `withCredentials: true` | | `src/api/interceptors.js` | Create 401 interceptor with queue + retry logic | | `src/main.jsx` | Add `import './api/interceptors'` at top | | `src/context/AuthContext.jsx` | Add `auth:logout-required` event listener | | `package.json` | `axios` present in `dependencies` | --- ## 10. What Each Integration Module Builds On Top of This | Module | What it adds | |--------|-------------| | **I1** — Auth Integration | Replaces `AuthContext` login/logout with real `/auth/login`, `/auth/logout`, `/auth/me` calls | | **I2** — Data Layer Migration | Wraps `MockStoreProvider` in a feature-flagged `ApiProvider`; establishes hook conventions | | **I3** — Properties | Hooks for map + property CRUD; geospatial filter params; lead status updates | | **I4** — Users & People | Hooks for `PeopleDirectory`, `VendorDirectory`; masked field handling | | **I5** — Meetings | Hooks for `AdminSchedule`, meeting status transitions, change request flow | | **I6** — Vendors & Compliance | Hooks for vendor CRUD, COI upload, compliance badge logic | | **I7** — Financial | Hooks for invoice tables, AR/AP views, payout approval flow | | **I8** — Chatbot | Streaming response from `/chatbot/message`; replaces direct Groq SDK calls | --- ## 11. Key Constraints — Do Not Violate 1. **Never store tokens in `localStorage` or `sessionStorage`.** The httpOnly cookie is set by the backend. The frontend never touches it directly. 2. **Never call `axios.create()` outside of `src/api/client.js`.** All API calls go through the shared instance so the interceptor works. 3. **Never modify `src/data/mockStore.jsx` during integration.** Mock data is the reference — it must remain intact as a fallback. 4. **Never remove `MockStoreProvider` from `main.jsx` until I2 is fully complete** and all feature flags have been validated. 5. **Money values arrive in cents from the API.** Divide by 100 before display. Never send or store display-formatted dollar values back to the API. 6. **All timestamps are ISO 8601 UTC.** Use `new Date(isoString).toLocaleDateString()` or the existing date util for display formatting — do not use raw strings. --- *Document maintained by Integration Team — mark complete in `docs/README.md` when all files in Section 9 checklist are created and verified.*