candle-annotator/CODE_REVIEW.md

22 KiB

Code Review: Candle Annotator

Date: 2026-02-17 Reviewer: Claude Code (Automated Audit) Scope: Full codebase - frontend (Next.js), backend (FastAPI), configuration, deployment Verdict: Request Changes - Critical and major issues must be fixed before production deployment.


Executive Summary

85 issues identified across the entire codebase: 15 Critical, 30 Major, 40 Minor.

The application has zero SQL injection vulnerabilities (Drizzle ORM and SQLAlchemy parameterize all queries), good error handling structure (every route has try/catch), and well-implemented timeouts on proxy routes.

However, the codebase has no authentication anywhere, hardcoded credentials in committed files, path traversal vectors, unsafe deserialization, and numerous frontend race conditions/memory leaks.

Zero test files exist anywhere in the codebase.


Table of Contents

  1. Critical Issues
  2. Major Issues
  3. Minor Issues
  4. Positive Feedback
  5. Prioritized Fix Plan

1. Critical Issues

C-1. No Authentication on Any Endpoint (Frontend + Backend)

  • Files: All 27 API route files in src/app/api/, all FastAPI endpoints in services/ml/app/main.py
  • Impact: Anyone with network access can read all data, delete all charts/annotations, upload files, trigger training, load arbitrary models.
  • Fix: Add Next.js middleware (src/middleware.ts) with API key or session auth. Add FastAPI Depends() for API key auth on the ML service. At minimum, protect write/delete/training operations.

C-2. SSRF / Path Traversal via run_id in Training Runs DELETE

  • File: src/app/api/training/runs/[run_id]/route.ts:15
  • Code: fetch(`${INFERENCE_API_URL}/training/runs/${run_id}`)
  • Impact: A crafted run_id like ../../admin/delete-all traverses to arbitrary internal endpoints.
  • Fix: Validate run_id matches /^[a-zA-Z0-9_-]+$/ before interpolation.

C-3. .env File NOT Gitignored - Credentials in Repo

  • File: .gitignore only ignores .env*.local, not .env
  • Impact: Database credentials (ml_user:ml_password) in DATABASE_URL are committed to git.
  • Fix: Add .env to .gitignore, remove .env from git history with git rm --cached .env.

C-4. Hardcoded Database Credentials Everywhere

  • Files: docker-compose.yml:10,37,73-74, services/ml/app/db.py:18-31, .env.example:3
  • Impact: ml_user / ml_password in plaintext in committed files.
  • Fix: Use Docker secrets or ${POSTGRES_PASSWORD} interpolation from .env. Remove SQL comment with credentials from db.py. Replace real creds in .env.example with placeholders.

C-5. PostgreSQL Port Exposed to Host

  • File: docker-compose.yml:69 - ports: "5432:5432"
  • Impact: Combined with weak hardcoded password, database is directly accessible externally.
  • Fix: Remove port mapping or bind to 127.0.0.1:5432:5432.

C-6. No File Upload Size Limit

  • File: src/app/api/upload/route.ts:27-39
  • Impact: Denial of service via memory exhaustion or database flooding with unlimited CSV rows.
  • Fix: Add explicit file.size check (e.g., reject > 10MB), limit row count inserted.

C-7. Unsafe Pickle Deserialization via joblib.load

  • File: services/ml/app/main.py:266
  • Impact: joblib.load(model_path) uses pickle internally - can execute arbitrary code if a malicious .pkl is placed in models/.
  • Fix: Add SHA256 integrity checks for model files. Consider ONNX/safetensors format.

C-8. CORS Wildcard with Credentials on ML Service

  • File: services/ml/app/main.py:51-57
  • Code: allow_origins=["*"] + allow_credentials=True
  • Impact: API accessible from any origin with credentials.
  • Fix: Replace * with explicit origins like ["http://localhost:3000"].

C-9. Stale Closure in fetchPredictions

  • File: src/app/page.tsx:552
  • Impact: predictionState.modelInfo captured in generateCacheKey (line 489) can be stale, producing incorrect cache keys and showing wrong predictions.
  • Fix: Extract modelInfo into its own useState or use a ref for model version in generateCacheKey.

