-
Notifications
You must be signed in to change notification settings - Fork 52
Feat: Enhance Contracts Module: Database, API, and Frontend Updates #125
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
WalkthroughThis PR introduces a comprehensive brand dashboard system with AI-powered query routing, contract management, and creator matching. It adds new database tables, backend routes for brand profiles, campaigns, analytics, and contracts, plus frontend components and services to support dashboard visualization, AI-assisted search, and contract details viewing. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant BrandChatAssistant
participant AIApiService
participant AIRouterBackend
participant GroqLLM
participant RedisSession
User->>BrandChatAssistant: Enter query
BrandChatAssistant->>AIApiService: queryAI(query, brandId, sessionId?)
alt Has Session ID
AIApiService->>AIApiService: Add X-Session-ID header
else No Session
AIApiService->>AIApiService: Generate new session ID
end
AIApiService->>AIRouterBackend: POST /api/ai/query
AIRouterBackend->>RedisSession: get_session_state(session_id)
RedisSession-->>AIRouterBackend: previous state (if any)
AIRouterBackend->>GroqLLM: Send system prompt + user query
GroqLLM-->>AIRouterBackend: LLM response (JSON intent)
AIRouterBackend->>AIRouterBackend: Parse intent, map to route & parameters
AIRouterBackend->>AIRouterBackend: Invoke mapped API endpoint
AIRouterBackend->>RedisSession: save_session_state(session_id, context)
AIRouterBackend-->>AIApiService: AIQueryResponse (intent, route, explanation, sessionId)
AIApiService-->>BrandChatAssistant: Response with session context
BrandChatAssistant->>BrandChatAssistant: Update sessionId for next query
BrandChatAssistant->>User: Display AI response & follow-up
sequenceDiagram
participant BrandDashboard
participant useBrandDashboard
participant BrandApiService
participant Backend
rect rgb(200, 220, 240)
Note over BrandDashboard,Backend: Initial Dashboard Load
BrandDashboard->>BrandDashboard: Mount
BrandDashboard->>useBrandDashboard: useEffect call refreshData()
useBrandDashboard->>BrandApiService: getDashboardOverview(brandId)
useBrandDashboard->>BrandApiService: getBrandCampaigns(brandId)
useBrandDashboard->>BrandApiService: getCreatorMatches(brandId)
useBrandDashboard->>BrandApiService: getBrandApplications(brandId)
BrandApiService->>Backend: Parallel API calls
Backend-->>BrandApiService: Aggregated dashboard data
BrandApiService-->>useBrandDashboard: Update hook state
useBrandDashboard-->>BrandDashboard: Return loaded data
BrandDashboard->>BrandDashboard: Render dashboard UI with KPIs & metrics
end
rect rgb(240, 200, 220)
Note over BrandDashboard,Backend: AI Search Flow
BrandDashboard->>BrandDashboard: User triggers AI search
BrandDashboard->>BrandChatAssistant: Open with initialQuery
BrandChatAssistant->>useBrandDashboard: queryAI(query)
useBrandDashboard->>BrandApiService: aiApi.queryAI(query, brandId)
BrandApiService-->>useBrandDashboard: AI response + suggestions
useBrandDashboard-->>BrandChatAssistant: Update AI results
BrandChatAssistant->>BrandDashboard: Display AI response in chat
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Rationale: Large heterogeneous PR spanning 30+ files across frontend and backend with multiple new features (brand dashboard, contracts management, AI routing, session management). Includes dense business logic (AI query orchestration with Groq LLM, hybrid intent mapping, comprehensive CRUD operations), significant database schema changes, and multiple React components with state management. High variety of edits (new models, routes, services, components, hooks, SQL tables) requiring separate reasoning for each domain. Possibly related PRs
Suggested labels
Suggested reviewers
Poem
Pre-merge checks and finishing touches✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 38
🧹 Nitpick comments (51)
Backend/app/models/models.py (2)
20-21: Consider centralizing thegenerate_uuidfunction.The
generate_uuid()function is duplicated across multiple files (models.py, chat.py, post.py). Consider extracting it to a shared utility module to follow the DRY principle.For example, create a utility module:
# app/utils/id_generator.py import uuid def generate_uuid(): return str(uuid.uuid4())Then import it in models:
+from app.utils.id_generator import generate_uuid from app.db.db import Base -import uuid - - -def generate_uuid(): - return str(uuid.uuid4())
216-216: Consider using enums for status fields.The
statusfields in Contract (line 216) and other models use string literals. Consider using Python enums for type safety and preventing invalid values.Example for Contract status:
from enum import Enum class ContractStatus(str, Enum): DRAFT = "draft" SIGNED = "signed" COMPLETED = "completed" CANCELLED = "cancelled" # Then in the model: status = Column(String, default=ContractStatus.DRAFT)Also applies to: 234-234
Frontend/src/App.tsx (1)
72-81: Unify contracts route to one canonical path.You now have both
/brand/contractsand/dashboard/contracts. Prefer one and redirect the other to avoid duplicate screens and broken deep links.Apply this change outside the shown range to redirect the legacy route:
- import { BrowserRouter as Router, Routes, Route } from "react-router-dom"; + import { BrowserRouter as Router, Routes, Route, Navigate } from "react-router-dom";Replace the legacy route:
- <Route - path="/dashboard/contracts" - element={ - <ProtectedRoute> - <Contracts /> - </ProtectedRoute> - } - /> + <Route path="/dashboard/contracts" element={<Navigate to="/brand/contracts" replace />} />Frontend/src/hooks/useBrandDashboard.ts (2)
20-21: TypeaiResponseto the API contract.Use the exported
AIQueryResponsetype to avoidany.- import { aiApi } from '../services/aiApi'; + import { aiApi, AIQueryResponse } from '../services/aiApi'; ... - const [aiResponse, setAiResponse] = useState<any>(null); + const [aiResponse, setAiResponse] = useState<AIQueryResponse | null>(null);
29-37: Loading state doesn’t cover all initial fetches.
setLoadingis toggled only inloadDashboardOverview, so UI may show loaded while other requests are pending.Move loading control to the aggregate loader:
- try { - setLoading(true); - const overview = await brandApi.getDashboardOverview(brandId); - setDashboardOverview(overview); - } catch (err) { + try { + const overview = await brandApi.getDashboardOverview(brandId); + setDashboardOverview(overview); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to load dashboard overview'); - } finally { - setLoading(false); - } + }And in the effect:
useEffect(() => { if (brandId) { - Promise.all([ + setLoading(true); + setError(null); + Promise.all([ loadDashboardOverview(), loadBrandProfile(), loadCampaigns(), loadCreatorMatches(), loadApplications(), loadPayments(), - ]).catch(err => { - console.error('Error loading dashboard data:', err); - }); + ]) + .catch(err => setError(err instanceof Error ? err.message : 'Failed to load dashboard')) + .finally(() => setLoading(false)); } }, [brandId, loadDashboardOverview, loadBrandProfile, loadCampaigns, loadCreatorMatches, loadApplications, loadPayments]);Also applies to: 224-239
Backend/app/services/redis_client.py (1)
19-25: Harden session helpers.Add basic error handling to avoid crashes on Redis outages.
async def get_session_state(session_id: str): - state = await redis_client.get(f"session:{session_id}") - return json.loads(state) if state else {} + try: + state = await redis_client.get(f"session:{session_id}") + return json.loads(state) if state else {} + except Exception: + return {} async def save_session_state(session_id: str, state: dict): - await redis_client.set(f"session:{session_id}", json.dumps(state), ex=SESSION_TTL) + try: + await redis_client.set(f"session:{session_id}", json.dumps(state), ex=SESSION_TTL) + except Exception: + passFrontend/src/services/aiApi.ts (1)
4-5: Don’t hard-code API base URL.Use env vars so dev/prod work without code changes.
-const AI_API_BASE_URL = 'http://localhost:8000/api/ai'; +const AI_API_BASE_URL = + (import.meta as any)?.env?.VITE_AI_API_BASE_URL || + `${window.location.origin}/api/ai`;Frontend/src/pages/Contracts.tsx (3)
1-21: Tighten TypeScript types and drop unused imports.
- Add a
Contract/Paymentinterface and type state accordingly.- Remove unused imports:
useEffect,Filter,Calendar,Users,AlertCircle.-import React, { useState, useEffect } from 'react'; +import React, { useState } from 'react'; -import { - Plus, - Search, - Filter, - Calendar, - DollarSign, - Users, - FileText, - CheckCircle, - Clock, - AlertCircle, - TrendingUp, - Eye, - Edit, - MoreVertical, - Download, - Upload -} from 'lucide-react'; +import { Plus, Search, DollarSign, FileText, CheckCircle, Clock, TrendingUp, Eye, Edit, MoreVertical, Download, Upload } from 'lucide-react'; ... +type Payment = { amount: number; status: 'paid' | 'pending'; date: string }; +type Contract = { + id: string; title: string; creator: string; brand: string; + status: 'active'|'pending'|'draft'|'completed'; + type: 'one-time'|'ongoing'|'performance-based'; + budget: number; startDate: string; endDate: string; progress: number; + milestones: number; completedMilestones: number; + deliverables: number; completedDeliverables: number; + payments: Payment[]; +}; ... -const [contracts, setContracts] = useState(mockContracts); +const [contracts, setContracts] = useState<Contract[]>(mockContracts); -const [selectedContract, setSelectedContract] = useState(null); +const [selectedContract, setSelectedContract] = useState<Contract | null>(null);Also applies to: 89-96
356-375: Prevent card click when pressing inner action buttons.Stop propagation on “View/Edit/More” so they don’t also open the details modal.
- <button style={{ ... }}> + <button onClick={(e) => { e.stopPropagation(); /* open details */ }} style={{ ... }}> <Eye size={14} /> View </button> - <button style={{ ... }}> + <button onClick={(e) => e.stopPropagation()} style={{ ... }}> <Edit size={14} /> Edit </button> - <button style={{ ... }}> + <button onClick={(e) => e.stopPropagation()} style={{ ... }}> <MoreVertical size={14} /> </button>Also applies to: 456-499
251-257: Revenue metric label vs logic mismatch.
totalRevenuesums paid payments across all contracts but the label says “From completed contracts”.Either:
- Change the label to “From paid invoices”, or
- Filter to completed contracts:
- totalRevenue: contracts.reduce((sum, c) => { + totalRevenue: contracts + .filter(c => c.status === 'completed') + .reduce((sum, c) => { const paidPayments = c.payments.filter(p => p.status === 'paid'); return sum + paidPayments.reduce((pSum, p) => pSum + p.amount, 0); - }, 0) + }, 0)Also applies to: 101-107
Frontend/src/components/chat/BrandChatAssistant.tsx (1)
208-221: Render result JSON with preserved formatting.Current JSON.stringify collapses to one line. Use pre-wrap for readability.
- <strong>Result:</strong> {JSON.stringify(msg.result, null, 2)} + <strong>Result:</strong> + <pre style={{ whiteSpace: "pre-wrap", margin: 0 }}> + {JSON.stringify(msg.result, null, 2)} + </pre>Frontend/src/pages/Brand/DashboardOverview.tsx (2)
120-169: Add AbortController to prevent state updates after unmount.Protects against race conditions and memory leaks during rapid nav.
useEffect(() => { - const fetchDashboardData = async () => { + const controller = new AbortController(); + const fetchDashboardData = async () => { try { @@ - fetchDashboardData(); - }, [brandId]); + fetchDashboardData(); + return () => controller.abort(); + }, [brandId]);Pass { signal: controller.signal } to each fetch.
101-118: Remove unused helpers.getStatusColor/getPerformanceColor aren’t used; delete to reduce noise.
Backend/app/routes/ai_query.py (3)
9-11: Avoid configuring root logging in a library/router module.logging.basicConfig here can clobber app-level logging. Remove and rely on app config.
-# Setup logging -logging.basicConfig(level=logging.INFO) -logger = logging.getLogger(__name__) +# Setup logger +logger = logging.getLogger(__name__)
64-70: Downgrade verbose param logging to debug and avoid PII in production logs.Dumping all params (brand_id, user_id) at info can leak identifiers.
-logger.info(f"Intent: {intent}") -logger.info(f"Params: {params}") -logger.info(f"Params type: {type(params)}") -for key, value in params.items(): - logger.info(f" {key}: {value} (type: {type(value)})") +logger.debug("Intent: %s; params keys: %s", intent, list(params.keys()))
138-141: Use logger.exception and chain exceptions; avoid blind excepts.Improve traceability and lint compliance.
-except Exception as api_exc: - logger.error(f"API call failed for intent '{intent}': {api_exc}") - api_error = str(api_exc) +except Exception as api_exc: + logger.exception("API call failed for intent '%s'", intent) + api_error = str(api_exc) @@ -except Exception as e: - logger.error(f"Error processing AI query: {e}") - raise HTTPException(status_code=500, detail="Failed to process AI query") +except Exception as e: + logger.exception("Error processing AI query") + raise HTTPException(status_code=500, detail="Failed to process AI query") from e @@ -except Exception as e: - logger.error(f"Error fetching available routes: {e}") - raise HTTPException(status_code=500, detail="Failed to fetch routes") +except Exception as e: + logger.exception("Error fetching available routes") + raise HTTPException(status_code=500, detail="Failed to fetch routes") from e @@ -except Exception as e: - logger.error(f"Error fetching route info: {e}") - raise HTTPException(status_code=500, detail="Failed to fetch route info") +except Exception as e: + logger.exception("Error fetching route info") + raise HTTPException(status_code=500, detail="Failed to fetch route info") from e @@ -except Exception as e: - logger.error(f"Error in test AI query: {e}") +except Exception as e: + logger.exception("Error in test AI query")Also applies to: 183-187, 200-202, 218-222, 238-239
Frontend/src/components/contracts/ContractDetailsModal.tsx (3)
22-42: Contract interface diverges from backend Contract model; confirm mapping.Backend Contract lacks title/type/budget/dates in the provided model; ensure the consumer composes this view-model from API responses before passing in.
I can add a ContractViewModel mapper to adapt backend fields (including sponsorship/title, payments, deliverables).
535-559: Add dialog semantics for accessibility.Mark modal with role="dialog" and aria-modal, and label it for screen readers.
-<div style={{ +<div + role="dialog" + aria-modal="true" + aria-labelledby={`contract-modal-title-${contract.id}`} + style={{ @@ - <div style={{ + <div + role="document" + style={{ @@ - <h2 style={{ fontSize: '24px', fontWeight: '700', marginBottom: '4px' }}>{contract.title}</h2> + <h2 id={`contract-modal-title-${contract.id}`} style={{ fontSize: '24px', fontWeight: '700', marginBottom: '4px' }}> + {contract.title} + </h2>
560-616: Optional: support Esc/overlay click to close.Improves UX parity with common modal behavior.
Frontend/src/pages/Brand/Dashboard.tsx (1)
465-469: Use onKeyDown instead of onKeyPress (deprecated).React deprecates onKeyPress; switch to onKeyDown for Enter handling.
-onKeyPress={(e) => { +onKeyDown={(e) => { if (e.key === 'Enter' && searchQuery.trim()) { handleAISearch(); } }}Backend/app/services/ai_router.py (5)
131-138: Use explicit Optional typing for brand_id.PEP 484: avoid implicit Optional. Also helps tools.
- async def process_query(self, query: str, brand_id: str = None) -> Dict[str, Any]: + async def process_query(self, query: str, brand_id: Optional[str] = None) -> Dict[str, Any]:
167-169: Harden exception handling (log trace, chain cause).Use logging.exception and raise from to preserve stack traces.
- except Exception as e: - logger.error(f"Error processing query with AI Router: {e}") - raise HTTPException(status_code=500, detail="AI processing error") + except Exception as e: + logger.exception("Error processing query with AI Router") + raise HTTPException(status_code=500, detail="AI processing error") from e
194-197: Use timezone-aware ISO timestamps.Naive datetime can confuse clients. Prefer UTC ISO 8601.
- response["timestamp"] = str(datetime.now()) + response["timestamp"] = datetime.utcnow().isoformat() + "Z"
200-223: Regex/json cleaning is brittle for nested JSON.Your pattern may truncate nested objects/arrays. Consider a stricter extractor or require code-block JSON only.
342-343: Avoid failing import-time singletons.If GROQ_API_KEY is missing in certain envs (e.g., tests), module import will crash.
-# Global instance -ai_router = AIRouter() +# Global instance (lazy) +ai_router: Optional[AIRouter] = None +try: + ai_router = AIRouter() +except Exception as e: + logger.warning("AIRouter not initialized at import: %s", e)Backend/sql.txt (3)
478-492: Add CHECKs or ENUMs for status fields.Codify allowed values (contracts, milestones, deliverables, payments) to prevent drift like 'active' vs commented set.
1-10: Ensure pgcrypto extension is enabled.gen_random_uuid() requires pgcrypto (or pgcrypto-like). Add a guard.
CREATE EXTENSION IF NOT EXISTS pgcrypto;Also applies to: 48-51, 71-74
539-566: Minor nomenclature in comments (cosmetic).Capitalize platform/payment names: YouTube, TikTok, Twitter, PayPal.
Frontend/src/services/brandApi.ts (2)
4-4: Don’t hardcode API base URL.Use env to support staging/prod and SSR.
-const API_BASE_URL = 'http://localhost:8000/api/brand'; +const API_BASE_URL = + (typeof process !== 'undefined' && process.env.NEXT_PUBLIC_API_BASE_URL) || + import.meta?.env?.VITE_API_BASE_URL || + 'http://localhost:8000/api/brand';
165-178: Preserve zero values in filters.Truthy check drops 0; use undefined check.
- if (filters?.min_engagement) params.append('min_engagement', filters.min_engagement.toString()); + if (filters?.min_engagement !== undefined) { + params.append('min_engagement', String(filters.min_engagement)); + }Backend/app/schemas/schema.py (2)
2-2: Import Any for Dict payloads.Some fields represent arbitrary JSON objects; add Any.
-from typing import Optional, Dict, List +from typing import Optional, Dict, List, Any
151-157: Type recent_activity precisely.Use List[Dict[str, Any]] for clarity and better validation.
- recent_activity: list + recent_activity: List[Dict[str, Any]]Backend/app/routes/contracts.py (5)
235-236: Avoid broad Exception; chain causes and log traces.Standardize to log, then raise with cause.
- except Exception as e: - raise HTTPException(status_code=500, detail=f"...: {str(e)}") + except Exception as e: + # logger.exception("Context message") # add if a logger is available + raise HTTPException(status_code=500, detail="Internal server error") from eAlso applies to: 262-264, 278-280, 298-300, 314-316, 335-337, 362-364, 378-380, 399-401, 410-412, 447-448, 467-469, 478-480, 498-500, 514-516, 536-538, 547-549, 565-569, 583-585, 605-607, 616-618, 632-634, 647-649, 665-666, 684-686, 700-702, 739-741, 773-774
650-657: Validate analytics payload against schema/table columns.Accepting arbitrary Dict risks 400s from unexpected keys. Define an AnalyticsCreate model mirroring table columns.
-class AnalyticsResponse(BaseModel): +class AnalyticsResponse(BaseModel): ... +class AnalyticsCreate(BaseModel): + performance_metrics: Optional[Dict[str, Any]] = None + engagement_data: Optional[Dict[str, Any]] = None + revenue_generated: Optional[float] = 0 + roi_percentage: Optional[float] = 0 + cost_per_engagement: Optional[float] = 0 + cost_per_click: Optional[float] = 0 ... -async def create_contract_analytics(contract_id: str, analytics_data: Dict[str, Any]): +async def create_contract_analytics(contract_id: str, analytics_data: AnalyticsCreate): ... - "contract_id": contract_id, - **analytics_data + "contract_id": contract_id, + **analytics_data.dict(exclude_unset=True)
707-737: Robustness in stats computation.Handle empty results; avoid None.
- contracts = result.data + contracts = result.data or []
746-773: Avoid shadowing built-ins and use safer search param name.query param named
queryis fine, but local varsearch_querycollides semantically. Also ensure ilike on empty data handled.-async def search_contracts( - query: str = Query(..., description="Search term"), +async def search_contracts( + q: str = Query(..., description="Search term"), ... - result = search_query.ilike("contract_title", f"%{query}%").limit(limit).execute() + result = search_query.ilike("contract_title", f"%{q}%").limit(limit).execute()
1-9: Remove unused imports.httpx and Depends aren’t used.
-from fastapi import APIRouter, HTTPException, Depends, Query +from fastapi import APIRouter, HTTPException, Query -import httpxBackend/app/routes/brand_dashboard.py (14)
67-75: Improve DB error handling and diagnostics.Surface Supabase errors, use logger.exception, and preserve traceback via raise ... from e.
def safe_supabase_query(query_func, error_message: str = "Database operation failed"): """Safely execute Supabase queries with proper error handling""" - try: - result = query_func() - return result.data if result.data else [] - except Exception as e: - logger.error(f"Supabase error in {error_message}: {e}") - raise HTTPException(status_code=500, detail=error_message) + try: + result = query_func() + except Exception as e: + logger.exception(f"Supabase error in {error_message}") + raise HTTPException(status_code=500, detail=error_message) from e + # Supabase responses may carry .error alongside .data + resp_error = getattr(result, "error", None) + if resp_error: + logger.error(f"Supabase responded with error in {error_message}: {resp_error}") + raise HTTPException(status_code=500, detail=error_message) + return result.data or []
28-36: Fail fast on missing Supabase config; avoid configuring logging here.
- Validate SUPABASE_URL/KEY at import to prevent None from reaching create_client.
- Avoid logging.basicConfig in libraries; set logging in app entrypoint.
# Load environment variables load_dotenv() url: str = os.getenv("SUPABASE_URL") key: str = os.getenv("SUPABASE_KEY") -supabase: Client = create_client(url, key) +if not url or not key: + raise RuntimeError("Supabase configuration missing: SUPABASE_URL/SUPABASE_KEY") +supabase: Client = create_client(url, key) @@ -logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__)Confirm logging is configured in Backend/app/main.py; otherwise move it there.
113-116: Remove unused brand profile fetch.Dead call + unused variable; saves a DB roundtrip.
- # Get brand's profile - profile_result = supabase.table("brand_profiles").select("*").eq("user_id", brand_id).execute() - profile = profile_result.data[0] if profile_result.data else None
367-380: Filter pending applications in SQL; avoid scanning all.Push brand campaign IDs into the query.
- applications = safe_supabase_query( - lambda: supabase.table("sponsorship_applications").select("*").eq("status", "pending").execute(), - "Failed to fetch applications" - ) - - # Filter applications for this brand's campaigns - brand_campaigns = safe_supabase_query( + brand_campaigns = safe_supabase_query( lambda: supabase.table("sponsorships").select("id").eq("brand_id", brand_id).execute(), "Failed to fetch brand campaigns" ) brand_campaign_ids = [campaign["id"] for campaign in brand_campaigns] - pending_applications = [app for app in applications if app.get("sponsorship_id") in brand_campaign_ids] + pending_applications = safe_supabase_query( + lambda: supabase.table("sponsorship_applications") + .select("*") + .eq("status", "pending") + .in_("sponsorship_id", brand_campaign_ids) + .execute(), + "Failed to fetch applications" + )
762-785: Schema mismatch risk: extra fields not in CreatorMatchResponse.You add creator_name/creator_role but response_model lacks them; FastAPI will drop extras. If frontend expects these, extend schema accordingly or stop injecting them.
Would you like me to patch Backend/app/schemas/schema.py to include:
- creator_name: Optional[str] = None
- creator_role: Optional[str] = None
813-829: Use provided filters or remove unused params.industry/location are accepted but unused. Implement at least location via audience_insights.top_markets.
for creator in creators: creator_insights = insights_lookup.get(creator["id"]) # Apply filters if min_engagement and creator_insights: if creator_insights.get("engagement_rate", 0) < min_engagement: continue + if location and creator_insights: + top_markets = creator_insights.get("top_markets", {}) + if isinstance(top_markets, dict) and location not in top_markets: + continue
877-891: Analytics correctness/perf: pick latest metrics per campaign only.Building a single metrics_lookup from all records collapses history arbitrarily. Query latest per campaign (order/limit) or aggregate by recorded_at in SQL. Also filter metrics by the brand’s campaign IDs to avoid scanning entire table.
If you want, I can provide a batched query pattern using Supabase RPC or a single in_ with order(..., desc=True).limit(1) per campaign.
922-947: Naming: these are payouts (spend), not revenue.Completed sponsorship_payments represent brand outflows. Consider renaming fields/route to “spend” to avoid confusion with top‑line revenue metrics.
1031-1073: N+1 queries when enriching applications.Batch fetch creators and campaigns using in_ to cut roundtrips.
- enhanced_applications = [] - for application in applications: - # Get creator details - creator_result = supabase.table("users").select("*").eq("id", application["creator_id"]).execute() - creator = creator_result.data[0] if creator_result.data else None - # Get campaign details - campaign_result = supabase.table("sponsorships").select("*").eq("id", application["sponsorship_id"]).execute() - campaign = campaign_result.data[0] if campaign_result.data else None - enhanced_application = {**application,"creator": creator,"campaign": campaign} - enhanced_applications.append(enhanced_application) + creator_ids = [a["creator_id"] for a in applications] + creators = safe_supabase_query(lambda: supabase.table("users").select("*").in_("id", creator_ids).execute(), "Failed to fetch creators") + creators_by_id = {c["id"]: c for c in creators} + campaigns_by_id = {c["id"]: c for c in campaigns} + enhanced_applications = [ + {**a, "creator": creators_by_id.get(a["creator_id"]), "campaign": campaigns_by_id.get(a["sponsorship_id"])} + for a in applications + ]
1214-1246: N+1 in payments enrichment.Batch creators/campaigns via in_ similar to applications to reduce latency and load.
1325-1357: Analytics robustness: parse timestamps safely.Slicing [:7] assumes ISO strings. Parse to date for reliability.
- month = payment["transaction_date"][:7] if payment["transaction_date"] else "unknown" + ts = payment.get("transaction_date") + if ts: + try: + dt = datetime.fromisoformat(ts.replace("Z","+00:00")) if isinstance(ts, str) else ts + month = dt.strftime("%Y-%m") + except Exception: + month = "unknown" + else: + month = "unknown"
1-9: Remove unused SQLAlchemy imports (and related dead code).AsyncSession, select, AsyncSessionLocal aren’t used in this module.
76-93: Rate limiting helper is unused.Either wire it into mutating endpoints or remove to avoid dead state in process memory. Prefer Redis‑backed rate limiting as noted in PR summary.
161-276: Async endpoints call sync client; event‑loop blocking.Supabase Python client is sync. Wrap calls in run_in_threadpool or switch to an async client to prevent request latency under load. I can provide a small safe_supabase_query_async helper and migrate hot paths first.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
Frontend/public/aossielogo.pngis excluded by!**/*.png
📒 Files selected for processing (26)
Backend/.env-example(1 hunks)Backend/app/main.py(2 hunks)Backend/app/models/models.py(2 hunks)Backend/app/routes/ai_query.py(1 hunks)Backend/app/routes/brand_dashboard.py(1 hunks)Backend/app/routes/contracts.py(1 hunks)Backend/app/schemas/schema.py(2 hunks)Backend/app/services/ai_router.py(1 hunks)Backend/app/services/ai_services.py(1 hunks)Backend/app/services/redis_client.py(1 hunks)Backend/requirements.txt(1 hunks)Backend/sql.txt(1 hunks)Frontend/README-INTEGRATION.md(1 hunks)Frontend/src/App.tsx(2 hunks)Frontend/src/components/chat/BrandChatAssistant.tsx(1 hunks)Frontend/src/components/collaboration-hub/CreatorMatchGrid.tsx(1 hunks)Frontend/src/components/contracts/ContractDetailsModal.tsx(1 hunks)Frontend/src/components/user-nav.tsx(2 hunks)Frontend/src/context/AuthContext.tsx(1 hunks)Frontend/src/hooks/useBrandDashboard.ts(1 hunks)Frontend/src/index.css(1 hunks)Frontend/src/pages/Brand/Dashboard.tsx(1 hunks)Frontend/src/pages/Brand/DashboardOverview.tsx(1 hunks)Frontend/src/pages/Contracts.tsx(1 hunks)Frontend/src/services/aiApi.ts(1 hunks)Frontend/src/services/brandApi.ts(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (11)
Frontend/src/hooks/useBrandDashboard.ts (3)
Frontend/src/context/AuthContext.tsx (1)
useAuth(216-222)Frontend/src/services/brandApi.ts (14)
DashboardOverview(7-13)BrandProfile(15-24)Campaign(26-36)CreatorMatch(38-44)Application(46-56)Payment(58-68)brandApi(246-246)createCampaign(132-144)updateCampaign(146-151)deleteCampaign(153-157)updateApplicationStatus(202-212)searchCreators(164-178)getCampaignPerformance(185-187)getRevenueAnalytics(189-191)Frontend/src/services/aiApi.ts (2)
queryAI(57-83)aiApi(102-102)
Frontend/src/App.tsx (1)
Frontend/src/services/brandApi.ts (1)
DashboardOverview(7-13)
Frontend/src/components/collaboration-hub/CreatorMatchGrid.tsx (1)
Frontend/src/components/collaboration-hub/CreatorMatchCard.tsx (1)
CreatorMatchCard(65-130)
Frontend/src/components/contracts/ContractDetailsModal.tsx (1)
Backend/app/models/models.py (1)
Contract(208-224)
Frontend/src/pages/Brand/Dashboard.tsx (1)
Frontend/src/hooks/useBrandDashboard.ts (1)
useBrandDashboard(6-288)
Frontend/src/pages/Brand/DashboardOverview.tsx (1)
Frontend/src/services/brandApi.ts (1)
DashboardOverview(7-13)
Backend/app/models/models.py (2)
Backend/app/models/chat.py (1)
generate_uuid(9-10)Backend/app/routes/post.py (1)
generate_uuid(31-32)
Backend/app/services/ai_router.py (1)
Backend/app/routes/ai_query.py (1)
get_route_info(205-222)
Backend/app/routes/brand_dashboard.py (2)
Backend/app/models/models.py (5)
User(25-53)Sponsorship(76-92)CampaignMetrics(189-204)Contract(208-224)SponsorshipApplication(114-128)Backend/app/schemas/schema.py (20)
BrandProfileCreate(61-67)BrandProfileUpdate(69-74)BrandProfileResponse(76-87)CampaignMetricsCreate(91-97)CampaignMetricsResponse(99-110)ContractCreate(114-119)ContractUpdate(121-123)ContractResponse(125-135)CreatorMatchResponse(139-147)DashboardOverviewResponse(151-156)CampaignAnalyticsResponse(158-166)CreatorMatchAnalyticsResponse(168-174)SponsorshipApplicationResponse(182-194)ApplicationUpdateRequest(196-198)ApplicationSummaryResponse(200-206)PaymentResponse(210-222)PaymentStatusUpdate(224-225)PaymentAnalyticsResponse(227-233)CampaignMetricsUpdate(237-242)SponsorshipCreate(21-27)
Backend/app/routes/ai_query.py (3)
Backend/app/services/redis_client.py (2)
get_session_state(19-21)save_session_state(23-24)Backend/app/services/ai_router.py (3)
process_query(131-169)list_available_routes(338-340)get_route_info(334-336)Backend/app/routes/brand_dashboard.py (9)
search_creators(792-834)get_dashboard_overview(99-159)get_creator_matches(763-789)get_brand_profile(590-606)get_brand_campaigns(634-646)get_creator_profile(837-871)get_campaign_performance(878-920)get_revenue_analytics(923-951)get_brand_contracts(958-968)
Backend/app/routes/contracts.py (1)
Frontend/src/utils/supabase.tsx (1)
supabase(11-11)
🪛 LanguageTool
Backend/sql.txt
[uncategorized] ~539-~539: The official name of this popular video platform is spelled with a capital “T”.
Context: ...latform TEXT NOT NULL, -- 'instagram', 'youtube', 'tiktok', 'twitter', 'facebook' r...
(YOUTUBE)
[uncategorized] ~539-~539: Did you mean “TikTok” (= social networking app)?
Context: ...T NOT NULL, -- 'instagram', 'youtube', 'tiktok', 'twitter', 'facebook' requirement...
(TIK_TOK)
[uncategorized] ~539-~539: Did you mean “Twitter” (= microblogging service, capitalized)?
Context: ..., -- 'instagram', 'youtube', 'tiktok', 'twitter', 'facebook' requirements JSONB, ...
(TWITTER)
[uncategorized] ~565-~565: The official name of this payment provider is spelled with two capital “P”.
Context: ...yment_method TEXT, -- 'bank_transfer', 'paypal', 'stripe', 'escrow' transaction_id...
(PAYPAL)
🪛 Ruff (0.14.1)
Backend/app/services/ai_router.py
22-22: Avoid specifying long messages outside the exception class
(TRY003)
131-131: PEP 484 prohibits implicit Optional
Convert to T | None
(RUF013)
165-165: Consider moving this statement to an else block
(TRY300)
167-167: Do not catch blind exception: Exception
(BLE001)
168-168: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
169-169: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
Backend/app/routes/brand_dashboard.py
71-71: Consider moving this statement to an else block
(TRY300)
72-72: Do not catch blind exception: Exception
(BLE001)
73-73: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
74-74: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
79-79: Unused function argument: window_seconds
(ARG001)
115-115: Local variable profile is assigned to but never used
Remove assignment to unused variable profile
(F841)
157-157: Do not catch blind exception: Exception
(BLE001)
158-158: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
159-159: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
177-177: Local variable total_campaigns is assigned to but never used
Remove assignment to unused variable total_campaigns
(F841)
182-182: Local variable total_reach is assigned to but never used
Remove assignment to unused variable total_reach
(F841)
280-280: Do not catch blind exception: Exception
(BLE001)
281-281: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
282-282: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
304-304: Function definition does not bind loop variable campaign
(B023)
351-351: Do not catch blind exception: Exception
(BLE001)
352-352: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
353-353: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
397-397: Function definition does not bind loop variable campaign
(B023)
452-454: Consider moving this statement to an else block
(TRY300)
458-458: Do not catch blind exception: Exception
(BLE001)
459-459: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
460-460: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
526-526: Do not catch blind exception: Exception
(BLE001)
527-527: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
528-528: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
552-552: Do not catch blind exception: Exception
(BLE001)
553-553: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
554-554: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
583-583: Abstract raise to an inner function
(TRY301)
585-585: Do not catch blind exception: Exception
(BLE001)
586-586: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
587-587: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
600-600: Abstract raise to an inner function
(TRY301)
604-604: Do not catch blind exception: Exception
(BLE001)
605-605: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
606-606: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
621-621: Abstract raise to an inner function
(TRY301)
625-625: Do not catch blind exception: Exception
(BLE001)
626-626: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
627-627: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
663-663: Abstract raise to an inner function
(TRY301)
667-667: Do not catch blind exception: Exception
(BLE001)
668-668: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
669-669: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
705-705: Abstract raise to an inner function
(TRY301)
709-709: Do not catch blind exception: Exception
(BLE001)
710-710: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
711-711: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
722-722: Abstract raise to an inner function
(TRY301)
729-729: Abstract raise to an inner function
(TRY301)
733-733: Do not catch blind exception: Exception
(BLE001)
734-734: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
735-735: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
746-746: Abstract raise to an inner function
(TRY301)
748-748: Local variable response is assigned to but never used
Remove assignment to unused variable response
(F841)
750-750: Consider moving this statement to an else block
(TRY300)
754-754: Do not catch blind exception: Exception
(BLE001)
755-755: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
756-756: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
785-785: Consider moving this statement to an else block
(TRY300)
787-787: Do not catch blind exception: Exception
(BLE001)
788-788: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
789-789: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
793-793: Unused function argument: brand_id
(ARG001)
794-794: Unused function argument: industry
(ARG001)
796-796: Unused function argument: location
(ARG001)
830-830: Consider moving this statement to an else block
(TRY300)
832-832: Do not catch blind exception: Exception
(BLE001)
833-833: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
834-834: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
837-837: Unused function argument: brand_id
(ARG001)
845-845: Abstract raise to an inner function
(TRY301)
860-865: Consider moving this statement to an else block
(TRY300)
869-869: Do not catch blind exception: Exception
(BLE001)
870-870: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
871-871: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
916-916: Consider moving this statement to an else block
(TRY300)
918-918: Do not catch blind exception: Exception
(BLE001)
919-919: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
920-920: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
949-949: Do not catch blind exception: Exception
(BLE001)
950-950: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
951-951: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
964-964: Consider moving this statement to an else block
(TRY300)
966-966: Do not catch blind exception: Exception
(BLE001)
967-967: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
968-968: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
992-992: Abstract raise to an inner function
(TRY301)
994-994: Do not catch blind exception: Exception
(BLE001)
995-995: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
996-996: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
1011-1011: Abstract raise to an inner function
(TRY301)
1018-1018: Abstract raise to an inner function
(TRY301)
1022-1022: Do not catch blind exception: Exception
(BLE001)
1023-1023: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
1024-1024: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
1074-1074: Consider moving this statement to an else block
(TRY300)
1078-1078: Do not catch blind exception: Exception
(BLE001)
1079-1079: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
1080-1080: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
1095-1095: Abstract raise to an inner function
(TRY301)
1102-1102: Abstract raise to an inner function
(TRY301)
1117-1117: Consider moving this statement to an else block
(TRY300)
1121-1121: Do not catch blind exception: Exception
(BLE001)
1122-1122: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
1123-1123: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
1142-1142: Abstract raise to an inner function
(TRY301)
1147-1147: Abstract raise to an inner function
(TRY301)
1159-1159: Abstract raise to an inner function
(TRY301)
1163-1163: Do not catch blind exception: Exception
(BLE001)
1164-1164: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
1165-1165: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
1205-1205: Do not catch blind exception: Exception
(BLE001)
1206-1206: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
1207-1207: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
1246-1246: Consider moving this statement to an else block
(TRY300)
1250-1250: Do not catch blind exception: Exception
(BLE001)
1251-1251: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
1252-1252: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
1266-1266: Abstract raise to an inner function
(TRY301)
1284-1284: Consider moving this statement to an else block
(TRY300)
1288-1288: Do not catch blind exception: Exception
(BLE001)
1289-1289: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
1290-1290: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
1309-1309: Abstract raise to an inner function
(TRY301)
1317-1317: Abstract raise to an inner function
(TRY301)
1321-1321: Do not catch blind exception: Exception
(BLE001)
1322-1322: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
1323-1323: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
1361-1361: Do not catch blind exception: Exception
(BLE001)
1362-1362: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
1363-1363: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
1387-1387: Abstract raise to an inner function
(TRY301)
1409-1409: Abstract raise to an inner function
(TRY301)
1413-1413: Do not catch blind exception: Exception
(BLE001)
1414-1414: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
1415-1415: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
1430-1430: Abstract raise to an inner function
(TRY301)
1438-1438: Consider moving this statement to an else block
(TRY300)
1442-1442: Do not catch blind exception: Exception
(BLE001)
1443-1443: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
1444-1444: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
1465-1465: Abstract raise to an inner function
(TRY301)
1474-1474: Abstract raise to an inner function
(TRY301)
1478-1478: Do not catch blind exception: Exception
(BLE001)
1479-1479: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
1480-1480: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
1523-1523: Do not catch blind exception: Exception
(BLE001)
1524-1524: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
1555-1555: Do not catch blind exception: Exception
(BLE001)
1556-1556: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
1579-1579: Do not catch blind exception: Exception
(BLE001)
1580-1580: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
1610-1610: Do not catch blind exception: Exception
(BLE001)
1611-1611: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
1625-1625: Do not catch blind exception: Exception
(BLE001)
1626-1626: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
1627-1627: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
Backend/app/routes/ai_query.py
40-40: Abstract raise to an inner function
(TRY301)
138-138: Do not catch blind exception: Exception
(BLE001)
139-139: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
147-147: Parenthesize a and b expressions when chaining and and or together, to make the precedence clear
Parenthesize the and subexpression
(RUF021)
182-182: Consider moving this statement to an else block
(TRY300)
185-185: Do not catch blind exception: Exception
(BLE001)
186-186: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
187-187: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
200-200: Do not catch blind exception: Exception
(BLE001)
201-201: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
202-202: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
212-212: Abstract raise to an inner function
(TRY301)
214-217: Consider moving this statement to an else block
(TRY300)
220-220: Do not catch blind exception: Exception
(BLE001)
221-221: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
222-222: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
233-237: Consider moving this statement to an else block
(TRY300)
238-238: Do not catch blind exception: Exception
(BLE001)
239-239: Use logging.exception instead of logging.error
Replace with exception
(TRY400)
Backend/app/routes/contracts.py
233-233: Abstract raise to an inner function
(TRY301)
235-235: Do not catch blind exception: Exception
(BLE001)
236-236: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
236-236: Use explicit conversion flag
Replace with conversion flag
(RUF010)
262-262: Do not catch blind exception: Exception
(BLE001)
263-263: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
263-263: Use explicit conversion flag
Replace with conversion flag
(RUF010)
272-272: Abstract raise to an inner function
(TRY301)
278-278: Do not catch blind exception: Exception
(BLE001)
279-279: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
279-279: Use explicit conversion flag
Replace with conversion flag
(RUF010)
292-292: Abstract raise to an inner function
(TRY301)
298-298: Do not catch blind exception: Exception
(BLE001)
299-299: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
299-299: Use explicit conversion flag
Replace with conversion flag
(RUF010)
308-308: Abstract raise to an inner function
(TRY301)
310-310: Consider moving this statement to an else block
(TRY300)
314-314: Do not catch blind exception: Exception
(BLE001)
315-315: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
315-315: Use explicit conversion flag
Replace with conversion flag
(RUF010)
333-333: Abstract raise to an inner function
(TRY301)
335-335: Do not catch blind exception: Exception
(BLE001)
336-336: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
336-336: Use explicit conversion flag
Replace with conversion flag
(RUF010)
362-362: Do not catch blind exception: Exception
(BLE001)
363-363: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
363-363: Use explicit conversion flag
Replace with conversion flag
(RUF010)
372-372: Abstract raise to an inner function
(TRY301)
378-378: Do not catch blind exception: Exception
(BLE001)
379-379: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
379-379: Use explicit conversion flag
Replace with conversion flag
(RUF010)
397-397: Abstract raise to an inner function
(TRY301)
399-399: Do not catch blind exception: Exception
(BLE001)
400-400: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
400-400: Use explicit conversion flag
Replace with conversion flag
(RUF010)
410-410: Do not catch blind exception: Exception
(BLE001)
411-411: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
411-411: Use explicit conversion flag
Replace with conversion flag
(RUF010)
424-424: Abstract raise to an inner function
(TRY301)
430-430: Do not catch blind exception: Exception
(BLE001)
431-431: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
431-431: Use explicit conversion flag
Replace with conversion flag
(RUF010)
440-440: Abstract raise to an inner function
(TRY301)
442-442: Consider moving this statement to an else block
(TRY300)
446-446: Do not catch blind exception: Exception
(BLE001)
447-447: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
447-447: Use explicit conversion flag
Replace with conversion flag
(RUF010)
465-465: Abstract raise to an inner function
(TRY301)
467-467: Do not catch blind exception: Exception
(BLE001)
468-468: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
468-468: Use explicit conversion flag
Replace with conversion flag
(RUF010)
478-478: Do not catch blind exception: Exception
(BLE001)
479-479: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
479-479: Use explicit conversion flag
Replace with conversion flag
(RUF010)
492-492: Abstract raise to an inner function
(TRY301)
498-498: Do not catch blind exception: Exception
(BLE001)
499-499: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
499-499: Use explicit conversion flag
Replace with conversion flag
(RUF010)
508-508: Abstract raise to an inner function
(TRY301)
510-510: Consider moving this statement to an else block
(TRY300)
514-514: Do not catch blind exception: Exception
(BLE001)
515-515: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
515-515: Use explicit conversion flag
Replace with conversion flag
(RUF010)
534-534: Abstract raise to an inner function
(TRY301)
536-536: Do not catch blind exception: Exception
(BLE001)
537-537: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
537-537: Use explicit conversion flag
Replace with conversion flag
(RUF010)
547-547: Do not catch blind exception: Exception
(BLE001)
548-548: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
548-548: Use explicit conversion flag
Replace with conversion flag
(RUF010)
561-561: Abstract raise to an inner function
(TRY301)
567-567: Do not catch blind exception: Exception
(BLE001)
568-568: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
568-568: Use explicit conversion flag
Replace with conversion flag
(RUF010)
577-577: Abstract raise to an inner function
(TRY301)
579-579: Consider moving this statement to an else block
(TRY300)
583-583: Do not catch blind exception: Exception
(BLE001)
584-584: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
584-584: Use explicit conversion flag
Replace with conversion flag
(RUF010)
603-603: Abstract raise to an inner function
(TRY301)
605-605: Do not catch blind exception: Exception
(BLE001)
606-606: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
606-606: Use explicit conversion flag
Replace with conversion flag
(RUF010)
616-616: Do not catch blind exception: Exception
(BLE001)
617-617: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
617-617: Use explicit conversion flag
Replace with conversion flag
(RUF010)
626-626: Abstract raise to an inner function
(TRY301)
628-628: Consider moving this statement to an else block
(TRY300)
632-632: Do not catch blind exception: Exception
(BLE001)
633-633: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
633-633: Use explicit conversion flag
Replace with conversion flag
(RUF010)
647-647: Do not catch blind exception: Exception
(BLE001)
648-648: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
648-648: Use explicit conversion flag
Replace with conversion flag
(RUF010)
662-662: Abstract raise to an inner function
(TRY301)
664-664: Do not catch blind exception: Exception
(BLE001)
665-665: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
665-665: Use explicit conversion flag
Replace with conversion flag
(RUF010)
684-684: Do not catch blind exception: Exception
(BLE001)
685-685: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
685-685: Use explicit conversion flag
Replace with conversion flag
(RUF010)
694-694: Abstract raise to an inner function
(TRY301)
696-696: Consider moving this statement to an else block
(TRY300)
700-700: Do not catch blind exception: Exception
(BLE001)
701-701: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
701-701: Use explicit conversion flag
Replace with conversion flag
(RUF010)
739-739: Do not catch blind exception: Exception
(BLE001)
740-740: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
740-740: Use explicit conversion flag
Replace with conversion flag
(RUF010)
773-773: Do not catch blind exception: Exception
(BLE001)
774-774: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling
(B904)
774-774: Use explicit conversion flag
Replace with conversion flag
(RUF010)
| port=5432 | ||
| dbname=postgres | ||
| GROQ_API_KEY= | ||
| GROQ_API_KEY=your_groq_api_key_here |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Avoid real secrets; align variable names with backend; document Redis SSL.
- Don’t commit real keys; keep placeholders. Add a header comment reminding contributors to copy this file to .env and never commit real values.
- Verify the backend actually reads
GROQ_API_KEY,YOUTUBE_API_KEY, and Redis vars as named here. - Redis Cloud typically requires TLS. The example suggests non‑SSL; either mark
REDIS_SSL=truein examples or annotate when to disable.
Run to confirm env var usage:
Also applies to: 10-10, 12-15
🏁 Script executed:
#!/bin/bash
rg -nP --type=py -C2 '\bos\.getenv\(\s*"(GROQ_API_KEY|YOUTUBE_API_KEY|REDIS_HOST|REDIS_PORT|REDIS_PASSWORD)"'Length of output: 1746
🏁 Script executed:
#!/bin/bash
# Get full redis_client.py to see complete redis.Redis() call
cat Backend/app/services/redis_client.pyLength of output: 834
Add REDIS_SSL to .env-example and make SSL configurable in redis_client.py.
Env var names are correctly aligned with backend usage (verified: GROQ_API_KEY, YOUTUBE_API_KEY, REDIS_HOST, REDIS_PORT, REDIS_PASSWORD all read correctly).
However, the .env-example is missing REDIS_SSL configuration. Currently, redis_client.py hardcodes ssl=False (line 14), which:
- Cannot be overridden per deployment
- Contradicts typical Redis Cloud requirements (which mandate TLS)
- Makes the comment "works without SSL" misleading
Add REDIS_SSL=true to .env-example and update redis_client.py to read it: ssl=os.getenv("REDIS_SSL", "false").lower() == "true" (or similar, depending on your parsing preference).
🤖 Prompt for AI Agents
In Backend/.env-example around line 6 add a REDIS_SSL entry (e.g.,
REDIS_SSL=true) so deployments can opt into TLS; then update
Backend/redis_client.py (around the current ssl=False on line 14) to read the
env var and set ssl accordingly using something like ssl =
os.getenv("REDIS_SSL", "false").lower() == "true", and update the inline comment
to reflect that SSL is now configurable and defaults to false.
| class AIQueryResponse(BaseModel): | ||
| intent: str | ||
| route: Optional[str] = None | ||
| parameters: Dict[str, Any] = {} | ||
| follow_up_needed: bool = False | ||
| follow_up_question: Optional[str] = None | ||
| explanation: str | ||
| original_query: str | ||
| timestamp: str | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Response model drops session_id/result; include them and fix mutable defaults.
FastAPI strips fields not in the response_model. Frontend expects session_id and result, but they’re not defined, so they won’t be returned. Also avoid {} as a default.
-from pydantic import BaseModel
+from pydantic import BaseModel, Field
@@
class AIQueryResponse(BaseModel):
intent: str
route: Optional[str] = None
- parameters: Dict[str, Any] = {}
+ parameters: Dict[str, Any] = Field(default_factory=dict)
follow_up_needed: bool = False
follow_up_question: Optional[str] = None
explanation: str
original_query: str
timestamp: str
+ session_id: Optional[str] = None
+ result: Optional[Any] = None
+ error: Optional[str] = None
@@
- response = AIQueryResponse(
+ response = AIQueryResponse(
intent=result.get("intent", "unknown"),
route=result.get("route"),
parameters=params,
- follow_up_needed=not all_params_present and not only_optional_params or api_error is not None,
+ follow_up_needed=((not all_params_present and not only_optional_params) or (api_error is not None)),
follow_up_question=(result.get("follow_up_question") if not all_params_present and not only_optional_params else None),
explanation=(result.get("explanation", "") if not api_error else f"An error occurred while processing your request: {api_error}"),
original_query=result.get("original_query", request.query),
timestamp=result.get("timestamp", ""),
)
@@
- response_dict = response.dict()
+ response_dict = response.dict()
@@
- if api_result is not None:
- response_dict["result"] = api_result
- if api_error is not None:
- response_dict["error"] = api_error
- return response_dict
+ if api_result is not None:
+ response_dict["result"] = api_result
+ if api_error is not None:
+ response_dict["error"] = api_error
+ return response_dictAlso applies to: 143-152, 154-182
🤖 Prompt for AI Agents
In Backend/app/routes/ai_query.py around lines 22-31 (and also apply same fixes
at 143-152 and 154-182), the AIQueryResponse Pydantic model is missing the
session_id and result fields which causes FastAPI to strip them from responses,
and it uses a mutable default (parameters: Dict = {}), which is unsafe; add
session_id: Optional[str] (or str if required) and result: Optional[Dict[str,
Any]] (or the correct type) to the model, and replace mutable defaults with None
and proper default_factory where needed (e.g., parameters: Dict[str, Any] =
Field(default_factory=dict) or parameters: Optional[Dict[str, Any]] = None) so
fields are returned to the frontend and defaults are immutable/initialized
per-instance.
| # Security Helper Functions | ||
| def validate_brand_access(brand_id: str, current_user_id: str): | ||
| """Validate that the current user can access the brand data""" | ||
| if brand_id != current_user_id: | ||
| raise HTTPException(status_code=403, detail="Access denied: You can only access your own data") | ||
| return True | ||
|
|
||
| def require_brand_role(user_role: str): | ||
| """Ensure user has brand role""" | ||
| if user_role != "brand": | ||
| raise HTTPException(status_code=403, detail="Access denied: Brand role required") | ||
| return True | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Authz missing across router; helpers unused. Enforce user–brand binding.
All routes trust a client‑supplied brand_id. Use Depends(get_current_user), require brand role, and enforce validate_brand_access(...) consistently. Example patch for one route (apply same pattern elsewhere):
@@
-from fastapi import APIRouter, HTTPException, Depends, Query
+from fastapi import APIRouter, HTTPException, Depends, Query
@@
router = APIRouter(prefix="/api/brand", tags=["Brand Dashboard"])
@@
-@router.get("/campaigns")
-async def get_brand_campaigns(brand_id: str = Query(..., description="Brand user ID")):
+# TODO: import your actual auth dependency
+from ..services.auth import get_current_user # adjust path
+
+@router.get("/campaigns")
+async def get_brand_campaigns(
+ brand_id: str = Query(..., description="Brand user ID"),
+ current_user=Depends(get_current_user),
+):
+ require_brand_role(current_user["role"])
+ validate_brand_access(brand_id, current_user["id"])
@@
- campaigns = safe_supabase_query(
+ campaigns = safe_supabase_query(
lambda: supabase.table("sponsorships").select("*").eq("brand_id", brand_id).execute(),
"Failed to fetch brand campaigns"
)Run to find all endpoints still trusting brand_id without Depends(get_current_user):
🏁 Script executed:
#!/bin/bash
rg -nP 'def\s+\w+\(.*brand_id:\s*str\s*=\s*Query' -C1 Backend/app/routes | sed -n '1,200p'Length of output: 6297
Now I need to search the codebase directly to verify the actual state of auth and helper usage:
Now let me search the codebase directly to verify the helpers' usage and auth status:
#!/bin/bash
# Check if helpers are called anywhere in the codebase
echo "=== Searching for calls to validate_brand_access ==="
rg -n 'validate_brand_access' Backend/app/routes/
echo ""
echo "=== Searching for calls to require_brand_role ==="
rg -n 'require_brand_role' Backend/app/routes/
echo ""
echo "=== Searching for get_current_user or auth dependency ==="
rg -n 'get_current_user|from.*auth import|from.*security import' Backend/app/routes/ -C2
🏁 Script executed:
#!/bin/bash
# Check if helpers are called anywhere in the codebase
echo "=== Searching for calls to validate_brand_access ==="
rg -n 'validate_brand_access' Backend/app/routes/
echo ""
echo "=== Searching for calls to require_brand_role ==="
rg -n 'require_brand_role' Backend/app/routes/
echo ""
echo "=== Searching for get_current_user or auth dependency ==="
rg -n 'get_current_user|from.*auth import|from.*security import' Backend/app/routes/ -C2
echo ""
echo "=== Sample endpoint implementation (lines 99-115) ==="
sed -n '99,115p' Backend/app/routes/brand_dashboard.py
echo ""
echo "=== Another sample endpoint (lines 162-178) ==="
sed -n '162,178p' Backend/app/routes/brand_dashboard.pyLength of output: 1785
Authorization bypass: 21 endpoints trust client-supplied brand_id without current user validation.
All endpoints accept brand_id via Query parameter and query Supabase directly without verifying the requesting user owns that brand. The helper functions validate_brand_access() and require_brand_role() are defined but never called. No auth dependency (get_current_user) exists in the router.
Critical fix: Inject current user via Depends(get_current_user) in all 21 endpoints and enforce validate_brand_access(brand_id, current_user["id"]) before any Supabase query. Remove dead helpers or use them consistently.
Example pattern to apply to all endpoints:
-@router.get("/dashboard/overview", response_model=DashboardOverviewResponse)
-async def get_dashboard_overview(brand_id: str = Query(..., description="Brand user ID")):
+@router.get("/dashboard/overview", response_model=DashboardOverviewResponse)
+async def get_dashboard_overview(
+ brand_id: str = Query(..., description="Brand user ID"),
+ current_user=Depends(get_current_user),
+):
"""Get dashboard overview with key metrics for a brand"""
validate_uuid_format(brand_id, "brand_id")
+ require_brand_role(current_user["role"])
+ validate_brand_access(brand_id, current_user["id"])Affected endpoints: lines 99, 162, 285, 356, 463, 634, 649, 714, 738, 763, 837, 878, 923, 958, 1032, 1083, 1168, 1215, 1255, 1326, 1418, 1483.
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In Backend/app/routes/brand_dashboard.py around lines 48 to 60 and for the
listed affected endpoints (lines 99, 162, 285, 356, 463, 634, 649, 714, 738,
763, 837, 878, 923, 958, 1032, 1083, 1168, 1215, 1255, 1326, 1418, 1483): the
router currently trusts a client-supplied brand_id and never injects or
validates the current user; update each endpoint signature to accept
current_user via Depends(get_current_user) and immediately call
validate_brand_access(brand_id, current_user["id"]) (and
require_brand_role(current_user["role"]) where appropriate) before any Supabase
queries, remove or keep the helper functions consistently (delete if unused),
and ensure no Supabase queries execute until access checks pass.
| def validate_uuid_format(id_value: str, field_name: str = "ID"): | ||
| """Validate UUID format""" | ||
| if not id_value or len(id_value) != 36: | ||
| raise HTTPException(status_code=400, detail=f"Invalid {field_name} format") | ||
| return True | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
Robust UUID validation.
Length check is weak. Parse using uuid.UUID to reject malformed inputs.
def validate_uuid_format(id_value: str, field_name: str = "ID"):
- """Validate UUID format"""
- if not id_value or len(id_value) != 36:
- raise HTTPException(status_code=400, detail=f"Invalid {field_name} format")
- return True
+ """Validate UUID format"""
+ try:
+ uuid.UUID(str(id_value))
+ except (ValueError, TypeError):
+ raise HTTPException(status_code=400, detail=f"Invalid {field_name} format")
+ return True📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| def validate_uuid_format(id_value: str, field_name: str = "ID"): | |
| """Validate UUID format""" | |
| if not id_value or len(id_value) != 36: | |
| raise HTTPException(status_code=400, detail=f"Invalid {field_name} format") | |
| return True | |
| def validate_uuid_format(id_value: str, field_name: str = "ID"): | |
| """Validate UUID format""" | |
| try: | |
| uuid.UUID(str(id_value)) | |
| except (ValueError, TypeError): | |
| raise HTTPException(status_code=400, detail=f"Invalid {field_name} format") | |
| return True |
🤖 Prompt for AI Agents
In Backend/app/routes/brand_dashboard.py around lines 61 to 66, the UUID
validation currently only checks length which is insufficient; replace the
length check with actual parsing using Python's uuid.UUID to validate format:
attempt uuid.UUID(id_value, version=None) inside a try/except, and on ValueError
raise HTTPException(status_code=400, detail=f"Invalid {field_name} format");
keep the function signature and return True on success so callers are unchanged.
| for campaign in campaigns: | ||
| campaign_metrics = safe_supabase_query( | ||
| lambda: supabase.table("campaign_metrics").select("*").eq("campaign_id", campaign["id"]).execute(), | ||
| f"Failed to fetch metrics for campaign {campaign['id']}" | ||
| ) | ||
|
|
||
| # Get latest metrics for this campaign | ||
| latest_metrics = campaign_metrics[-1] if campaign_metrics else {} | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Latest metrics selection is unreliable. Order by timestamp and limit 1.
Relying on [-1] without ordering can pick arbitrary rows.
- campaign_metrics = safe_supabase_query(
- lambda: supabase.table("campaign_metrics").select("*").eq("campaign_id", campaign["id"]).execute(),
- f"Failed to fetch metrics for campaign {campaign['id']}"
- )
-
- # Get latest metrics for this campaign
- latest_metrics = campaign_metrics[-1] if campaign_metrics else {}
+ campaign_metrics = safe_supabase_query(
+ lambda: supabase.table("campaign_metrics")
+ .select("*")
+ .eq("campaign_id", campaign["id"])
+ .order("recorded_at", desc=True)
+ .limit(1)
+ .execute(),
+ f"Failed to fetch metrics for campaign {campaign['id']}"
+ )
+ latest_metrics = campaign_metrics[0] if campaign_metrics else {}🧰 Tools
🪛 Ruff (0.14.1)
304-304: Function definition does not bind loop variable campaign
(B023)
🤖 Prompt for AI Agents
In Backend/app/routes/brand_dashboard.py around lines 302 to 310, selecting the
"latest" metrics by indexing campaign_metrics[-1] is unreliable because the
query doesn't guarantee order; change the supabase query to order by the
timestamp (or created_at) field descending and limit to 1 so you fetch only the
newest row, then use the first element (if present) as latest_metrics; keep
using safe_supabase_query for the call and update the subsequent logic to handle
a single-row response (check for empty result before accessing index 0).
| <button | ||
| onClick={() => setShowCreateModal(true)} | ||
| style={{ | ||
| background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)', | ||
| color: '#fff', | ||
| border: 'none', | ||
| borderRadius: '12px', | ||
| padding: '12px 24px', | ||
| fontSize: '16px', | ||
| fontWeight: '600', | ||
| cursor: 'pointer', | ||
| display: 'flex', | ||
| alignItems: 'center', | ||
| gap: '8px', | ||
| transition: 'all 0.2s ease', | ||
| boxShadow: '0 4px 16px rgba(99, 102, 241, 0.3)' | ||
| }} | ||
| onMouseEnter={(e) => e.currentTarget.style.transform = 'translateY(-2px)'} | ||
| onMouseLeave={(e) => e.currentTarget.style.transform = 'translateY(0)'} | ||
| > | ||
| <Plus size={20} /> | ||
| Create Contract | ||
| </button> | ||
| </div> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Create button toggles state but no modal is rendered.
showCreateModal is never used in JSX, so “Create Contract” does nothing.
- const [showCreateModal, setShowCreateModal] = useState(false);
+ const [showCreateModal, setShowCreateModal] = useState(false);
...
- {/* Contract Details Modal */}
+ {/* Contract Details Modal */}
<ContractDetailsModal
contract={selectedContract}
onClose={() => setSelectedContract(null)}
/>
+ {/* Create Contract Modal (placeholder until backend wiring) */}
+ {showCreateModal && (
+ <div role="dialog" aria-modal="true" style={{ background: 'rgba(0,0,0,0.6)', position:'fixed', inset:0, display:'grid', placeItems:'center' }}>
+ <div style={{ background:'#1e1e1e', padding:24, borderRadius:12 }}>
+ <h3 style={{ marginBottom:12 }}>Create Contract</h3>
+ <p style={{ color:'#a0a0a0' }}>Form coming soon…</p>
+ <button onClick={() => setShowCreateModal(false)} style={{ marginTop:16 }}>Close</button>
+ </div>
+ </div>
+ )}Also applies to: 517-521
🤖 Prompt for AI Agents
In Frontend/src/pages/Contracts.tsx around lines 162-185 (also apply same fix at
517-521), the onClick sets showCreateModal but the JSX never reads it so no
modal appears; update the component to render the create-contract modal
conditionally when showCreateModal === true (e.g., {showCreateModal &&
<CreateContractModal .../>}), wire a close handler that calls
setShowCreateModal(false), pass any required props (callbacks to refresh list or
initial data), and ensure the CreateContractModal import is present at top of
the file; repeat the same conditional render/close wiring for the other
occurrence at lines 517-521.
| private async makeRequest<T>( | ||
| endpoint: string, | ||
| options: RequestInit = {} | ||
| ): Promise<T> { | ||
| const url = `${AI_API_BASE_URL}${endpoint}`; | ||
|
|
||
| try { | ||
| const response = await fetch(url, { | ||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| ...options.headers, | ||
| }, | ||
| ...options, | ||
| }); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Header merge bug can drop Content-Type.
...options after headers can overwrite the composed headers. Merge safely.
- const response = await fetch(url, {
- headers: {
- 'Content-Type': 'application/json',
- ...options.headers,
- },
- ...options,
- });
+ const { headers: optHeaders, ...rest } = options;
+ const mergedHeaders = {
+ 'Content-Type': 'application/json',
+ ...(optHeaders as Record<string, string> | undefined),
+ };
+ const response = await fetch(url, {
+ ...rest,
+ headers: mergedHeaders,
+ });📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| private async makeRequest<T>( | |
| endpoint: string, | |
| options: RequestInit = {} | |
| ): Promise<T> { | |
| const url = `${AI_API_BASE_URL}${endpoint}`; | |
| try { | |
| const response = await fetch(url, { | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| ...options.headers, | |
| }, | |
| ...options, | |
| }); | |
| private async makeRequest<T>( | |
| endpoint: string, | |
| options: RequestInit = {} | |
| ): Promise<T> { | |
| const url = `${AI_API_BASE_URL}${endpoint}`; | |
| try { | |
| const { headers: optHeaders, ...rest } = options; | |
| const mergedHeaders = { | |
| 'Content-Type': 'application/json', | |
| ...(optHeaders as Record<string, string> | undefined), | |
| }; | |
| const response = await fetch(url, { | |
| ...rest, | |
| headers: mergedHeaders, | |
| }); |
🤖 Prompt for AI Agents
In Frontend/src/services/aiApi.ts around lines 29 to 43, the current request
build spreads ...options after setting headers which can overwrite the composed
headers and drop Content-Type; fix by extracting headers from options first
(e.g., const { headers, ...rest } = options) and then pass fetch an options
object that sets headers: { 'Content-Type': 'application/json', ...(headers ||
{}) } and spreads rest (not the original options) so custom headers can override
the default safely without being overwritten.
| private async makeRequest<T>( | ||
| endpoint: string, | ||
| options: RequestInit = {} | ||
| ): Promise<T> { | ||
| const url = `${API_BASE_URL}${endpoint}`; | ||
|
|
||
| try { | ||
| const response = await fetch(url, { | ||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| ...options.headers, | ||
| }, | ||
| ...options, | ||
| }); | ||
|
|
||
| if (!response.ok) { | ||
| const errorData = await response.json().catch(() => ({})); | ||
| throw new Error(errorData.detail || `HTTP error! status: ${response.status}`); | ||
| } | ||
|
|
||
| return await response.json(); | ||
| } catch (error) { | ||
| console.error(`API Error (${endpoint}):`, error); | ||
| throw error; | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Handle 204/empty responses to avoid JSON parse errors.
DELETE endpoints and some PUTs may return no content.
- return await response.json();
+ const text = await response.text();
+ return (text ? JSON.parse(text) : undefined) as T;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| private async makeRequest<T>( | |
| endpoint: string, | |
| options: RequestInit = {} | |
| ): Promise<T> { | |
| const url = `${API_BASE_URL}${endpoint}`; | |
| try { | |
| const response = await fetch(url, { | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| ...options.headers, | |
| }, | |
| ...options, | |
| }); | |
| if (!response.ok) { | |
| const errorData = await response.json().catch(() => ({})); | |
| throw new Error(errorData.detail || `HTTP error! status: ${response.status}`); | |
| } | |
| return await response.json(); | |
| } catch (error) { | |
| console.error(`API Error (${endpoint}):`, error); | |
| throw error; | |
| } | |
| } | |
| private async makeRequest<T>( | |
| endpoint: string, | |
| options: RequestInit = {} | |
| ): Promise<T> { | |
| const url = `${API_BASE_URL}${endpoint}`; | |
| try { | |
| const response = await fetch(url, { | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| ...options.headers, | |
| }, | |
| ...options, | |
| }); | |
| if (!response.ok) { | |
| const errorData = await response.json().catch(() => ({})); | |
| throw new Error(errorData.detail || `HTTP error! status: ${response.status}`); | |
| } | |
| const text = await response.text(); | |
| return (text ? JSON.parse(text) : undefined) as T; | |
| } catch (error) { | |
| console.error(`API Error (${endpoint}):`, error); | |
| throw error; | |
| } | |
| } |
🤖 Prompt for AI Agents
In Frontend/src/services/brandApi.ts around lines 72 to 97 the makeRequest
method assumes every successful response has JSON and will throw when a 204 or
an empty-body response is returned; update the success path to detect no-content
responses (e.g., response.status === 204 or empty body/Content-Length === '0')
and return a sensible empty value (null or undefined cast to T) instead of
calling response.json(), otherwise read response.text() and if the text is empty
return null/undefined, else parse JSON and return it; keep existing error
handling for non-ok responses.
| async deleteCampaign(campaignId: string, brandId: string): Promise<void> { | ||
| return this.makeRequest<void>(`/campaigns/${campaignId}?brand_id=${brandId}`, { | ||
| method: 'DELETE', | ||
| }); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
DELETE/PUT callers rely on server body.
After 204 handling in makeRequest, ensure callers don’t assume JSON. Consider returning { ok: true } or void consistently.
Also applies to: 219-236
🏁 Script executed:
#!/bin/bash
# Find methods calling DELETE/PUT that return void across services
rg -nP 'makeRequest<\s*void\s*>\s*\(' -C2Length of output: 383
I need to investigate further to verify the review comment. Let me search for the additional methods mentioned and examine the makeRequest implementation.
---
🏁 Script executed:
```shell
#!/bin/bash
# Find the code at lines 219-236 in brandApi.ts to see what the review also applies to
cat Frontend/src/services/brandApi.ts | sed -n '219,236p'
Length of output: 593
🏁 Script executed:
#!/bin/bash
# Search for makeRequest implementation to understand 204 handling
rg -nP 'makeRequest\s*<' Frontend/src/services/brandApi.ts -A10 | head -50Length of output: 1740
🏁 Script executed:
#!/bin/bash
# Find all DELETE/PUT methods in brandApi.ts (not just void returns)
rg -nP "method:\s*['\"]?(DELETE|PUT)" Frontend/src/services/brandApi.ts -B5 -A3Length of output: 1426
I need to see the complete makeRequest implementation to understand how it handles 204 responses.
---
🏁 Script executed:
```shell
#!/bin/bash
# Get the complete makeRequest implementation
sed -n '72,100p' Frontend/src/services/brandApi.ts
Length of output: 775
🏁 Script executed:
#!/bin/bash
# Search for response parsing and 204 handling in makeRequest
rg -nP '(status|204|No Content|response\.)' Frontend/src/services/brandApi.ts -A1 -B1 | head -80Length of output: 957
🏁 Script executed:
#!/bin/bash
# Find callers of deleteCampaign to see how they handle the response
rg -nP 'deleteCampaign\s*\(' -B5 -A5 --type=ts --type=tsxLength of output: 27
Let me search more carefully for void-returning methods and their callers.
---
🏁 Script executed:
```shell
#!/bin/bash
# Find all methods returning Promise<void> in brandApi.ts
rg -nP 'Promise<\s*void\s*>' Frontend/src/services/brandApi.ts -B3
Length of output: 105
🏁 Script executed:
#!/bin/bash
# Find callers of deleteCampaign (corrected search without tsx flag)
rg -nP 'deleteCampaign\s*\(' --type=ts -B3 -A3Length of output: 1098
🏁 Script executed:
#!/bin/bash
# Check if there are other DELETE methods
rg -nP "method:\s*['\"]DELETE" Frontend/src/services/brandApi.ts -B5 -A2Length of output: 241
Handle 204 No Content responses in deleteCampaign.
The makeRequest method unconditionally calls response.json() for all successful responses (line 92), but a 204 No Content response (standard for DELETE operations) has no body. This will cause a JSON parsing error. The current code at lines 153-157 will fail if the server returns 204. Modify makeRequest to detect 204 status and return undefined instead of attempting to parse the response body.
🤖 Prompt for AI Agents
In Frontend/src/services/brandApi.ts around lines 153-157 (deleteCampaign) and
the makeRequest implementation around line ~92, makeRequest currently calls
response.json() for all successful responses which breaks on 204 No Content;
update makeRequest to check if response.status === 204 (or response.status >=
200 && response.status < 300 && response.status !== 204) and return undefined
immediately for 204 responses instead of calling response.json(), otherwise
continue to parse and return the JSON as before; ensure the return type remains
compatible with callers like deleteCampaign which expect Promise<void>.
| async updateApplicationStatus( | ||
| applicationId: string, | ||
| status: string, | ||
| notes?: string, | ||
| brandId?: string | ||
| ): Promise<Application> { | ||
| return this.makeRequest<Application>(`/applications/${applicationId}?brand_id=${brandId}`, { | ||
| method: 'PUT', | ||
| body: JSON.stringify({ status, notes }), | ||
| }); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
brandId is optional in signature but required in the URL.
Avoid brand_id=undefined. Make it required.
- async updateApplicationStatus(
- applicationId: string,
- status: string,
- notes?: string,
- brandId?: string
- ): Promise<Application> {
+ async updateApplicationStatus(
+ applicationId: string,
+ status: string,
+ notes: string | undefined,
+ brandId: string
+ ): Promise<Application> {🤖 Prompt for AI Agents
In Frontend/src/services/brandApi.ts around lines 202-212, the function accepts
an optional brandId but interpolates it into the URL causing
`brand_id=undefined`; make brandId required (remove the `?` from the parameter)
and update the function signature to require a string brandId, then ensure the
URL always includes the provided brandId; if you cannot change all call sites
immediately, add a defensive runtime check at the start of the function that
throws a clear error when brandId is missing so the request never includes
`undefined`.
📝 Description
This pull request introduces enhancements and new features to the contracts module in the backend and frontend. It includes updates to the database schema, API endpoints, and frontend components for managing contracts. These changes aim to improve the functionality, scalability, and user experience of the contracts system.
🔧 Changes Made
Backend:
Database Schema Updates:
Enhanced the contracts table with additional fields such as contract_title, contract_type, terms_and_conditions, payment_terms, deliverables, start_date, end_date, total_budget, and more.
Added new tables for contract_analytics and contract_notifications to support analytics and notifications.
API Enhancements:
Added endpoints for creating, updating, deleting, and fetching contracts.
Introduced analytics and notifications endpoints for contracts.
Implemented search functionality for contracts with filtering options.
Added support for updating contract status and marking notifications as read.
Error Handling:
Improved error handling for contract-related operations.
Frontend:
Contracts Page:
Updated the contracts page to display contract details, analytics, and notifications.
Added a modal for creating new contracts.
Enhanced the UI for managing contracts, including filtering and searching.
UI/UX Improvements:
Improved the design and layout of the contracts management interface.
✅ Checklist
Summary by CodeRabbit