candle-annotator/openspec/changes/code-review-fix/tasks.md
Marko Djordjevic ff15adc847 feat: add SHA256 model integrity check before joblib.load()
Add verify_model_checksum() that validates model files against a
models/checksums.sha256 manifest before loading. Fails open when
manifest is missing or file not listed (backward compat), raises
HTTP 500 on hash mismatch. Created empty manifest placeholder.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-18 11:25:14 +01:00

13 KiB

1. Security Critical — Git & Credentials

  • 1.1 [haiku] Add .env to .gitignore and run git rm --cached .env to untrack it
  • 1.2 [haiku] Add models/ and *.pkl to .gitignore
  • 1.3 [haiku] Replace real credentials in .env.example with placeholders (POSTGRES_PASSWORD=change_me_to_a_strong_password)
  • 1.4 [haiku] Remove SQL comment with credentials from services/ml/app/db.py and add fail-fast check for missing DATABASE_URL
  • 1.5 [sonnet] Update docker-compose.yml to use ${POSTGRES_USER}, ${POSTGRES_PASSWORD}, ${POSTGRES_DB} env var interpolation in all DATABASE_URL values
  • 1.6 [haiku] Bind PostgreSQL port to 127.0.0.1:5432:5432 in docker-compose.yml
  • 1.7 [haiku] Bind MLflow port to 127.0.0.1:5000:5000 in docker-compose.yml
  • 1.8 [haiku] Bind ML service port to 127.0.0.1:8001:8001 in docker-compose.yml

2. Security Critical — Input Validation & CORS

  • 2.1 [haiku] Validate run_id matches /^[a-zA-Z0-9_-]+$/ in src/app/api/training/runs/[run_id]/route.ts before interpolation
  • 2.2 [sonnet] Validate run_id format and use Path.resolve() + directory containment check in services/ml/app/main.py (model load at line 1203, delete at line 1312)
  • 2.3 [sonnet] Add file size check (reject >10MB) and row count limit (500,000) to src/app/api/upload/route.ts
  • 2.4 [haiku] Add file type validation (.csv extension, text MIME type) to src/app/api/upload/route.ts
  • 2.5 [haiku] Fix CORS in services/ml/app/main.py: replace allow_origins=["*"] with ["http://localhost:3000"] and support CORS_ORIGINS env var

