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
This commit is contained in:
@@ -0,0 +1,388 @@
|
||||
# LLD — Architecture (B0)
|
||||
|
||||
**Scope:** FastAPI backend — internal structure, request lifecycle, auth flows, module dependencies
|
||||
**Companion doc:** `docs/backend/00_project_overview.md`
|
||||
|
||||
---
|
||||
|
||||
## Diagram 1 — FastAPI Application Layer Structure
|
||||
|
||||
How the files inside `app/` relate to each other. Arrows show import/dependency direction.
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph Entry["Entry Point"]
|
||||
MAIN["main.py\nFastAPI() instance\nMiddleware registration\nRouter registration"]
|
||||
end
|
||||
|
||||
subgraph Config["Configuration"]
|
||||
CFG["config.py\nSettings (pydantic-settings)\nReads .env"]
|
||||
DB_MOD["database.py\nasync engine\nAsyncSession factory\nget_db()"]
|
||||
end
|
||||
|
||||
subgraph Deps["Shared Dependencies"]
|
||||
DEPMOD["dependencies.py\nget_db()\nget_current_user()\nrequire_role(*roles)"]
|
||||
end
|
||||
|
||||
subgraph Routers["Routers — /api/v1/"]
|
||||
R_AUTH["auth.py\n/auth/login\n/auth/refresh\n/auth/logout"]
|
||||
R_USERS["users.py\n/users/me\n/users/{id}"]
|
||||
R_PROP["properties.py\n/properties\n/properties/{id}"]
|
||||
R_MEET["meetings.py\n/meetings\n/meetings/{id}"]
|
||||
R_VEND["vendors.py\n/vendors\n/vendors/{id}"]
|
||||
R_PROJ["projects.py\n/projects\n/projects/{id}/tasks"]
|
||||
R_INV["invoices.py\n/invoices\n/invoices/{id}/approve"]
|
||||
R_DOC["documents.py\n/documents\n/documents/{id}"]
|
||||
R_CHAT["chatbot.py\n/chatbot/message"]
|
||||
end
|
||||
|
||||
subgraph Services["Service Layer (Business Logic)"]
|
||||
S_AUTH["auth_service.py\nverify_password()\ncreate_tokens()\nrotate_refresh_token()"]
|
||||
S_PROP["property_service.py\nget_properties()\nassign_agent()\nmark_contacted()"]
|
||||
S_MEET["meeting_service.py\nschedule()\nchange_status()\nrequest_change()"]
|
||||
S_VEND["vendor_service.py\ncheck_compliance()\nupdate_coi()"]
|
||||
S_NOTIF["notification_service.py\nsend_email()\ncreate_notification()"]
|
||||
S_CHAT["chatbot_service.py\nbuild_context(role)\nproxy_to_groq()"]
|
||||
end
|
||||
|
||||
subgraph Models["ORM Models (SQLAlchemy)"]
|
||||
M_ALL["user.py · property.py\nmeeting.py · vendor.py\nproject.py · invoice.py\ndocument.py · audit_log.py"]
|
||||
end
|
||||
|
||||
subgraph Schemas["Pydantic Schemas (Request/Response)"]
|
||||
SCH_ALL["user.py · property.py\nmeeting.py · vendor.py\nauth.py · invoice.py"]
|
||||
end
|
||||
|
||||
subgraph Utils["Utilities"]
|
||||
U_SEC["security.py\njwt_encode / jwt_decode\nbcrypt helpers"]
|
||||
U_PERM["permissions.py\nrole_can_access()"]
|
||||
U_MASK["mask.py\nmask_ssn() · mask_bank()"]
|
||||
end
|
||||
|
||||
MAIN --> CFG
|
||||
MAIN --> DB_MOD
|
||||
MAIN --> DEPMOD
|
||||
MAIN --> R_AUTH & R_USERS & R_PROP & R_MEET & R_VEND & R_PROJ & R_INV & R_DOC & R_CHAT
|
||||
|
||||
R_AUTH --> DEPMOD --> S_AUTH
|
||||
R_PROP --> DEPMOD --> S_PROP
|
||||
R_MEET --> DEPMOD --> S_MEET
|
||||
R_VEND --> DEPMOD --> S_VEND
|
||||
R_CHAT --> DEPMOD --> S_CHAT
|
||||
S_NOTIF --> S_PROP & S_MEET & S_VEND
|
||||
|
||||
S_AUTH & S_PROP & S_MEET & S_VEND & S_PROJ --> M_ALL
|
||||
R_AUTH & R_PROP & R_MEET --> SCH_ALL
|
||||
S_AUTH --> U_SEC
|
||||
DEPMOD --> U_PERM
|
||||
R_USERS --> U_MASK
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Diagram 2 — HTTP Request Lifecycle
|
||||
|
||||
Every request follows this exact path through the backend. Each step is a named file/function.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant C as Client (React)
|
||||
participant CORS as CORS Middleware
|
||||
participant RL as Rate Limiter
|
||||
participant LOG as Request Logger
|
||||
participant RT as Router (e.g. properties.py)
|
||||
participant DEP as Dependencies
|
||||
participant SVC as Service Layer
|
||||
participant ORM as SQLAlchemy ORM
|
||||
participant DB as PostgreSQL
|
||||
|
||||
C->>CORS: HTTP Request
|
||||
Note over CORS: Check Origin header<br/>against ALLOWED_ORIGINS
|
||||
CORS-->>C: 403 if origin not allowed
|
||||
CORS->>RL: Pass through
|
||||
Note over RL: Check rate limit<br/>by IP / user ID
|
||||
RL-->>C: 429 if exceeded
|
||||
RL->>LOG: Pass through
|
||||
Note over LOG: Log: method, path,<br/>request_id, timestamp
|
||||
LOG->>RT: Route matched
|
||||
|
||||
Note over RT,DEP: FastAPI resolves Depends()
|
||||
RT->>DEP: get_db() → yields AsyncSession
|
||||
RT->>DEP: get_current_user(token)
|
||||
Note over DEP: Decode JWT<br/>Validate expiry<br/>Load user from DB
|
||||
DEP-->>RT: 401 if invalid token
|
||||
RT->>DEP: require_role("ADMIN","OWNER")
|
||||
DEP-->>RT: 403 if role mismatch
|
||||
DEP-->>RT: current_user injected
|
||||
|
||||
RT->>SVC: call service function(db, current_user, params)
|
||||
Note over SVC: Business logic<br/>Permission refinement<br/>Data transformation
|
||||
SVC->>ORM: db.execute(select(Property)...)
|
||||
ORM->>DB: SQL query
|
||||
DB-->>ORM: Rows
|
||||
ORM-->>SVC: Model instances
|
||||
SVC-->>RT: Result dict / model
|
||||
|
||||
Note over RT: Pydantic response schema<br/>serializes result
|
||||
RT-->>C: 200 { data: {...}, meta: {...} }
|
||||
Note over LOG: Log: status_code,<br/>duration_ms, response_size
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Diagram 3 — Authentication Flow
|
||||
|
||||
### 3a — Login
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
actor U as User
|
||||
participant FE as React AuthContext
|
||||
participant API as POST /auth/login
|
||||
participant SVC as auth_service.py
|
||||
participant DB as users table
|
||||
participant SEC as security.py
|
||||
|
||||
U->>FE: login(identifier, password, type)
|
||||
FE->>API: { identifier, password, type }
|
||||
API->>SVC: authenticate(identifier, password, type)
|
||||
|
||||
alt type == "customer"
|
||||
SVC->>DB: SELECT WHERE username = identifier
|
||||
else type == "employee" (FIELD_AGENT / ADMIN)
|
||||
SVC->>DB: SELECT WHERE emp_id = identifier
|
||||
else type == "employee" (OWNER / CONTRACTOR / VENDOR / SUBCONTRACTOR)
|
||||
SVC->>DB: SELECT WHERE username = identifier
|
||||
end
|
||||
|
||||
DB-->>SVC: User row (or None)
|
||||
|
||||
alt User not found
|
||||
SVC-->>API: None
|
||||
API-->>FE: 401 { detail: "Invalid credentials" }
|
||||
FE-->>U: Toast "Invalid credentials"
|
||||
else User found
|
||||
SVC->>SEC: verify_password(plain, hash)
|
||||
alt Password wrong
|
||||
SEC-->>SVC: False
|
||||
SVC-->>API: None
|
||||
API-->>FE: 401 { detail: "Invalid credentials" }
|
||||
else Password correct
|
||||
SEC-->>SVC: True
|
||||
SVC->>SEC: create_access_token(user.id, user.role)
|
||||
SVC->>SEC: create_refresh_token(user.id)
|
||||
SVC->>DB: UPDATE users SET refresh_token_hash = ?, last_login_at = NOW()
|
||||
SVC-->>API: { access_token, refresh_token, user }
|
||||
API-->>FE: Set httpOnly cookie (access + refresh)\n200 { user: { id, name, role, ... } }
|
||||
FE-->>U: Redirect to role portal
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
### 3b — Authenticated Request
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant FE as React
|
||||
participant API as Any Protected Endpoint
|
||||
participant DEP as get_current_user()
|
||||
participant SEC as security.py
|
||||
participant DB as users table
|
||||
|
||||
FE->>API: GET /properties\nCookie: access_token=<jwt>
|
||||
|
||||
API->>DEP: Depends(get_current_user)
|
||||
DEP->>DEP: Extract token from cookie / Authorization header
|
||||
|
||||
alt No token present
|
||||
DEP-->>API: HTTPException 401
|
||||
API-->>FE: 401 UNAUTHORIZED
|
||||
end
|
||||
|
||||
DEP->>SEC: jwt_decode(token, SECRET_KEY)
|
||||
|
||||
alt Token expired
|
||||
SEC-->>DEP: JWTError (ExpiredSignatureError)
|
||||
DEP-->>API: HTTPException 401 "Token expired"
|
||||
API-->>FE: 401 → FE triggers refresh flow
|
||||
else Token invalid (tampered)
|
||||
SEC-->>DEP: JWTError
|
||||
DEP-->>API: HTTPException 401 "Invalid token"
|
||||
else Token valid
|
||||
SEC-->>DEP: { sub: user_id, role: "FIELD_AGENT", exp: ... }
|
||||
DEP->>DB: SELECT WHERE id = user_id AND deleted_at IS NULL
|
||||
DB-->>DEP: User row
|
||||
DEP-->>API: current_user injected
|
||||
API->>API: require_role check
|
||||
API-->>FE: 200 with data
|
||||
end
|
||||
```
|
||||
|
||||
### 3c — Token Refresh
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant FE as React (axios interceptor)
|
||||
participant API as POST /auth/refresh
|
||||
participant SVC as auth_service.py
|
||||
participant DB as users table
|
||||
participant SEC as security.py
|
||||
|
||||
Note over FE: Receives 401 "Token expired"
|
||||
FE->>API: POST /auth/refresh\nCookie: refresh_token=<jwt>
|
||||
|
||||
API->>SVC: refresh_access_token(refresh_token)
|
||||
SVC->>SEC: jwt_decode(refresh_token)
|
||||
|
||||
alt Refresh token invalid/expired
|
||||
SEC-->>SVC: JWTError
|
||||
SVC-->>API: Raise 401
|
||||
API-->>FE: 401 → FE calls logout(), redirect to /login
|
||||
else Valid
|
||||
SEC-->>SVC: { sub: user_id }
|
||||
SVC->>DB: SELECT refresh_token_hash WHERE id = user_id
|
||||
SVC->>SEC: verify(incoming_token, stored_hash)
|
||||
|
||||
alt Hash mismatch (token reuse attack)
|
||||
SEC-->>SVC: False
|
||||
SVC->>DB: UPDATE users SET refresh_token_hash = NULL (revoke all)
|
||||
SVC-->>API: 401 "Refresh token reuse detected"
|
||||
API-->>FE: 401 → Force logout
|
||||
else Hash matches
|
||||
SVC->>SEC: create_access_token(user_id, role)
|
||||
SVC->>SEC: create_refresh_token(user_id)
|
||||
SVC->>DB: UPDATE users SET refresh_token_hash = new_hash
|
||||
SVC-->>API: { new_access_token, new_refresh_token }
|
||||
API-->>FE: Set new cookies\n200 OK
|
||||
FE->>FE: Retry original request with new token
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Diagram 4 — RBAC Middleware Decision Tree
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
REQ["Incoming Request"] --> HAS_TOKEN{Token present\nin cookie\nor header?}
|
||||
|
||||
HAS_TOKEN -->|No| R401_A["401 UNAUTHORIZED\nNo authentication provided"]
|
||||
|
||||
HAS_TOKEN -->|Yes| DECODE{Decode JWT\nvalid signature?}
|
||||
DECODE -->|No / tampered| R401_B["401 UNAUTHORIZED\nInvalid token"]
|
||||
DECODE -->|Yes| EXPIRED{Token\nexpired?}
|
||||
|
||||
EXPIRED -->|Yes| R401_C["401 UNAUTHORIZED\nToken expired\n→ client should refresh"]
|
||||
|
||||
EXPIRED -->|No| LOAD_USER{User exists\nin DB and\nis_active?}
|
||||
LOAD_USER -->|No| R401_D["401 UNAUTHORIZED\nUser not found or deactivated"]
|
||||
|
||||
LOAD_USER -->|Yes| HAS_ROLE_CHECK{Route has\nrequire_role()?}
|
||||
HAS_ROLE_CHECK -->|No - public route| PASS["✅ Proceed to handler\ncurrent_user = None"]
|
||||
|
||||
HAS_ROLE_CHECK -->|Yes| ROLE_MATCH{user.role\nin allowed_roles?}
|
||||
ROLE_MATCH -->|No| R403["403 FORBIDDEN\nInsufficient permissions\nfor this resource"]
|
||||
ROLE_MATCH -->|Yes| HANDLER["✅ Proceed to handler\ncurrent_user = User object"]
|
||||
|
||||
HANDLER --> SERVICE["Service layer\nfurther scoping\n(e.g. agent only sees own meetings)"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Diagram 5 — Module Build Dependency Graph
|
||||
|
||||
The recommended order to build backend modules. Each module depends on the ones above it.
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
B0["B0\nProject Overview\n& Conventions"]
|
||||
B2["B2\nDatabase Schema\n(All ORM models)"]
|
||||
B1["B1\nAuthentication\n(JWT, RBAC, all 6 roles)"]
|
||||
B3["B3\nProperties Module\n(CRUD, geospatial, leads)"]
|
||||
B4["B4\nUsers & People\n(profiles, masking)"]
|
||||
B5["B5\nMeetings & Scheduling\n(status machine, change requests)"]
|
||||
B6["B6\nVendors & Compliance\n(COI, W9, expiry alerts)"]
|
||||
B7["B7\nFinancials\n(invoices, AR/AP, payouts)"]
|
||||
B8["B8\nChatbot & AI\n(RBAC context, Groq proxy)"]
|
||||
B9["B9\nNotifications\n(email, in-app, alerts)"]
|
||||
|
||||
B0 --> B2
|
||||
B2 --> B1
|
||||
B1 --> B3
|
||||
B1 --> B4
|
||||
B3 --> B5
|
||||
B4 --> B5
|
||||
B5 --> B6
|
||||
B3 --> B7
|
||||
B6 --> B7
|
||||
B3 & B4 & B5 & B6 & B7 --> B8
|
||||
B5 & B6 & B7 --> B9
|
||||
|
||||
style B0 fill:#1e3a5f,color:#fff
|
||||
style B2 fill:#1e3a5f,color:#fff
|
||||
style B1 fill:#1e5f3a,color:#fff
|
||||
style B3 fill:#3a1e5f,color:#fff
|
||||
style B4 fill:#3a1e5f,color:#fff
|
||||
style B5 fill:#5f3a1e,color:#fff
|
||||
style B6 fill:#5f3a1e,color:#fff
|
||||
style B7 fill:#5f1e3a,color:#fff
|
||||
style B8 fill:#1e5f5f,color:#fff
|
||||
style B9 fill:#5f5f1e,color:#fff
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Diagram 6 — Chatbot RBAC Context Builder
|
||||
|
||||
How the chatbot service decides what data to inject into the LLM prompt based on the requesting user's role.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
REQ["POST /chatbot/message\n{ message, role }"] --> AUTH["Validate JWT\nExtract role"]
|
||||
|
||||
AUTH --> ROLE{user.role?}
|
||||
|
||||
ROLE -->|FIELD_AGENT| FA_CTX["Build Agent Context\n• Today's meetings\n• Assigned properties\n• Personal XP / streak\n• Top 5 hot leads in territory"]
|
||||
|
||||
ROLE -->|ADMIN| ADM_CTX["Build Admin Context\n• Unassigned leads count\n• Today's schedule (all agents)\n• Pending signatures\n• Revenue MTD"]
|
||||
|
||||
ROLE -->|OWNER| OWN_CTX["Build Owner Context\n• Project health scores\n• Overdue invoices\n• Compliance alerts\n• Revenue vs budget"]
|
||||
|
||||
ROLE -->|CONTRACTOR| CON_CTX["Build Contractor Context\n• Assigned projects\n• Pending tasks\n• Open change orders"]
|
||||
|
||||
ROLE -->|VENDOR| VEN_CTX["Build Vendor Context\n• Open orders\n• Compliance doc expiry\n• Payment status"]
|
||||
|
||||
ROLE -->|CUSTOMER| CUS_CTX["Build Customer Context\n• Own property details\n• Upcoming meetings\n• Service history"]
|
||||
|
||||
FA_CTX & ADM_CTX & OWN_CTX & CON_CTX & VEN_CTX & CUS_CTX --> PROMPT["Assemble system prompt\nRole context + user message"]
|
||||
|
||||
PROMPT --> GROQ["POST Groq API\nqwen-32b\nstream=true"]
|
||||
|
||||
GROQ --> AUDIT["Write audit_log\nactor_id, action=chatbot.query\nredacted message"]
|
||||
|
||||
AUDIT --> STREAM["Stream response\nback to client"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Diagram 7 — Environment Configuration Flow
|
||||
|
||||
How environment variables flow from `.env` into every part of the app.
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
ENV_FILE[".env file\n(never committed)"]
|
||||
ENV_EXAMPLE[".env.example\n(committed template)"]
|
||||
|
||||
ENV_FILE -->|"pydantic-settings\nBaseSettings"| SETTINGS["config.py\nSettings singleton\nsettings.database_url\nsettings.secret_key\nsettings.groq_api_key\n..."]
|
||||
|
||||
SETTINGS -->|"settings.database_url"| DB_MOD["database.py\ncreate_async_engine(url)"]
|
||||
SETTINGS -->|"settings.secret_key"| SEC["security.py\njwt_encode/decode"]
|
||||
SETTINGS -->|"settings.groq_api_key"| CHAT_SVC["chatbot_service.py\nhttpx.post(headers={api_key})"]
|
||||
SETTINGS -->|"settings.backend_cors_origins"| CORS_MW["main.py\nCORSMiddleware(allow_origins=...)"]
|
||||
SETTINGS -->|"settings.aws_*"| S3_SVC["document upload\nboto3.client(s3)"]
|
||||
SETTINGS -->|"settings.sendgrid_api_key"| NOTIF_SVC["notification_service.py\nSendGrid client"]
|
||||
|
||||
ENV_EXAMPLE -.->|"documents expected vars\n(no real values)"| ENV_FILE
|
||||
```
|
||||
Reference in New Issue
Block a user