C-10. Race Condition in fetchPredictions - No AbortController

  • File: src/app/page.tsx:494-552
  • Impact: Rapid clicks on "Run on Visible" causes earlier responses to resolve after later ones, overwriting correct state. Same for handleFetchBatchPredictions (lines 600-663).
  • Fix: Use AbortController to cancel previous in-flight requests, or track a request ID and discard stale results.

C-11. Stale Closure in CandleChart Click Handler

  • File: src/components/CandleChart.tsx:572-971
  • Impact: Async click handler captures drawingState, selectedLineId, dragState, annotations from closure. 12+ item dependency array causes excessive re-subscriptions and stale values between re-subscriptions.
  • Fix: Use refs for frequently-changing values that the click handler reads, reducing re-subscription frequency.

C-12. Pervasive any Types Disable Type Safety

  • Files: page.tsx:134,147,196-198, CandleChart.tsx:62,389, SpanAnnotationManager.tsx:25, Toolbox.tsx:23, SpanAnnotationList.tsx:15, TalibPatternPanel.tsx:19, FileUpload.tsx:45, rectangle-drawing.ts:60,63
  • Impact: Type errors at runtime go undetected at compile time. Especially dangerous for geometry, sub_spans, and prediction cache values.
  • Fix: Define proper interfaces for geometry, sub_spans. Type candle arrays properly.

2. Major Issues

M-1. Bulk Delete All Annotations Without Guard

  • File: src/app/api/annotations/route.ts:102-106
  • Code: if (all === 'true') deletes ALL annotations across ALL charts in one unauthenticated request.
  • Fix: Require chartId for bulk deletes. Never allow unscoped deletion.

M-2. Proxy Routes Forward Arbitrary JSON Without Validation

  • Files: src/app/api/predict/route.ts, predict/batch/route.ts, model/load/route.ts, training/start/route.ts, patterns/detect/route.ts
  • Impact: Arbitrary payloads forwarded to internal service.
  • Fix: Validate request body with Zod schemas before forwarding.

M-3. Error Messages Leak Internal Details (Frontend)

  • Files: health/route.ts:25, candles/route.ts:40, annotations/route.ts:35,86, annotations/[id]/route.ts:47,83, upload/route.ts:135,153, export/route.ts:67, span-annotations/export/route.ts:123
  • Impact: Raw error.message returned to clients can reveal table names, paths, connection strings.
  • Fix: Return generic error messages to clients. Log full errors server-side only.

M-4. Error Messages Leak Internal Details (Backend)

  • File: services/ml/app/main.py:640,778,1091,1134,1199,1296
  • Impact: Exception details including tracebacks returned to clients.
  • Fix: Return generic messages. Log details server-side.

M-5. Race Condition in Chart Cascade Delete

  • File: src/app/api/charts/[id]/route.ts:43-45
  • Impact: Three separate DELETE queries without a transaction.
  • Fix: Wrap in db.transaction(async (tx) => { ... }).

M-6. Missing Span Annotations Deletion in Chart Delete

  • File: src/app/api/charts/[id]/route.ts:42-45
  • Impact: Deletes annotations and candles but NOT spanAnnotations - FK violation or orphans.
  • Fix: Add db.delete(spanAnnotations).where(eq(spanAnnotations.chart_id, chartId)).

M-7. Race Condition in Unique Chart Name Generation

  • File: src/app/api/upload/route.ts:7-25
  • Impact: Concurrent uploads with same filename can cause unique constraint violation.
  • Fix: Handle constraint error with retry, or use database-level upsert.

M-8. Path Traversal via run_id in Model Load/Delete (Python)

  • File: services/ml/app/main.py:1203,1312
  • Code: Path("models") / f"{run_id}.pkl" - not sanitized.
  • Fix: Validate UUID format, use Path.resolve() and verify within expected directory.

M-9. Dynamic Module Import from Config

  • File: services/ml/features/custom_loader.py:54
  • Code: importlib.import_module(feature_path) from YAML config.
  • Fix: Restrict to a whitelist of allowed module paths.

M-10. No Resource Limits on Training

  • File: services/ml/app/main.py:907-1030
  • Impact: Loads entire dataset into memory, no timeout, daemon thread can corrupt models.
  • Fix: Add file size check, training timeout, consider Celery/RQ task queue.