3. Authentication

  • 3.1 [sonnet] Create src/middleware.ts with API key auth middleware: read API_KEY env var, check X-API-Key header on all /api/* routes except /api/health, return 401 if invalid
  • 3.2 [sonnet] Add FastAPI Depends() API key dependency in services/ml/app/main.py: read API_KEY env var, check X-API-Key header, exempt /health endpoint
  • 3.3 [sonnet] Update all Next.js proxy routes to forward X-API-Key header to ML service
  • 3.4 [haiku] Add API_KEY to .env.example with placeholder value and instructions

4. API Route Hardening (Next.js)

  • 4.1 [sonnet] Add Zod schema validation to src/app/api/predict/route.ts (validate pair, timeframe, candles array)
  • 4.2 [sonnet] Add Zod schema validation to src/app/api/predict/batch/route.ts (validate pair, timeframe, start_date, end_date)
  • 4.3 [sonnet] Add Zod schema validation to src/app/api/model/load/route.ts (validate run_id)
  • 4.4 [sonnet] Add Zod schema validation to src/app/api/training/start/route.ts (validate model_type)
  • 4.5 [sonnet] Add Zod schema validation to src/app/api/patterns/detect/route.ts (validate candles, patterns array)
  • 4.6 [sonnet] Replace error.message with generic "Internal server error" in all catch blocks across 7+ route files: health/route.ts, candles/route.ts, annotations/route.ts, annotations/[id]/route.ts, upload/route.ts, export/route.ts, span-annotations/export/route.ts
  • 4.7 [sonnet] Require chartId for bulk delete in src/app/api/annotations/route.ts — reject ?all=true without chartId with HTTP 400
  • 4.8 [sonnet] Wrap chart cascade delete in db.transaction() and add spanAnnotations deletion in src/app/api/charts/[id]/route.ts
  • 4.9 [haiku] Add parseInt(value, 10) with isNaN() guard to all routes parsing integer query params
  • 4.10 [sonnet] Add CSV injection protection (prefix =+@- cells with ') to all export routes
  • 4.11 [sonnet] Add response.ok checks before .json() in src/app/page.tsx (lines 214, 230, 245, 257)
  • 4.12 [sonnet] Add response.ok checks before .json() in src/components/CandleChart.tsx (lines 163, 178, 192)

5. ML Service Hardening (Python)

  • 5.1 [sonnet] Replace error.message / traceback details with generic "Internal server error" in FastAPI exception handlers at lines 640, 778, 1091, 1134, 1199, 1296 of services/ml/app/main.py
  • 5.2 [opus] Add SHA256 model integrity check: create models/checksums.sha256 manifest, verify hash before joblib.load() in services/ml/app/main.py:266
  • 5.3 [sonnet] Add _model_swap_lock to prediction reads (not just writes) in services/ml/app/main.py for thread-safe model access
  • 5.4 [sonnet] Add date range validation (max 1 year) to POST /predict/batch in services/ml/app/main.py
  • 5.5 [sonnet] Add candle time-sort validation/auto-sort to POST /predict in services/ml/app/main.py
  • 5.6 [sonnet] Implement real health checks: SELECT 1 for PostgreSQL, MLflow API ping in services/ml/app/main.py:396-409
  • 5.7 [sonnet] Add training resource limits: 500MB dataset size check, 30-minute timeout with status update on expiry in services/ml/app/main.py:907-1030
  • 5.8 [haiku] Add run_id format validation to DELETE /training/runs/{run_id} and GET /training/runs/{run_id} endpoints

6. Infrastructure & Docker

  • 6.1 [sonnet] Add headers() function to next.config.js with X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, Content-Security-Policy
  • 6.2 [sonnet] Add USER appuser to services/ml/Dockerfile: create user with useradd, set ownership, add USER directive before CMD
  • 6.3 [haiku] Create .dockerignore with .git, .env, .env*, node_modules, .next, data/, *.md, __pycache__/, mlruns/, models/
  • 6.4 [haiku] Change TA-Lib download URL to HTTPS in services/ml/Dockerfile:10
  • 6.5 [sonnet] Add SHA256 checksum verification for TA-Lib download in services/ml/Dockerfile
  • 6.6 [haiku] Remove COPY --from=builder /app/node_modules ./node_modules line from Dockerfile:29 (standalone doesn't need it)
  • 6.7 [sonnet] Pin Docker base images to @sha256: digests in both Dockerfiles
  • 6.8 [haiku] Fix healthcheck tool mismatch: use same tool (curl) in Dockerfile and docker-compose.yml

7. Frontend — Stale Closures & Race Conditions

  • 7.1 [opus] Fix stale closure in fetchPredictions: extract modelInfo into a useRef that stays in sync with state, use ref in generateCacheKey (src/app/page.tsx:489-552)
  • 7.2 [opus] Add AbortController to fetchPredictions and handleFetchBatchPredictions: store controller in ref, abort previous on new request, discard stale responses (src/app/page.tsx:494-663)
  • 7.3 [opus] Fix stale closure in CandleChart click handler: convert drawingState, selectedLineId, dragState, annotations to refs, update refs alongside setState, read refs in handler (src/components/CandleChart.tsx:572-971)
  • 7.4 [opus] Fix stale closure in SpanAnnotationManager keyboard handler: wrap handleDeleteSpan in useCallback, use ref for selectedSpanId (src/components/SpanAnnotationManager.tsx:461-535)

8. Frontend — Memory Leaks & Performance

  • 8.1 [opus] Fix SpanAnnotationManager preview primitive memory leak: replace useState with useRef for preview primitive, add cleanup on unmount (src/components/SpanAnnotationManager.tsx:265-324)
  • 8.2 [sonnet] Use chart.applyOptions() for theme changes instead of re-creating chart (src/components/CandleChart.tsx:333)
  • 8.3 [sonnet] Remove fitContent() from reconciliation effect, only call on initial load (src/components/SpanAnnotationManager.tsx:160)
  • 8.4 [opus] Implement incremental primitive updates in SpanAnnotationManager: update only selection state on selection change, full reconciliation only on annotation list changes (src/components/SpanAnnotationManager.tsx:104-161)
  • 8.5 [sonnet] Add bounded prediction cache (max 100 entries, FIFO eviction) to predictionCacheRef (src/app/page.tsx:195-199)
  • 8.6 [sonnet] Fix hardcoded 1-minute candle interval: detect interval from data, use for span iteration (src/components/CandleChart.tsx:452)
  • 8.7 [haiku] Extract magic numbers (8px, 60s, colors) to named constants in CandleChart.tsx
  • 8.8 [haiku] Move new Set<string>() default prop to module-level constant in CandleChart.tsx:125

9. Frontend — Shared Types & Type Safety

  • 9.1 [sonnet] Create src/types/candles.ts with Candle interface
  • 9.2 [sonnet] Create src/types/annotations.ts with Annotation, AnnotationType, Geometry interfaces
  • 9.3 [sonnet] Create src/types/charts.ts with Chart interface
  • 9.4 [sonnet] Create src/types/predictions.ts with PredictionSpan, PredictionState, ModelInfo interfaces
  • 9.5 [sonnet] Create src/types/span-annotations.ts with SpanAnnotation, SpanLabelType, SubSpan interfaces
  • 9.6 [haiku] Create src/types/index.ts barrel file re-exporting all types
  • 9.7 [sonnet] Replace duplicate interfaces in page.tsx, CandleChart.tsx, SpanAnnotationManager.tsx, Toolbox.tsx, SpanAnnotationList.tsx, SpanPopover.tsx with imports from @/types
  • 9.8 [sonnet] Replace all any types with proper interfaces: geometryGeometry | null, sub_spansSubSpan[], candle arrays → Candle[], prediction cache → Map<string, PredictionSpan[]>

10. Frontend — Error Boundary & UX

  • 10.1 [sonnet] Create src/components/ErrorBoundary.tsx React class component with error state, fallback UI (error message + "Try Again" + "Reload" buttons), and console.error logging
  • 10.2 [haiku] Wrap {children} with ErrorBoundary in src/app/layout.tsx
  • 10.3 [sonnet] Add confirmation dialog before delete-all annotations in src/app/page.tsx:412-425 using Radix Dialog
  • 10.4 [haiku] Fix SpanAnnotationList.tsx:110-115 confidence check: replace falsy check with != null for confidence value 0

11. Frontend — Accessibility

  • 11.1 [sonnet] Add aria-label attributes to all toolbar buttons in Toolbox.tsx
  • 11.2 [sonnet] Add role="dialog", aria-modal="true", and focus trapping to KeyboardShortcutsModal.tsx
  • 11.3 [sonnet] Make ChartSelector.tsx keyboard accessible: add arrow key navigation and click-outside close handler
  • 11.4 [sonnet] Add aria-pressed to toggle buttons (span drawing mode, prediction visibility)

12. Frontend — Polish & Cleanup

  • 12.1 [haiku] Remove unused imports TrendingUp, ChevronUp from Toolbox.tsx:4
  • 12.2 [haiku] Remove @ts-ignore in Toolbox.tsx:246-247, replace with proper type assertion for CSS custom property
  • 12.3 [sonnet] Fix dark theme on annotation-types/page.tsx and span-label-types/page.tsx: replace hardcoded light colors with theme-aware CSS variables
  • 12.4 [sonnet] Add debounce (150ms) or onPointerUp to confidence slider in PredictionPanel.tsx:148-155
  • 12.5 [sonnet] Replace Google Font CSS @import in globals.css with next/font/google in layout.tsx
  • 12.6 [haiku] Remove manual Escape handler in SpanPopover.tsx:114-117 (conflicts with Radix Dialog)
  • 12.7 [sonnet] Fix TalibPatternPanel.tsx:39 re-fetch when API returns empty array: add hasLoaded flag
  • 12.8 [haiku] Add [candles] to useImperativeHandle dependency array in CandleChart.tsx:202-221
  • 12.9 [haiku] Add activeChartId to primitive cleanup effect dependency arrays in CandleChart.tsx:1059-1110
  • 12.10 [sonnet] Implement or remove no-op Tooltip component in ui/tooltip.tsx (replace with Radix Tooltip if used)
  • 12.11 [haiku] Remove eslint-disable in TrainingPanel.tsx:128-129, include stable callbacks in dependency array
  • 12.12 [haiku] Remove dead filter code (TODO comment, no-op) in page.tsx:49-53

13. Package & Dependencies

  • 13.1 [haiku] Move @types/node, @types/react, @types/react-dom, @types/papaparse, @types/pg from dependencies to devDependencies in package.json
  • 13.2 [haiku] Move typescript, eslint, eslint-config-next, autoprefixer, postcss from dependencies to devDependencies in package.json
  • 13.3 [haiku] Add zod to dependencies in package.json

14. Dead Code Removal

  • 14.1 [haiku] Delete src/lib/db/migrate.ts (dead SQLite migration code)
  • 14.2 [haiku] Remove get_db_session() function from services/ml/app/db.py:99-111
  • 14.3 [haiku] Remove dead inference* package reference from pyproject.toml:32
  • 14.4 [haiku] Fix inconsistent uuid as uuid_lib import in services/ml/app/main.py:9 — use standard import uuid
  • 14.5 [haiku] Remove duplicate TALIB_PATTERNS dict — import from one location (app/patterns.py or generate_talib_annotations.py)

15. Deprecated Python API Replacements

  • 15.1 [sonnet] Replace @app.on_event("startup") with FastAPI lifespan pattern in services/ml/app/main.py:307
  • 15.2 [haiku] Replace declarative_base() with class Base(DeclarativeBase) in services/ml/app/db.py:45
  • 15.3 [haiku] Replace all datetime.utcnow() with datetime.now(datetime.UTC) in services/ml/app/main.py:325,1000,1019,1080
  • 15.4 [haiku] Add missing fallback return for volume indicators in features/talib_features.py:169-190