7694788387
- 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
354 lines
14 KiB
Markdown
354 lines
14 KiB
Markdown
# 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=<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.
|
||
|
||
```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=<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
|
||
```
|
||
|