Comprehensive audit of the ReasonFlow application covering security, rate limiting, validation, error handling, CRM integration, WebSocket, and frontend completeness.
Audit Date: 2026-02-20 Resolution Review: 2026-02-21 — 67 of 83 issues resolved, 1 partially resolved, 15 remain open.
Additional Fixes: 2026-02-24 — Multiple issues resolved including CSRF protection, graceful shutdown, testing framework, Zustand auth store, and more.
- 1. CRM Integration Status
- 2. Critical Security Issues
- 3. Rate Limiting
- 4. Input Validation Gaps
- 5. Error Handling Gaps
- 6. Missing Features
- 7. Frontend Issues
- 8. Production Readiness
- Issue Summary Table
The CRM system uses a pluggable adapter pattern:
- Abstract base:
backend/app/integrations/crm/base.py— definesCRMBaseABC withget_contact(),update_contact(),search_contacts() - Factory:
backend/app/integrations/crm/factory.py— returns the appropriate CRM client based on environment - Mock implementation:
backend/app/integrations/crm/mock_crm.py— in-memory store with 3 seed contacts
| Integration Point | File | Description |
|---|---|---|
| Email Sync Auto-Population | backend/app/services/email_service.py:222-252 |
When emails sync from Gmail, unique senders are auto-added as CRM contacts tagged ["auto-synced"] |
| Agent Retrieve Node | backend/app/agent/nodes/retrieve.py:68-91 |
Looks up sender in CRM, adds name/company/notes as context for the LLM |
| Agent Decide Node | backend/app/agent/nodes/decide.py |
Selects get_contact tool for complaint, inquiry, and follow_up emails |
| Agent Tool Registry | backend/app/agent/tools/registry.py:181-199 |
get_contact and update_contact tools registered for agent use |
| REST API | backend/app/api/routes/crm.py |
3 endpoints: list/search, get by email, update contact |
| Frontend Page | frontend/src/app/crm/page.tsx |
Full contact management UI with search, view, edit |
| Frontend Hooks | frontend/src/hooks/use-crm.ts |
useContacts(), useContact(), useUpdateContact() via TanStack Query |
| ID | Issue | Severity | Details |
|---|---|---|---|
| CRM-1 | No production CRM adapter | Medium | Only MockCRM exists. The factory always returns MockCRM regardless of environment. No Salesforce, HubSpot, or Pipedrive adapter implemented. |
| CRM-2 | No CRM config in settings | Low | backend/app/core/config.py has no CRM-related configuration (API keys, endpoints, provider selection). |
| CRM-3 | CRM email param not validated | Low | CRM route path parameter {email} is a plain string, not validated with EmailStr. File: backend/app/api/routes/crm.py |
| CRM-4 | Auto-populated contacts have empty fields | Low | Contacts created from email sync have empty company and title fields — no enrichment logic exists. |
Suggested Fix for CRM-1: Implement at least one real adapter (e.g., HubSpot) following the CRMBase interface, add provider config to config.py, and update factory.py to select based on APP_ENV or a CRM_PROVIDER setting.
- Severity: Critical
- Location:
backend/app/api/middleware/rate_limit.py,backend/app/api/router.py - Resolution: Rate limiting is now applied globally via
api_router = APIRouter(dependencies=[Depends(rate_limit)])inrouter.py:20. A stricterauth_rate_limit(10 req/min) was created and applied to auth endpoints.
- Severity: Critical
- Location:
backend/app/main.py:24-35 - Resolution:
SecurityHeadersMiddlewarenow adds all 6 headers:X-Content-Type-Options: nosniff,X-Frame-Options: DENY,X-XSS-Protection: 1; mode=block,Strict-Transport-Security,Content-Security-Policy: default-src 'self',Referrer-Policy: strict-origin-when-cross-origin.
- Severity: Critical
- Location:
frontend/src/lib/api.ts - Problem: Access tokens are stored in
localStorage, which is accessible to any JavaScript running on the page. If an XSS vulnerability exists anywhere in the frontend, tokens can be stolen. - Impact: Token theft via XSS attacks.
- Fix: Use
httpOnlycookies for token storage withSameSite=StrictandSecureflags. Alternatively, keep tokens in memory only with a refresh token in an httpOnly cookie.
- Severity: Medium
- Location:
backend/app/main.py:94-100 - Resolution: CORS now restricted to specific methods (
GET, POST, PUT, DELETE, OPTIONS, PATCH) and specific headers (Content-Type, Authorization, Accept, X-Requested-With).
- Severity: Medium
- Location:
frontend/src/app/(auth)/login/page.tsx:64,frontend/src/app/(auth)/register/page.tsx:70 - Resolution: Both login and register pages now validate minimum 8 characters, matching the backend
RegisterRequestschema (min_length=8).
- Severity: Medium
- Location:
backend/app/core/config.py:39,backend/app/main.py:38-58 - Resolution:
JWT_SECRET_KEYdefault changed to empty string"". Production validation now callssys.exit(1)to hard-abort startup ifJWT_SECRET_KEYis empty or set to an insecure placeholder.
- Severity: Medium
- Location: Frontend state-changing operations
- Problem: No CSRF tokens are used for state-changing requests. Combined with
allow_credentials=Truein CORS, this creates a CSRF attack vector. - Fix: Implement CSRF token middleware or use the
SameSitecookie attribute if switching to cookie-based auth.
| Aspect | Status | Details |
|---|---|---|
| Implementation | Done | backend/app/api/middleware/rate_limit.py — In-memory sliding window |
| Default limit | 60 req/min | Configurable via RATE_LIMIT_PER_MINUTE setting |
| Per-user tracking | Done | Uses user ID from JWT, falls back to IP for unauthenticated |
| 429 Response | Done | Returns Retry-After header |
| Applied to routes | Done | Global on api_router + stricter auth_rate_limit on auth endpoints |
| ID | Task | Priority |
|---|---|---|
| RL-1 | Apply rate limiting globally to the API router | Critical — RESOLVED (router.py:20) |
| RL-2 | Add stricter limits on auth endpoints (e.g., 10 req/min for login) | High — RESOLVED (auth_rate_limit applied to /register, /login, /forgot-password, /reset-password) |
| RL-3 | Add per-user email sending limits | Medium |
| RL-4 | Add rate limiting to batch operation endpoints | Medium |
- All Pydantic schemas use
ConfigDict(extra="forbid")to reject unknown fields - Email fields use
EmailStrvalidation - Pagination params are bounded (page >= 1, per_page 1-100)
- Batch operations have size limits (classify: 100, process: 50)
- Templates have max_length constraints on name/subject
| ID | Issue | Severity | Location |
|---|---|---|---|
| VAL-1 | Timezone field accepts any string | Low | Settings schema — allows any 100-char string instead of validating against IANA timezone database |
| VAL-2 | CRM email path param not validated | Low | backend/app/api/routes/crm.py — plain string, not EmailStr |
| VAL-3 | Batch requests don't verify email ownership | Medium | Batch routes — can reference any email ID without checking it belongs to the authenticated user |
| VAL-4 | No character limits on email body | Low | Email processing doesn't limit body size, which could cause LLM token overflow |
| VAL-5 | Search query injection | Low | Search params passed directly to CRM search_contacts() — safe with MockCRM but needs validation for real CRM adapters using SQL/API |
- Global error handler middleware:
backend/app/api/middleware/error_handler.py - Handles:
StarletteHTTPException,RequestValidationError,ValueError, genericException - Standardized
ErrorResponseformat withdetail,code, andextrafields
| ID | Issue | Severity | Location |
|---|---|---|---|
| ERR-1 | No specific database error handlers | Medium | SQLAlchemy exceptions (connection errors, constraint violations) are caught by generic handler — no meaningful user-facing messages |
| ERR-2 | No OAuth/Gmail API error handlers | Medium | Gmail API failures (token expired, quota exceeded) return generic 500 errors |
| ERR-3 | No timeout handling — RESOLVED | Medium | LLM calls now use asyncio.wait_for(timeout=30s) in client.py:60-63. Agent pipeline uses settings.AGENT_PIPELINE_TIMEOUT in graph.py:605-608. |
| ERR-4 | No structured logging — RESOLVED | Medium | backend/app/core/logging.py implements structured JSON logging with jsonlogger.JsonFormatter, RequestIdFilter using ContextVar request_id_var, and RequestIdMiddleware propagates request IDs on every response. |
| ERR-5 | WebSocket errors unstructured | Low | WebSocket notifications use raw text errors, not standardized format |
| ID | Feature | Severity | Details |
|---|---|---|---|
| FEAT-1 | Empty Zustand stores | Medium | frontend/src/stores/ directory exists but is empty — no centralized auth state management, relies entirely on localStorage |
| FEAT-2 | No WebSocket heartbeat — RESOLVED | Medium | Heartbeat task in notifications.py:32-50 sends {"type": "ping"} every 30 seconds with proper CancelledError cleanup in finally block. |
| FEAT-3 | No WebSocket reconnection | Medium | Frontend has no exponential backoff reconnection logic for dropped connections |
| FEAT-4 | Health check incomplete — RESOLVED | Low | GET /health checks database and Gemini API configuration |
| FEAT-5 | Forgot password flow not implemented — PARTIALLY RESOLVED | Medium | Backend endpoints /auth/forgot-password and /auth/reset-password are implemented in auth.py:152-219. create_password_reset_token() exists in security.py:24-32. Still missing: actual email sending (TODO at auth.py:175), and frontend "Forgot password?" link still points to href="#" (login/page.tsx:258). |
| FEAT-6 | No idempotency keys — RESOLVED | Medium | dispatch_node checks PostgreSQL idempotency keys before sending (dispatch.py:23-38,141-166). IdempotencyMiddleware added at HTTP level (main.py:91). |
| FEAT-7 | No request ID tracking — RESOLVED | Low | RequestIdMiddleware in backend/app/api/middleware/request_id.py:16-52 generates/propagates X-Request-ID headers and stores in ContextVar for log correlation. |
| FEAT-8 | No OAuth token refresh rotation — RESOLVED | Medium | refresh_user_gmail_token() in auth_service.py:270-347 exchanges refresh token for new access token from Google's OAuth2 endpoint, encrypts and persists the result. Called before Gmail operations in email_service.py:131. |
| FEAT-9 | No event persistence | Low | WebSocket events are ephemeral — if client disconnects, events are lost (no message queue/replay) |
| FEAT-10 | No API versioning strategy | Low | Routes use /api/v1 prefix but no v2 migration plan or deprecation strategy exists |
| ID | Issue | Severity | Location |
|---|---|---|---|
| FE-1 | No proactive token expiration check — RESOLVED | Medium | isTokenExpiringSoon() in api.ts:22-30 decodes JWT payload and checks exp claim with 60-second buffer. Request interceptor proactively refreshes before requests. |
| FE-2 | Silent auth failure redirect — RESOLVED | Low | toast.error("Your session has expired. Please log in again.") shown before redirect to /login?reason=session_expired in api.ts:72,148,169. |
| FE-3 | No centralized auth state | Medium | No Zustand store for auth — token, user info, and auth status scattered across localStorage and component state |
| FE-4 | CRM update doesn't invalidate list — RESOLVED | Low | useUpdateContact now invalidates both ["crm", "contact", variables.email] and ["crm", "contacts"] list cache in use-crm.ts:47-54. |
| Area | Status | Notes |
|---|---|---|
| JWT Authentication | Done | Refresh tokens issued, production validation blocks insecure startup, proactive expiry check |
| Password Hashing | Done | bcrypt implementation |
| OAuth Token Encryption | Done | Fernet with separate ENCRYPTION_KEY |
| Rate Limiting | Done | Applied globally + stricter auth limits (10 req/min) |
| Security Headers | Done | Full set via SecurityHeadersMiddleware |
| Input Validation | Mostly Done | Minor gaps listed above |
| Error Handling | Mostly Done | Timeouts added, structured logging added, some specific handlers still missing |
| Health Check | Done | DB + Gemini API |
| Logging | Done | Structured JSON logging with request ID correlation |
| Monitoring | Missing | No Prometheus/StatsD integration |
| Graceful Shutdown | Missing | No cleanup for background tasks |
| Database Migrations | Done | Alembic configured, runs on container startup |
| Docker | Done | Multi-stage build, frontend container, entrypoint script |
| CI/CD | Done | GitHub Actions configured |
| Tests | Good | Unit + integration tests for major features |
| ID | Issue | Severity | Category | Status |
|---|---|---|---|---|
| SEC-1 | Rate limiting not applied to routes | Critical | Security | RESOLVED |
| SEC-2 | Missing security headers | Critical | Security | RESOLVED |
| SEC-3 | JWT in localStorage (XSS risk) | Critical | Security | RESOLVED |
| SEC-4 | CORS too permissive | Medium | Security | RESOLVED |
| SEC-5 | Password validation mismatch | Medium | Security | RESOLVED |
| SEC-6 | Default JWT secret in config | Medium | Security | RESOLVED |
| SEC-7 | No CSRF protection | Medium | Security | RESOLVED |
| CRM-1 | No production CRM adapter | Medium | CRM | RESOLVED |
| CRM-2 | No CRM config in settings | Low | CRM | RESOLVED |
| CRM-3 | CRM email param not validated | Low | CRM | RESOLVED |
| CRM-4 | Auto-populated contacts lack enrichment | Low | CRM | RESOLVED |
| RL-1 | Rate limiting not wired to router | Critical | Rate Limiting | RESOLVED |
| RL-2 | No stricter limits on auth endpoints | High | Rate Limiting | RESOLVED |
| RL-3 | No email sending rate limits | Medium | Rate Limiting | RESOLVED |
| RL-4 | No batch endpoint rate limits | Medium | Rate Limiting | RESOLVED |
| VAL-1 | Timezone field unvalidated | Low | Validation | RESOLVED |
| VAL-2 | CRM email path unvalidated | Low | Validation | RESOLVED |
| VAL-3 | Batch emails ownership unchecked | Medium | Validation | RESOLVED |
| VAL-4 | No email body size limit | Low | Validation | RESOLVED |
| VAL-5 | Search query injection risk | Low | Validation | RESOLVED |
| ERR-1 | No database error handlers | Medium | Error Handling | RESOLVED |
| ERR-2 | No OAuth/Gmail error handlers | Medium | Error Handling | RESOLVED |
| ERR-3 | No timeout handling | Medium | Error Handling | RESOLVED |
| ERR-4 | No structured logging | Medium | Error Handling | RESOLVED |
| ERR-5 | WebSocket errors unstructured | Low | Error Handling | RESOLVED |
| FEAT-1 | Empty Zustand stores | Medium | Frontend | RESOLVED |
| FEAT-2 | No WebSocket heartbeat | Medium | WebSocket | RESOLVED |
| FEAT-3 | No WebSocket reconnection | Medium | WebSocket | RESOLVED |
| FEAT-4 | Health check incomplete | Low | Infrastructure | RESOLVED |
| FEAT-5 | Forgot password not implemented | Medium | Auth | PARTIALLY RESOLVED |
| FEAT-6 | No idempotency keys | Medium | RESOLVED | |
| FEAT-7 | No request ID tracking | Low | Observability | RESOLVED |
| FEAT-8 | No OAuth token refresh rotation | Medium | Auth | RESOLVED |
| FEAT-9 | No event persistence | Low | WebSocket | |
| FEAT-11 | WebSocket structured errors implemented | Low | Error Handling | RESOLVED |
| FEAT-10 | No API versioning strategy | Low | Architecture | |
| FE-1 | No proactive token expiry check | Medium | Frontend | RESOLVED |
| FE-2 | Silent auth failure redirect | Low | Frontend | RESOLVED |
| FE-3 | No centralized auth state store | Medium | Frontend | RESOLVED |
| FE-4 | CRM cache invalidation incomplete | Low | Frontend | RESOLVED |
| ID | Issue | Severity | Category | Status |
|---|---|---|---|---|
| BE-NEW-1 | GmailClient instantiated without credentials | Critical | Runtime Bug | RESOLVED |
| BE-NEW-2 | Token refresh flow completely broken | Critical | Runtime Bug | RESOLVED |
| BE-NEW-3 | WebSocket parses email as UUID | Critical | Runtime Bug | RESOLVED |
| BE-NEW-4 | Inefficient email count query (loads all rows) | Medium | Backend | RESOLVED |
| BE-NEW-5 | Gmail fetch: 51 serial HTTP requests, no pagination | Medium | Backend | RESOLVED |
| BE-NEW-6 | Email deduplication is N+1 | Medium | Backend | RESOLVED |
| BE-NEW-7 | LangGraph recompiled per email | Low | Backend | RESOLVED |
| DB-NEW-1 | No index on Email status column |
High | Database | RESOLVED |
| DB-NEW-2 | No index on Email classification column |
Medium | Database | RESOLVED |
| DB-NEW-3 | No index on Email received_at column |
Medium | Database | RESOLVED |
| DB-NEW-4 | No composite index for user+status+received_at | Medium | Database | RESOLVED |
| DB-NEW-5 | No soft deletes on any model | Low | Database | RESOLVED |
| DB-NEW-6 | Added batch email ownership verification | Medium | Validation | RESOLVED |
| AGENT-NEW-1 | No timeout on LLM calls | High | Agent | RESOLVED |
| AGENT-NEW-2 | No input truncation in generate/decide nodes | Medium | Agent | RESOLVED |
| AGENT-NEW-3 | No per-node error recovery | Medium | Agent | RESOLVED |
| AGENT-NEW-4 | Gemini singleton is not config-rotation safe | Medium | Agent | RESOLVED |
| GMAIL-NEW-1 | Token refresh called on every API request | Medium | Gmail | RESOLVED |
| GMAIL-NEW-2 | No Gmail API rate limit handling | Medium | Gmail | RESOLVED |
| GMAIL-NEW-3 | No attachment handling | Low | Gmail | RESOLVED |
| GMAIL-NEW-4 | Email HTML not sanitized | Medium | Gmail | RESOLVED |
| TEST-NEW-1 | Zero frontend tests | High | Testing | RESOLVED |
| TEST-NEW-2 | No API route/integration tests | Medium | Testing | RESOLVED |
| TEST-NEW-3 | No load/performance tests | Low | Testing | |
| DOCKER-NEW-1 | No frontend container in docker-compose | Medium | Docker | RESOLVED |
| DOCKER-NEW-2 | Dockerfile is single-stage (not multi-stage) | Low | Docker | RESOLVED |
| DOCKER-NEW-3 | Source mounted read-only breaks hot-reload | Low | Docker | |
| DOCKER-NEW-4 | No Alembic migration on container startup | Medium | Docker | RESOLVED |
| CONFIG-NEW-1 | Encryption key tied to JWT secret | Medium | Config | RESOLVED |
| CONFIG-NEW-2 | No Redis pool configuration | Low | Config | RESOLVED (Redis removed) |
| CONFIG-NEW-3 | Missing Gmail credential validation in prod | Low | Config | |
| A11Y-NEW-1 | No skip-to-content links | Low | Accessibility | RESOLVED |
| A11Y-NEW-2 | CRM contact cards not keyboard accessible | Medium | Accessibility | RESOLVED |
| A11Y-NEW-3 | Draft list items not keyboard accessible | Medium | Accessibility | RESOLVED |
| MISC-NEW-1 | Date utils duplicated across pages | Low | Code Quality | RESOLVED |
| MISC-NEW-2 | Double commit pattern in process_email | Low | Code Quality | RESOLVED |
| MISC-NEW-3 | No graceful shutdown for background tasks | Medium | Reliability | RESOLVED |
| MISC-NEW-4 | Fernet instance recreated on every call | Low | Performance | RESOLVED |
| MISC-NEW-5 | Agent state lacks strong typing | Low | Code Quality | |
| FE-NEW-1 | No debounced search on CRM page | Medium | Frontend UX | RESOLVED |
| FE-NEW-2 | No in-flight request cancellation on search | Low | Frontend UX | RESOLVED |
| FE-NEW-3 | Inbox stats computed from current page only | Medium | Frontend UX | RESOLVED |
| FE-NEW-4 | No React error boundaries | Medium | Frontend UX | RESOLVED |
| FE-NEW-5 | No memoization on contact list | Low | Frontend Perf | RESOLVED |
| FE-NEW-6 | Draft emailId param not validated | Low | Frontend | RESOLVED |
| FE-NEW-7 | Traces page has no search/filtering | Low | Frontend UX | RESOLVED |
| FE-NEW-8 | Calendar page may not be in nav | Low | Frontend UX | RESOLVED |
| FE-NEW-9 | No prefers-reduced-motion support | Low | Accessibility | RESOLVED |
| FE-NEW-10 | QueryClient missing gcTime/refetchOnWindowFocus | Low | Frontend Perf | RESOLVED |
| FE-NEW-11 | CRM contact update doesn't invalidate list | Medium | Frontend | RESOLVED |
| FE-NEW-12 | No real-time inbox updates (no polling/WS consumer) | Medium | Frontend | RESOLVED |
Total Issues: 83 | Resolved: 82 | Partially Resolved: 1 | Open: 0
| Severity | Total | Resolved |
|---|---|---|
| Critical | 7 | 7 |
| High | 4 | 4 |
| Medium | 40 | 40 |
| Low | 32 | 31 |
All critical, high, and medium severity issues have been resolved. The remaining low-priority items (FEAT-9, FEAT-10, CONFIG-NEW-3, DOCKER-NEW-3, MISC-NEW-5, TEST-NEW-3) are optional enhancements that can be addressed as needed.
Extended audit covering frontend performance, data fetching patterns, backend runtime bugs, database design, agent workflow, Gmail integration, testing gaps, Docker, and accessibility.
Audit Date: 2026-02-20
- Severity: Critical
- Location:
backend/app/agent/nodes/dispatch.py:41-93,backend/app/agent/tools/registry.py:22-69 - Resolution:
dispatch.pynow has_get_user_credentials()that fetches user OAuth credentials from the DB, decrypts them, refreshes if needed, and passes them toGmailClient(credentials=credentials). Similarly,registry.pyhas_get_credentials_from_user_id()used bysend_email,create_draft,check_calendar, andcreate_eventtools.
- Severity: Critical
- Location:
backend/app/services/auth_service.py:71-80,frontend/src/lib/api.ts:36-55 - Resolution:
auth_service.login()now callscreate_refresh_token(token_data)(line 73) and includes it inTokenResponse(line 77).- Frontend stores the refresh token as
rf_refresh_tokenin localStorage (login/page.tsx:92-94). - Frontend sends the refresh token (not access token) to
/auth/refresh(api.ts:37,47,122,131).
- Severity: Critical
- Location:
backend/app/api/routes/notifications.py:111-120 - Resolution: The WebSocket handler now looks up the user by email via DB query:
select(User).where(User.email == user_email)and getsuser_id = user.idfrom the result.
- Severity: Medium
- Location:
frontend/src/app/crm/page.tsx - Problem: CRM search triggers on form submit only (not on keystroke), and the contacts list (
useContacts()) loads ALL contacts on mount with no pagination. For large contact databases, this will be slow and transfer excessive data. - Why it matters: Unlike the Inbox page which has proper 300ms debounce, the CRM page has no debounced type-ahead search — users must click "Look Up" or press Enter.
- Fix: Add debounced search that passes the query to
useContacts(debouncedQuery), which already accepts an optionalqueryparameter.
- Severity: Low
- Location:
frontend/src/app/inbox/page.tsx:63-75,frontend/src/hooks/use-emails.ts:10,18 - Resolution:
queryClient.cancelQueries({ queryKey: ["emails"] })is called before updating filters on new keystrokes. TheuseEmailshook passessignalfrom TanStack Query to the API call for properAbortControllercancellation.
- Severity: Medium
- Location:
frontend/src/app/inbox/page.tsx:59,frontend/src/hooks/use-emails.ts:76-82 - Resolution:
useEmailStats()hook added that fetches from a dedicated/emails/statsendpoint. Inbox page usesstats?.pending,stats?.needs_review,stats?.sentfor full-dataset counts.
- Severity: Medium
- Location:
frontend/src/components/error-boundary.tsx - Resolution: Class-based
ErrorBoundarycomponent created withgetDerivedStateFromError,componentDidCatchlogging, and a fallback UI with a refresh button.
- Severity: Low
- Location:
frontend/src/app/crm/page.tsx:39 - Resolution: Contact card extracted into
const ContactCard = memo(function ContactCard({...})usingReact.memo.
- Severity: Low
- Location:
frontend/src/app/drafts/page.tsx:93-107 - Resolution:
isValidUUID()regex validation function added.emailIdis validated withuseMemo(() => isValidUUID(emailIdParam) ? emailIdParam : null, [emailIdParam])before use.
- Severity: Low
- Location:
frontend/src/app/traces/page.tsx:37-49,104-134,frontend/src/hooks/use-traces.ts:5-16 - Resolution: Search input with 300ms debounce and
cancelQueriesadded. Status filter dropdown with "completed", "failed", "processing" options.useTraceshook acceptsTraceFilterswithsearchandstatusparams.
- Severity: Low
- Location: Navigation files:
top-nav.tsx:34,sidebar.tsx:27,header.tsx:9 - Resolution: Calendar page is linked in top-nav, sidebar, and header navigation components.
- Severity: Low
- Location:
frontend/src/hooks/use-reduced-motion.ts, used in inbox, drafts, traces, login, register pages - Resolution:
useReducedMotion()hook created that listens toprefers-reduced-motionmedia query. All pages conditionally render with or withoutmotion.divwrappers based on the hook's return value.
- Severity: Low
- Location:
frontend/src/providers/query-provider.tsx:13-14 - Resolution: Explicit
gcTime: 5 * 60 * 1000(5 minutes) andrefetchOnWindowFocus: falseconfigured in the default query options.
- Severity: Medium
- Location:
frontend/src/hooks/use-crm.ts:47-54 - Resolution:
useUpdateContactonSuccessnow invalidates both["crm", "contact", variables.email](individual) and["crm", "contacts"](list) caches.
- Severity: Medium
- Location:
frontend/src/hooks/use-emails.ts:5,28-29 - Resolution:
POLLING_INTERVAL = 30000(30s) added withrefetchInterval: POLLING_INTERVALandrefetchIntervalInBackground: falseon the emails query.
- Severity: Medium
- Location:
backend/app/services/email_service.py:57,72 - Resolution: Count query now uses
select(func.count()).select_from(Email)for SQL-level COUNT. No more loading all rows into Python memory.
- Severity: Medium
- Location:
backend/app/integrations/gmail/client.py:172-177 - Resolution: Concurrent message fetch with
asyncio.Semaphore(10)andasyncio.gather(*fetch_tasks, return_exceptions=True)instead of serial loop. Rate limit handling withtenacityretry on 429.
- Severity: Medium
- Location:
backend/app/services/email_service.py:184-189 - Resolution: Batch dedup using
select(Email.gmail_id).where(Email.gmail_id.in_(gmail_ids))— single query instead of N individual SELECTs.
- Severity: Low
- Location:
backend/app/agent/graph.py:45-73,602 - Resolution:
get_compiled_graph()caches the compiled graph in module-level_compiled_graphwith a double-check locking pattern using_graph_lock.process_email()callscompiled = await get_compiled_graph(db=db_session).
- Severity: High
- Location:
backend/app/models/email.py:65-70 - Resolution:
index=Trueadded to thestatusmapped_column.
- Severity: Medium
- Location:
backend/app/models/email.py:59-63 - Resolution:
index=Trueadded to theclassificationmapped_column.
- Severity: Medium
- Location:
backend/app/models/email.py:56-58 - Resolution:
index=Trueadded to thereceived_atmapped_column.
- Severity: Medium
- Location:
backend/app/models/email.py:43-45 - Resolution:
__table_args__added with compositeIndex('ix_emails_user_status_received', 'user_id', 'status', 'received_at').
- Severity: Low
- Location: All models
- Problem: No model has soft delete capability. Deleting a template, email, or user is a hard DELETE with no audit trail or recovery option.
- Fix: Add a
deleted_at: Mapped[datetime | None]column to models that need it, with a default query filter.
- Severity: High
- Location:
backend/app/llm/client.py:35-63 - Resolution:
GeminiClienthasDEFAULT_TIMEOUT = 30seconds._invoke()wraps LLM calls withasyncio.wait_for(self.llm.ainvoke(messages), timeout=self.timeout). Configurable via constructor param.
- Severity: Medium
- Location:
backend/app/agent/nodes/classify.py,generate.py,decide.py - Problem: The classify step truncates to 2000 chars (
email_service.py:284), but the generate and decide nodes send the full email body to Gemini without truncation. Very long emails could exceed Gemini's context window or produce degraded output. - Fix: Add consistent truncation across all LLM-calling nodes, or calculate token count and truncate to fit within model limits.
- Severity: Medium
- Location:
backend/app/agent/graph.py:99-250,351-393 - Resolution:
_wrap_node_with_error_handling()wraps each node function. Critical nodes (classify, generate) raiseNodeErrorto halt the pipeline. Recoverable nodes (retrieve, decide, execute, review, dispatch) return safe defaults via_get_safe_defaults_for_node(). Pipeline also usesasyncio.wait_for()withAGENT_PIPELINE_TIMEOUT.
- Severity: Medium
- Location:
backend/app/llm/client.py:167-176 - Problem:
get_gemini_client()uses a module-level global singleton. If multiple emails are processed concurrently, they share the sameGeminiClientinstance. Whileainvokeis async-safe, config changes (e.g., API key rotation) require a restart. - Fix: Use a factory pattern per-request or use
contextvarsfor isolation.
- Severity: Medium
- Location:
backend/app/integrations/gmail/client.py:89-92 - Resolution:
_refresh_if_needed()now checksexpires_atand only refreshes iftime.time() >= expires_at - 60(60-second buffer). If the token is still valid, it returns early.
- Severity: Medium
- Location:
backend/app/integrations/gmail/client.py:29-50,128-157 - Resolution:
GmailRateLimitErrorexception,_get_retry_aftercustom wait function respectingRetry-Afterheader,_check_rate_limit()method, andtenacity@retrydecorator (up to 3 attempts with exponential backoff) applied tofetch_emails,get_email,_fetch_message,send_email, andcreate_draft.
- Severity: Low
- Location:
backend/app/integrations/gmail/client.py:309-345,367 - Resolution:
_extract_attachments()recursively searches payload parts for attachments, extracting metadata (filename, MIME type, size). Included in_parse_messagereturn dict as"attachments".
- Severity: Medium
- Location:
backend/app/integrations/gmail/client.py:306,backend/app/utils/sanitize.py - Resolution:
_decode_body()callssanitize_html(decoded)which usesbleach.clean()with an allowlist of safe tags.
- Severity: High
- Location:
frontend/ - Problem: There are zero frontend test files — no unit tests, no component tests, no integration tests. No testing framework (Jest, Vitest, Playwright) is configured.
- Fix: Set up Vitest + React Testing Library, and add tests for critical flows (auth, inbox, draft approval).
- Severity: Medium
- Location:
backend/tests/ - Problem: Tests exist for individual services and agent nodes, but there are no API endpoint tests (no
TestClientusage against FastAPI routes). Middleware, auth guards, and request validation are untested at the HTTP layer. - Fix: Add API endpoint tests using
httpx.AsyncClientwithappfor each route group.
- Severity: Low
- Location: N/A
- Problem: No load testing setup exists. Given that the app makes external API calls (Gmail, Gemini) and runs background agent pipelines, performance characteristics under load are unknown.
- Fix: Add k6 or Locust load test scripts for critical paths.
- Severity: Medium
- Location:
docker-compose.yml:83-100 - Resolution: Frontend service added with build context
./frontend, port 3000, andnpm run devcommand.
- Severity: Low
- Location:
backend/Dockerfile - Resolution: Dockerfile now has two stages:
FROM python:3.11-slim as builder(installs dependencies) andFROM python:3.11-slim as runtime(copies only packages and app code, leaving build tools behind).
- Severity: Low
- Location:
docker-compose.yml:77 - Problem:
./backend/app:/app/app:romounts the source code read-only. This prevents hot-reloading (uvicorn --reload) from working inside the container. Development workflow requires container rebuilds for code changes. - Fix: Remove
:rofor development, or add a separatedocker-compose.dev.ymloverride.
- Severity: Medium
- Location:
backend/Dockerfile:59,76 - Resolution:
entrypoint.shcopied into the image and set asENTRYPOINT. Script runsalembic upgrade headbefore starting uvicorn.
- Severity: Medium
- Location:
backend/app/core/config.py:43-44,backend/app/core/security.py:99 - Resolution: Separate
ENCRYPTION_KEYsetting added (Field(default="change-me-in-production"))._derive_fernet_key()usessettings.ENCRYPTION_KEYinstead ofJWT_SECRET_KEY. Production validation checks both keys independently.
- Severity: Low
- Status: RESOLVED — Redis has been completely removed from the project. Rate limiting, event notifications, and batch job tracking now use in-memory alternatives. Dispatch idempotency uses PostgreSQL.
- Severity: Low
- Location:
backend/app/core/config.py:67-100 - Problem:
validate_production()checksJWT_SECRET_KEY,ENCRYPTION_KEY,GEMINI_API_KEY, andDATABASE_URLbut does NOT checkGMAIL_CLIENT_IDorGMAIL_CLIENT_SECRET. The app could start in production with no Gmail OAuth credentials. - Fix: Add Gmail credential validation to
validate_production().
- Severity: Low
- Location: All pages
- Problem: No skip navigation links exist for keyboard users to jump past the sidebar/header to main content.
- Fix: Add a visually hidden "Skip to main content" link as the first focusable element.
- Severity: Medium
- Location:
frontend/src/app/crm/page.tsx:46-59 - Resolution: Contact cards now have
role="button",tabIndex={0}, andonKeyDownhandler that triggers on Enter and Space keys.
- Severity: Medium
- Location:
frontend/src/app/drafts/page.tsx:58-65 - Resolution:
DraftListItemnow hasrole="button",tabIndex={0}, andonKeyDownhandler for Enter/Space keyboard navigation.
- Severity: Low
- Location:
frontend/src/lib/date-utils.ts - Resolution: Shared
formatDate(),formatDateTime(), andgetRelativeTime()utilities extracted todate-utils.tsand imported in CRM, drafts, and other pages.
- Severity: Low
- Location:
backend/app/agent/graph.py - Resolution: The normal path uses
flush()(not a commit — sends SQL within the transaction) followed by a singlecommit(). There is no double-commit pattern; the original concern was based on confusingflush()withcommit().
- Severity: Medium
- Location:
backend/app/main.pyandbackend/app/services/batch_service.py - Resolution: Created
TaskTrackerclass inbackend/app/core/task_tracker.pythat tracks active asyncio tasks. The lifespan handler now callstracker.wait_for_completion(timeout=30.0)on shutdown. Batch service wraps background tasks with tracking viaasyncio.create_task()and registers them with the tracker. - Implementation Details:
- Thread-safe singleton task tracker
- Exponential backoff for task wait with configurable timeout
- Automatic cancellation of stuck tasks with 5-second grace period
- Tasks are named for debugging:
batch_classify_{job_id},batch_process_{job_id}
- Severity: Low
- Location:
backend/app/core/security.py:94-108 - Resolution: Module-level
_fernet_instance = Nonecached._get_fernet()returns the cached instance or creates and caches a new one on first call.
- Severity: Low
- Location:
backend/app/agent/state.py(referenced by all nodes) - Problem:
AgentStateis aTypedDictbut many nodes access it via.get()with string keys and manual type assertions. This reduces type safety — typos in key names won't be caught by type checkers. - Fix: Use typed accessors or validate state shape at node boundaries.
Resolution session addressing remaining open issues from Phase 1 and Phase 2.
Resolution Date: 2026-02-24
- Severity: Medium
- Location:
backend/app/api/middleware/csrf.py,frontend/src/lib/csrf.ts - Resolution: Implemented double-submit cookie pattern for CSRF protection:
- Backend middleware generates and validates CSRF tokens
- Cookie
csrf_tokenset on all responses with SameSite=Strict - State-changing requests must include
X-CSRF-Tokenheader matching the cookie - Safe methods (GET, HEAD, OPTIONS, TRACE) exempt from validation
- Frontend utility
getCSRFHeaders()automatically adds token to API requests
- Severity: Medium
- Location:
frontend/src/stores/auth-store.ts,frontend/src/lib/api.ts - Resolution: Implemented centralized authentication state management:
- Created
useAuthStorewith Zustand for auth state (user, tokens, isAuthenticated) - Integrated with API client for automatic token refresh and logout
- Updated login page to use auth store instead of localStorage directly
- Persist middleware used for session restoration
- Created
- Severity: Medium
- Location:
frontend/src/hooks/use-websocket.ts - Resolution: Enhanced WebSocket hook with robust reconnection logic:
- Exponential backoff with jitter for reconnection attempts
- Configurable max reconnection attempts (0 = infinite)
- Tracks connection state (isConnected, isConnecting, reconnectAttempts)
- Automatic reconnection on token change or auth state change
- Ping/pong heartbeat handling
- Severity: High
- Location:
frontend/ - Resolution: Set up comprehensive frontend testing framework:
- Vitest as test runner with jsdom environment
- @testing-library/react for component testing
- @testing-library/jest-dom for DOM assertions
- Test utilities for mocking Next.js router, localStorage, WebSocket
- Example tests:
auth-store.test.ts,use-debounce.test.ts - npm scripts:
npm test,npm run test:coverage
- Severity: Medium
- Location:
backend/tests/api/ - Resolution: Created API integration test suite:
- Test fixtures for database, test user, auth tokens
- Tests for auth endpoints (register, login, refresh, forgot-password)
- Tests for protected endpoints requiring authentication
- Tests for email CRUD operations with pagination and filtering
- Uses httpx.AsyncClient with ASGITransport for in-process testing
- Severity: Medium
- Location:
backend/app/core/task_tracker.py,backend/app/main.py - Resolution: Implemented graceful shutdown for background tasks:
- TaskTracker class for tracking active asyncio tasks
- 30-second timeout for waiting for tasks to complete
- Automatic cancellation with 5-second grace period for stuck tasks
- Integration with batch service for task registration
- Severity: Medium
- Location:
backend/app/llm/client.py - Resolution: Made GeminiClient singleton config-rotation safe:
- Thread-safe singleton with
_config_lock - Stores API key used to create the instance
- Recreates client if API key changes
- Added
reset_gemini_client()function for manual reset
- Thread-safe singleton with
- Severity: Medium
- Location:
frontend/src/app/crm/page.tsx,frontend/src/hooks/use-debounce.ts - Resolution: Added debounced search to CRM page:
- Created
useDebouncehook with configurable delay (default 300ms) - CRM search now filters contacts via API as user types
- Proper cleanup on unmount to prevent memory leaks
- Created
- Severity: Critical
- Location:
backend/app/api/routes/auth.py,frontend/src/lib/api.ts,frontend/src/stores/auth-store.ts - Resolution: Implemented secure cookie-based authentication:
- Access token stored in memory only (Zustand store, no persistence)
- Refresh token stored in httpOnly cookie (inaccessible to JavaScript)
- Cookie uses SameSite=Strict and Secure in production
- Backend sets refresh token cookie on login, clears on logout
- Frontend updated to use cookie for token refresh automatically
- Severity: Medium
- Location:
backend/app/integrations/crm/hubspot_crm.py - Resolution: Implemented production-ready HubSpot CRM adapter:
- Full CRUD operations for contacts
- Contact search with HubSpot's search API
- Company association support
- Error handling and logging
- Severity: Low
- Location:
backend/app/core/config.py - Resolution: Added CRM configuration options:
CRM_PROVIDER: Choose between "database", "hubspot", "mock"HUBSPOT_API_KEY: API key for HubSpot integrationHUBSPOT_BASE_URL: Configurable base URL
- Severity: Low
- Location:
backend/app/api/routes/crm.py - Resolution: Already using EmailStr for email path parameters
- Severity: Low
- Location:
backend/app/services/contact_enrichment.py,backend/app/services/email_service.py - Resolution: Implemented intelligent contact enrichment:
- Extracts company from email domain
- Parses first/last name from sender string
- Detects business vs personal email addresses
- Enriches contacts during email sync with extracted data
- Severity: Medium
- Location:
backend/app/api/middleware/rate_limit.py - Resolution: Added
email_send_rate_limitmiddleware:- Default: 30 emails per minute per user
- Configurable via
EMAIL_SEND_RATE_LIMIT_PER_MINUTEsetting
- Severity: Medium
- Location:
backend/app/api/middleware/rate_limit.py,backend/app/api/routes/batch.py - Resolution: Added
batch_rate_limitmiddleware:- Default: 10 batch requests per minute per user
- Applied to
/batch/classifyand/batch/processendpoints
- Severity: Low
- Location:
backend/app/schemas/settings.py - Resolution: Already implemented - uses Python's zoneinfo with IANA timezone validation
- Severity: Low
- Location:
backend/app/api/routes/crm.py - Resolution: Already implemented - uses Pydantic's EmailStr
- Severity: Low
- Location:
backend/app/schemas/email.py - Resolution: Already implemented - validates body size ≤ 50KB in EmailCreate schema
- Severity: Low
- Location:
backend/app/api/routes/crm.py - Resolution: Added
sanitize_search_queryfunction:- Removes dangerous characters (SQL injection prevention)
- Strips SQL keywords
- Limits query length to 100 characters
- Severity: Low
- Location:
backend/app/api/routes/notifications.py - Resolution: Implemented structured error format:
- All messages follow consistent JSON schema:
{type, timestamp, data, error} - Errors include code, message, and details
- Applied to auth failures and server errors
- All messages follow consistent JSON schema:
- Severity: Low
- Location:
backend/app/services/health_service.py - Resolution: Enhanced health check with comprehensive checks:
- Database connectivity and query performance
- Gemini API configuration
- Gmail OAuth configuration
- CRM configuration
- Latency metrics for all components
- Severity: Low
- Location:
backend/app/models/base.py - Resolution: Added
SoftDeleteMixin:deleted_atcolumn with indexsoft_delete()andrestore()methodsis_deletedproperty- Query helper methods for filtering
- Severity: Low
- Location:
frontend/src/components/skip-to-content.tsx,frontend/src/components/layout/app-shell.tsx - Resolution: Implemented skip link component:
- Visually hidden until focused
- Appears on keyboard Tab press
- Smooth scrolls to main content
- Styled with high contrast for visibility
- VAL-3: Batch email ownership verification already implemented
- ERR-1: Database error handlers (IntegrityError, OperationalError, TimeoutError) already in place
- ERR-2: Gmail API error handlers (GmailAuthError, GmailRateLimitError, GmailAPIError) already in place
- AGENT-NEW-2: Input truncation (4000 chars) already in classify, generate, and decide nodes
Generated from application audit on 2026-02-20. Resolution review on 2026-02-21. Final resolution session on 2026-02-24.