M-11. Thread Safety Issue with Model State

  • File: services/ml/app/main.py:59-74,579-588,1330
  • Impact: Model read during prediction has no lock, but writes use _model_swap_lock.
  • Fix: Use same lock for reads, or atomic reference swap pattern.

M-12. ML Service Container Runs as Root

  • File: services/ml/Dockerfile - no USER directive.
  • Fix: Add RUN useradd -m -r appuser && USER appuser.

M-13. MLflow + ML Service Ports Exposed Without Auth

  • File: docker-compose.yml:31,54-55
  • Fix: Remove port mappings or bind to 127.0.0.1.

M-14. TA-Lib Downloaded Over HTTP Without Checksum

  • File: services/ml/Dockerfile:10
  • Fix: Use HTTPS, add sha256sum -c verification.

M-15. No Security Response Headers (CSP, X-Frame-Options)

  • File: next.config.js
  • Fix: Add headers() function with security headers.

M-16. Unvalidated JSONB Fields Accept Arbitrary JSON

  • Files: annotations/route.ts, span-annotations/route.ts (POST/PATCH)
  • Impact: Storage abuse, deeply nested JSON DoS.
  • Fix: Validate JSON structure and impose size limits.

M-17. Full node_modules Copied to Production Image

  • File: Dockerfile:29
  • Fix: Remove the COPY line - standalone output bundles what it needs.

M-18. @types/* in Production Dependencies

  • File: package.json:23-27
  • Fix: Move @types/*, typescript, eslint, autoprefixer, postcss to devDependencies.

M-19. Missing response.ok Checks Before .json() (page.tsx)

  • File: src/app/page.tsx:214,230,245,257
  • Impact: If API returns 500 with non-JSON body, uncaught exception and silent failure.
  • Fix: Add if (!response.ok) throw new Error(...) before parsing JSON.

M-20. Missing response.ok Checks Before .json() (CandleChart)

  • File: src/components/CandleChart.tsx:163-164,178-179,192-193
  • Fix: Check response status before parsing.

M-21. Memory Leak - SpanAnnotationManager Preview Primitive

  • File: src/components/SpanAnnotationManager.tsx:305-316
  • Impact: New SpanRectanglePrimitive on every mouse move, state/effect feedback loop.
  • Fix: Use a ref for the preview primitive instead of state.

M-22. Duplicate Interface Definitions Across 6 Files

  • Files: page.tsx:123-161, CandleChart.tsx:21-76, SpanAnnotationManager.tsx:8-39, Toolbox.tsx:9-50, SpanAnnotationList.tsx:6-29, SpanPopover.tsx:25-34
  • Impact: Maintenance burden and drift bugs.
  • Fix: Centralize shared interfaces in /src/types/ and import everywhere.

M-23. Chart Re-creation on Theme Change Destroys State

  • File: src/components/CandleChart.tsx:333
  • Impact: Theme toggle destroys entire chart and re-creates it - loses all primitives, causes flicker.
  • Fix: Use chart.applyOptions() to update theme colors without re-creating.

M-24. Unbounded Prediction Cache

  • File: src/app/page.tsx:195-199
  • Impact: predictionCacheRef Map grows without limit.
  • Fix: Implement LRU cache or limit to fixed number of entries.

M-25. fitContent() Called on Every Span Change

  • File: src/components/SpanAnnotationManager.tsx:160
  • Impact: Resets user zoom/scroll position on every span annotation change.
  • Fix: Remove from reconciliation effect. Only call on initial load.

M-26. No Database SSL/TLS

  • Files: .env:3, docker-compose.yml:10,37
  • Fix: For production, add ?sslmode=require to connection strings.

M-27. eslint-disable Suppressing Missing Dependencies

  • File: src/components/TrainingPanel.tsx:128-129
  • Fix: Include stable callbacks in dependency array and remove the suppress.

M-28. Hardcoded DB Credentials in Python Source

  • File: services/ml/app/db.py:18-31
  • Fix: Remove SQL comment with credentials. Require env vars, fail fast if missing.

M-29. Health Check Fakes MLflow/DB Status

  • File: services/ml/app/main.py:396-409
  • Fix: Implement actual connectivity checks (SELECT 1 for DB).

M-30. Delete All Annotations Has No Confirmation Dialog

  • File: src/app/page.tsx:412-425
  • Fix: Add confirmation dialog before destructive action.

3. Minor Issues

Configuration & Deployment

# File Issue Fix
m-1 All routes No rate limiting on any endpoint Add rate limiting middleware
m-2 Dockerfiles Base images not pinned to digest Use @sha256:<hash>
m-3 (missing) No .dockerignore file Create with .git, .env, node_modules
m-4 .gitignore models/ and .pkl files not ignored Add models/, *.pkl
m-5 Dockerfile vs docker-compose.yml Healthcheck tool mismatch (wget vs curl) Use same tool
m-6 package.json Wide dependency ranges with ^ Pin critical deps
m-7 Dockerfile:38 EURUSD.csv data file baked into image Mount at runtime

API Routes

# File Issue Fix
m-8 Multiple routes parseInt() missing radix or NaN check Always parseInt(v, 10) + isNaN() guard
m-9 upload/route.ts:30-42 No file type validation on upload Check MIME type/extension
m-10 Export routes CSV injection (values starting with =+@-) Prefix dangerous cells with '
m-11 Multiple routes console.error logs full error objects Use structured logger
m-12 src/lib/db/migrate.ts Dead SQLite migration code Remove if unused

Python ML Service

# File Issue Fix
m-13 app/main.py:307 Deprecated @app.on_event("startup") Migrate to lifespan pattern
m-14 app/db.py:45 Deprecated declarative_base() Use class Base(DeclarativeBase)
m-15 app/main.py:325,1000,1019,1080 Deprecated datetime.utcnow() Use datetime.now(datetime.UTC)
m-16 app/patterns.py + generate_talib_annotations.py Duplicate TALIB_PATTERNS dict Import from one location
m-17 app/db.py:99-111 get_db_session() leaks sessions (dead code) Remove
m-18 app/main.py:129-134 No input size limits on batch prediction Add max date range validation
m-19 app/main.py:93 No validation candles are time-sorted Add sort validation
m-20 features/talib_features.py:169-190 Missing fallback return for volume indicators Add default return/raise
m-21 pyproject.toml:32 Dead inference* package reference Remove
m-22 app/main.py:9 Inconsistent uuid as uuid_lib import Use standard import uuid

React Components

# File Issue Fix
m-23 Toolbox.tsx:4 Unused imports: TrendingUp, ChevronUp Remove
m-24 CandleChart.tsx:452,569,837 Magic numbers (8px, 60s, hardcoded 1-min interval) Extract to named constants
m-25 Multiple files Missing ARIA labels on buttons, dropdowns, modals Add aria-label, aria-pressed
m-26 ChartSelector.tsx:38-109 Custom dropdown not keyboard accessible Use Radix DropdownMenu
m-27 KeyboardShortcutsModal.tsx:38-77 Modal not focus-trapped, missing role="dialog" Add focus trapping
m-28 SpanAnnotationList.tsx:110-115 Falsy check on confidence fails for value 0 Use != null check
m-29 page.tsx:56-85 O(n*m) in detectDisagreements Use sweep-line algorithm
m-30 page.tsx:49-53 Dead filter code (TODO comment, no-op) Implement or remove
m-31 CandleChart.tsx:125 new Set<string>() default prop recreated each render Use module-level constant
m-32 page.tsx ~20 props drilled to CandleChart Consider React Context
m-33 ui/tooltip.tsx Tooltip component is a no-op placeholder Use Radix Tooltip or remove
m-34 SpanAnnotationList.tsx:105 White text on light label colors - poor contrast Dynamic text color from luminance
m-35 page.tsx:892-906 Settings menu uses <a> tags instead of Next.js Link Use Link component
m-36 Toolbox.tsx:246-247 @ts-ignore for CSS custom property Use type assertion
m-37 SpanPopover.tsx:114-117 Manual Escape handler conflicts with Radix Dialog Remove manual handler
m-38 TalibPatternPanel.tsx:39 Re-fetches when API returns empty array Use hasLoaded flag
m-39 CandleChart.tsx:202-221 useImperativeHandle missing dependency array Add [candles] dep
m-40 CandleChart.tsx:1059-1110 Missing activeChartId in primitive cleanup effects Add to dependency arrays

4. Positive Feedback

  • Zero SQL injection - Drizzle ORM parameterizes all frontend queries; SQLAlchemy does the same on the backend.
  • Consistent error handling - Every API route has try/catch with appropriate HTTP status codes.
  • Good timeout management - All proxy routes use AbortController with configurable timeouts.
  • Solid Docker practices for Next.js - Multi-stage build, non-root user (nextjs), healthcheck, standalone output.
  • Clean project structure - Clear separation between frontend, API routes, ML service, and database layers.
  • Pydantic validation on ML endpoints - Request/response models are well-defined with proper types.
  • Thread-safe training state - Uses threading.Lock for active_training_run_id management.
  • Parameterized queries throughout - Both Drizzle and SQLAlchemy prevent injection by design.
  • Proper OHLC data model - Schema is well-designed with unique constraints and proper FK relationships.

5. Prioritized Fix Plan

Phase 1: Security Critical (Immediate)

Task Files to Change Effort
Add .env to .gitignore, remove from git history .gitignore Low
Replace hardcoded creds with env var interpolation docker-compose.yml Low
Remove real creds from .env.example and db.py comments .env.example, services/ml/app/db.py Low
Bind DB port to 127.0.0.1 only docker-compose.yml Low
Validate run_id format in training routes src/app/api/training/runs/[run_id]/route.ts, services/ml/app/main.py Low
Add file upload size limit src/app/api/upload/route.ts Low
Fix CORS on ML service services/ml/app/main.py Low

Phase 2: Before Production

Task Files to Change Effort
Add API key authentication middleware src/middleware.ts (new), ML service Medium
Add Zod input validation on proxy routes 5 proxy route files Medium
Wrap chart cascade delete in transaction + add span delete src/app/api/charts/[id]/route.ts Low
Stop leaking error details to clients 7+ route files, ML main.py Medium
Add security response headers next.config.js Low
Fix ML Dockerfile to run as non-root services/ml/Dockerfile Low
Create .dockerignore .dockerignore (new) Low
Remove bulk delete-all without chartId guard src/app/api/annotations/route.ts Low
Add model file integrity checks services/ml/app/main.py Medium

Phase 3: Code Quality & Frontend Fixes

Task Files to Change Effort
Add AbortController to prediction fetching src/app/page.tsx Medium
Fix SpanAnnotationManager preview primitive memory leak src/components/SpanAnnotationManager.tsx Medium
Use chart.applyOptions() for theme changes src/components/CandleChart.tsx Medium
Add response.ok checks on all fetch calls page.tsx, CandleChart.tsx Low
Centralize duplicate type definitions src/types/ (new files), 6 component files Medium
Remove fitContent() from reconciliation effect src/components/SpanAnnotationManager.tsx Low
Use refs for click handler closure values src/components/CandleChart.tsx Medium
Move dev deps to devDependencies package.json Low
Remove dead code (migrate.ts, get_db_session, dead filter) Multiple files Low

Phase 4: Hardening & Polish

Task Files to Change Effort
Add rate limiting Middleware Medium
Add confirmation dialog for delete-all src/app/page.tsx Low
Fix accessibility (ARIA labels, focus trapping, keyboard nav) Multiple components Medium
Add CSV injection protection to exports 3 export route files Low
Pin Docker images to digests Dockerfiles Low
Implement proper health checks (ML service) services/ml/app/main.py Low
Replace deprecated Python APIs services/ml/app/main.py, db.py Low
Extract magic numbers to constants CandleChart.tsx, page.tsx Low
Write tests New test files High

Test Coverage Assessment

  • No unit tests for API routes
  • No integration tests
  • No ML model/pipeline tests
  • No frontend component tests
  • No end-to-end tests

Summary by Area

Category Critical Major Minor Total
API Routes (TS) 3 7 5 15
React Components 4 9 18 31
Python ML Service 2 7 10 19
Config & Deployment 3 7 7 17
Total 12 30 40 82

Note: Some issues overlap across categories (e.g., auth affects both frontend and backend).