Sync user-accounts delta specs to main specs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marko Djordjevic 2026-02-20 18:50:14 +01:00
parent 34d58948a3
commit 448b67199f
9 changed files with 703 additions and 6 deletions

View file

@ -230,3 +230,117 @@ All `fetch()` calls in frontend components (`page.tsx`, `CandleChart.tsx`) SHALL
#### Scenario: Error response handled #### Scenario: Error response handled
- **WHEN** a fetch call returns HTTP 500 - **WHEN** a fetch call returns HTTP 500
- **THEN** the code detects `!response.ok` and shows an error message instead of attempting JSON parse - **THEN** the code detects `!response.ok` and shows an error message instead of attempting JSON parse
## MODIFIED Requirements (user-accounts)
### Requirement: Predict proxy endpoint
The system SHALL provide a `POST /api/predict` Next.js API route that proxies requests to the Python inference service at `${INFERENCE_API_URL}/predict`. The route SHALL require authentication via `getAuthUser()`. The route SHALL forward the request body (pair, timeframe, candles array) and include the `X-User-ID` header with the authenticated user's UUID. If the inference service is unreachable, the route SHALL return HTTP 503 with `{ "error": "Inference service unavailable" }`.
#### Scenario: Successful prediction proxy
- **WHEN** an authenticated user calls POST /api/predict with valid candle data and the Python service is running
- **THEN** the route forwards the request with `X-User-ID` header and returns the prediction response with HTTP 200
#### Scenario: Unauthenticated prediction request
- **WHEN** POST /api/predict is called without authentication
- **THEN** the route returns HTTP 401 with `{ "error": "Unauthorized" }`
#### Scenario: Inference service down
- **WHEN** POST /api/predict is called but the Python inference service is unreachable
- **THEN** the route returns HTTP 503 with `{ "error": "Inference service unavailable" }`
#### Scenario: Inference service error
- **WHEN** the Python inference service returns an error status (4xx or 5xx)
- **THEN** the route forwards the error status and message to the client
### Requirement: Batch predict proxy endpoint
The system SHALL provide a `POST /api/predict/batch` Next.js API route that proxies batch prediction requests to `${INFERENCE_API_URL}/predict/batch`. The route SHALL require authentication and include the `X-User-ID` header.
#### Scenario: Successful batch prediction
- **WHEN** an authenticated user calls POST /api/predict/batch with valid parameters
- **THEN** the route forwards to the inference service with `X-User-ID` header and returns the response
#### Scenario: Timeout on large batch
- **WHEN** the batch prediction takes longer than INFERENCE_BATCH_TIMEOUT
- **THEN** the route returns HTTP 504 with `{ "error": "Batch prediction timed out" }`
### Requirement: Training proxy endpoints
The Next.js API SHALL provide proxy routes for training operations: `POST /api/training/start`, `GET /api/training/runs`, and `GET /api/training/dataset-info`. All training proxy routes SHALL require authentication and include the `X-User-ID` header.
#### Scenario: Proxy training start
- **WHEN** an authenticated user calls POST /api/training/start
- **THEN** the route forwards to the FastAPI service with `X-User-ID` header
#### Scenario: Proxy training runs
- **WHEN** an authenticated user calls GET /api/training/runs
- **THEN** the route forwards to the FastAPI service with `X-User-ID` header and returns the run list
### Requirement: Model load proxy
The Next.js API SHALL provide a `POST /api/model/load` route that proxies to the FastAPI `/model/load` endpoint. The route SHALL require authentication and include the `X-User-ID` header.
#### Scenario: Proxy model load
- **WHEN** an authenticated user calls POST /api/model/load with a run_id
- **THEN** the route forwards to the FastAPI service with `X-User-ID` header
### Requirement: Model info proxy endpoint
The system SHALL provide a `GET /api/model/info` Next.js API route that proxies to `${INFERENCE_API_URL}/model/info`. The route SHALL require authentication and include the `X-User-ID` header.
#### Scenario: Successful model info
- **WHEN** an authenticated user calls GET /api/model/info
- **THEN** the route returns the model metadata JSON
#### Scenario: No model available
- **WHEN** GET /api/model/info is called and the inference service returns 503
- **THEN** the route returns HTTP 503 with `{ "error": "No model available" }`
### Requirement: Pattern detection proxy
The Next.js API SHALL provide a `POST /api/patterns/detect` route that proxies to the FastAPI `/patterns/detect` endpoint. The route SHALL require authentication and include the `X-User-ID` header.
#### Scenario: Proxy pattern detection
- **WHEN** an authenticated user calls POST /api/patterns/detect
- **THEN** the route forwards the request with `X-User-ID` header and returns the response
### Requirement: Available patterns proxy
The Next.js API SHALL provide a `GET /api/patterns/available` route that proxies to the FastAPI `/patterns/available` endpoint. The route SHALL require authentication.
#### Scenario: Proxy available patterns
- **WHEN** an authenticated user calls GET /api/patterns/available
- **THEN** the route forwards to the FastAPI service and returns the pattern list
### Requirement: Bulk delete by source
The Next.js API `DELETE /api/span-annotations` endpoint SHALL require authentication and scope deletion by the authenticated user. When `source` is provided, all span annotations matching that source (and optionally `label` filter) for the current chart belonging to the authenticated user SHALL be deleted.
#### Scenario: Bulk delete TA-Lib annotations
- **WHEN** an authenticated user calls `DELETE /api/span-annotations?chartId=1&source=talib`
- **THEN** all span annotations with `source: "talib"` for chart 1 belonging to that user are deleted
#### Scenario: Bulk delete by source and label
- **WHEN** an authenticated user calls `DELETE /api/span-annotations?chartId=1&source=talib&label=Engulfing`
- **THEN** only TA-Lib annotations containing "Engulfing" for chart 1 belonging to that user are deleted
## ADDED Requirements (user-accounts)
### Requirement: Auth guard on all data API routes
All existing data API routes (`/api/upload`, `/api/candles`, `/api/annotations`, `/api/annotation-types`, `/api/charts`, `/api/span-annotations`, `/api/span-label-types`, `/api/export`) SHALL call `getAuthUser()` at the top. If the user is not authenticated, the route SHALL return HTTP 401.
#### Scenario: Unauthenticated data API access
- **WHEN** any data API route is called without authentication
- **THEN** the route returns HTTP 401 with `{ "error": "Unauthorized" }`
#### Scenario: Authenticated data API access
- **WHEN** any data API route is called with valid authentication
- **THEN** the route proceeds with queries scoped to the authenticated user's ID
### Requirement: User-scoped queries in all data routes
All Drizzle queries in data API routes SHALL include a `user_id` filter matching the authenticated user. INSERT operations SHALL set `user_id` to the authenticated user's UUID.
#### Scenario: GET queries filtered by user
- **WHEN** an authenticated user requests data (charts, annotations, annotation types, etc.)
- **THEN** the query includes `.where(eq(table.user_id, user.id))` or equivalent join condition
#### Scenario: INSERT operations set user_id
- **WHEN** an authenticated user creates new data (upload, create annotation, etc.)
- **THEN** the inserted row has `user_id` set to the authenticated user's UUID
#### Scenario: Cross-user data isolation
- **WHEN** user A requests data
- **THEN** no data belonging to user B is returned

View file

@ -0,0 +1,51 @@
## ADDED Requirements
### Requirement: Landing page at root route
The system SHALL serve a public landing page at `/` (route `src/app/(public)/page.tsx`). The page SHALL match the Lovable design mockup from `lovable_design_html/candles_lovable_design_landing_page.html`.
#### Scenario: Landing page renders
- **WHEN** an unauthenticated user navigates to `/`
- **THEN** the landing page renders with the CandleAnnotator branding, hero section, features grid, stats bar, and CTA section
#### Scenario: Authenticated user visits landing
- **WHEN** an authenticated user navigates to `/`
- **THEN** the landing page renders normally (no redirect — user can still view marketing page)
### Requirement: Landing page navigation
The landing page navbar SHALL display the CandleAnnotator logo/name on the left and "Log in" + "Get Started" buttons on the right. "Log in" SHALL link to `/login`. "Get Started" SHALL link to `/register`.
#### Scenario: Nav links for unauthenticated user
- **WHEN** an unauthenticated user views the landing page
- **THEN** the navbar shows "Log in" (text button) linking to `/login` and "Get Started" (primary button) linking to `/register`
#### Scenario: Nav links for authenticated user
- **WHEN** an authenticated user views the landing page
- **THEN** the navbar shows "Go to App" (primary button) linking to `/app` instead of login/register buttons
### Requirement: Hero section
The hero section SHALL display: a badge "Built for quants & ML engineers", the heading "Annotate OHLC Charts. Train Smarter Models.", a description paragraph, and two CTAs: "Start Annotating" (primary, links to `/register`) and "Try Demo" (secondary, links to `/app`).
#### Scenario: Hero renders correctly
- **WHEN** the landing page loads
- **THEN** the hero section displays the heading, description, badge, and both CTA buttons
### Requirement: Features grid
The features section SHALL display 6 feature cards in a 3-column grid: Precision Annotation, ML Training Pipeline, Real-Time Predictions, Multi-Chart Workspace, Keyboard-First Workflow, Export & Persist. Each card SHALL have an icon, title, and description.
#### Scenario: Features grid renders
- **WHEN** the landing page loads
- **THEN** 6 feature cards are displayed in a responsive grid (3 columns on desktop, 1 on mobile)
### Requirement: Stats bar
The stats section SHALL display 3 metrics: "50ms" (Render latency), "6" (Shortcut keys), "JSON" (Export format) in a horizontal bar.
#### Scenario: Stats bar renders
- **WHEN** the landing page loads
- **THEN** the stats bar displays the 3 metrics with labels
### Requirement: Footer CTA section
The page SHALL include a bottom CTA section with heading "Ready to label?", description "No credit card required", and a "Create Free Account" button linking to `/register`. Below that, a footer with the CandleAnnotator name and copyright year.
#### Scenario: Footer CTA renders
- **WHEN** the landing page loads
- **THEN** the CTA section and footer render with correct links

View file

@ -0,0 +1,58 @@
## ADDED Requirements
### Requirement: Login page at /login
The system SHALL serve a login page at `/login` (route `src/app/(public)/login/page.tsx`). The page SHALL match the Lovable design mockup from `lovable_design_html/candles_lovable_design_login_page.html`.
#### Scenario: Login page renders
- **WHEN** an unauthenticated user navigates to `/login`
- **THEN** the login page renders with a centered card containing the login form
#### Scenario: Authenticated user redirected
- **WHEN** an authenticated user navigates to `/login`
- **THEN** they are redirected to `/app`
### Requirement: Login page navigation
The login page navbar SHALL display a back arrow and the CandleAnnotator logo/name, linking to `/` (landing page).
#### Scenario: Back to landing
- **WHEN** a user clicks the CandleAnnotator logo in the login page navbar
- **THEN** they are navigated to `/`
### Requirement: Email/password login form
The login card SHALL display a "Welcome back" heading, "Sign in to your workspace" subtitle, and a form with email input, password input, and "Sign In" submit button. The form SHALL use Auth.js `signIn("credentials", ...)` on submit.
#### Scenario: Successful email login
- **WHEN** a user enters valid email and password and clicks "Sign In"
- **THEN** `signIn("credentials", { email, password, redirect: true, callbackUrl: "/app" })` is called
- **AND** on success, the user is redirected to `/app`
#### Scenario: Failed email login
- **WHEN** a user enters invalid credentials and clicks "Sign In"
- **THEN** an error message is displayed: "Invalid email or password"
- **AND** the user remains on the login page
#### Scenario: Form validation
- **WHEN** a user clicks "Sign In" with empty email or password fields
- **THEN** browser-native validation prevents submission (fields are `required`)
### Requirement: Google OAuth login button
The login form SHALL include a "Continue with Google" button below the email/password form. Clicking it SHALL call `signIn("google", { callbackUrl: "/app" })`.
#### Scenario: Google login initiated
- **WHEN** a user clicks "Continue with Google"
- **THEN** they are redirected to Google's OAuth consent screen
- **AND** on successful auth, they are redirected back to `/app`
### Requirement: Forgot password link
The login form SHALL display a "Forgot password?" link next to the password label. Since password reset is deferred, clicking it SHALL show a toast: "Password reset is not yet available. Contact support."
#### Scenario: Forgot password clicked
- **WHEN** a user clicks "Forgot password?"
- **THEN** a toast notification appears with the message about contacting support
### Requirement: Register link
The login page SHALL display "Don't have an account? Sign up" below the form. "Sign up" SHALL link to `/register`.
#### Scenario: Navigate to register
- **WHEN** a user clicks "Sign up"
- **THEN** they are navigated to `/register`

View file

@ -16,11 +16,11 @@ The Next.js application SHALL connect to PostgreSQL using Drizzle ORM with the `
- **THEN** the application SHALL fail to start with a connection error - **THEN** the application SHALL fail to start with a connection error
### Requirement: PostgreSQL schema definitions ### Requirement: PostgreSQL schema definitions
The Drizzle schema SHALL define all frontend tables using `pgTable` from `drizzle-orm/pg-core`. The following tables SHALL be defined: `charts`, `candles`, `annotation_types`, `annotations`, `span_label_types`, `span_annotations`. The Drizzle schema SHALL define all frontend tables using `pgTable` from `drizzle-orm/pg-core`. The following tables SHALL be defined: `users`, `charts`, `candles`, `annotation_types`, `annotations`, `span_label_types`, `span_annotations`. All tables except `users` and `candles` SHALL include a `user_id` column (uuid, foreign key to users.id, not null). The `candles` table inherits user scope through its `chart_id` foreign key to `charts`.
#### Scenario: Charts table schema #### Scenario: Charts table schema
- **WHEN** the schema is loaded - **WHEN** the schema is loaded
- **THEN** the `charts` table has columns: `id` (serial, primary key), `name` (text, unique, not null), `created_at` (timestamp, not null, default now) - **THEN** the `charts` table has columns: `id` (serial, primary key), `name` (text, not null), `user_id` (uuid, foreign key to users.id, not null), `created_at` (timestamp, not null, default now), with a unique index on `(user_id, name)`
#### Scenario: Candles table schema #### Scenario: Candles table schema
- **WHEN** the schema is loaded - **WHEN** the schema is loaded
@ -28,19 +28,19 @@ The Drizzle schema SHALL define all frontend tables using `pgTable` from `drizzl
#### Scenario: Annotation types table schema #### Scenario: Annotation types table schema
- **WHEN** the schema is loaded - **WHEN** the schema is loaded
- **THEN** the `annotation_types` table has columns: `id` (serial, primary key), `name` (text, unique, not null), `display_name` (text, not null), `color` (text, not null), `category` (text, not null), `icon` (text, nullable), `is_active` (boolean, not null, default true), `created_at` (timestamp, not null, default now) - **THEN** the `annotation_types` table has columns: `id` (serial, primary key), `name` (text, not null), `display_name` (text, not null), `color` (text, not null), `category` (text, not null), `icon` (text, nullable), `is_active` (boolean, not null, default true), `user_id` (uuid, foreign key to users.id, not null), `created_at` (timestamp, not null, default now), with a unique index on `(user_id, name)`
#### Scenario: Annotations table schema #### Scenario: Annotations table schema
- **WHEN** the schema is loaded - **WHEN** the schema is loaded
- **THEN** the `annotations` table has columns: `id` (serial, primary key), `chart_id` (integer, foreign key to charts.id, not null), `timestamp` (timestamp, not null), `label_type` (text, not null), `geometry` (jsonb, nullable), `color` (text, default '#3b82f6'), `created_at` (timestamp, not null, default now) - **THEN** the `annotations` table has columns: `id` (serial, primary key), `chart_id` (integer, foreign key to charts.id, not null), `timestamp` (timestamp, not null), `label_type` (text, not null), `geometry` (jsonb, nullable), `color` (text, default '#3b82f6'), `user_id` (uuid, foreign key to users.id, not null), `created_at` (timestamp, not null, default now)
#### Scenario: Span label types table schema #### Scenario: Span label types table schema
- **WHEN** the schema is loaded - **WHEN** the schema is loaded
- **THEN** the `span_label_types` table has columns: `id` (serial, primary key), `name` (text, unique, not null), `display_name` (text, not null), `color` (text, not null), `hotkey` (text, nullable), `is_active` (boolean, not null, default true), `sort_order` (integer, not null, default 0), `created_at` (timestamp, not null, default now) - **THEN** the `span_label_types` table has columns: `id` (serial, primary key), `name` (text, not null), `display_name` (text, not null), `color` (text, not null), `hotkey` (text, nullable), `is_active` (boolean, not null, default true), `sort_order` (integer, not null, default 0), `user_id` (uuid, foreign key to users.id, not null), `created_at` (timestamp, not null, default now), with a unique index on `(user_id, name)`
#### Scenario: Span annotations table schema #### Scenario: Span annotations table schema
- **WHEN** the schema is loaded - **WHEN** the schema is loaded
- **THEN** the `span_annotations` table has columns: `id` (serial, primary key), `chart_id` (integer, foreign key to charts.id, not null), `start_time` (timestamp, not null), `end_time` (timestamp, not null), `label` (text, not null), `confidence` (integer, nullable), `outcome` (text, nullable), `notes` (text, nullable), `sub_spans` (jsonb, nullable), `color` (text, not null, default '#2196F3'), `source` (text, not null, default 'human'), `model_prediction` (jsonb, nullable), `created_at` (timestamp, not null, default now) - **THEN** the `span_annotations` table has columns: `id` (serial, primary key), `chart_id` (integer, foreign key to charts.id, not null), `start_time` (timestamp, not null), `end_time` (timestamp, not null), `label` (text, not null), `confidence` (integer, nullable), `outcome` (text, nullable), `notes` (text, nullable), `sub_spans` (jsonb, nullable), `color` (text, not null, default '#2196F3'), `source` (text, not null, default 'human'), `model_prediction` (jsonb, nullable), `user_id` (uuid, foreign key to users.id, not null), `created_at` (timestamp, not null, default now)
### Requirement: PostgreSQL migrations via Drizzle Kit ### Requirement: PostgreSQL migrations via Drizzle Kit
The project SHALL use Drizzle Kit to generate and apply PostgreSQL migrations. The `drizzle.config.ts` SHALL target the `postgresql` dialect. Existing SQLite migrations SHALL be removed. The project SHALL use Drizzle Kit to generate and apply PostgreSQL migrations. The `drizzle.config.ts` SHALL target the `postgresql` dialect. Existing SQLite migrations SHALL be removed.
@ -115,3 +115,41 @@ The `package.json` SHALL list `@types/*`, `typescript`, `eslint`, `eslint-config
#### Scenario: Build tools in devDependencies #### Scenario: Build tools in devDependencies
- **WHEN** `package.json` is inspected - **WHEN** `package.json` is inspected
- **THEN** `typescript`, `eslint`, `eslint-config-next`, `autoprefixer`, `postcss` are in `devDependencies` - **THEN** `typescript`, `eslint`, `eslint-config-next`, `autoprefixer`, `postcss` are in `devDependencies`
## ADDED Requirements (user-accounts)
### Requirement: Users table schema
The Drizzle schema SHALL define a `users` table with columns: `id` (uuid, primary key, default `gen_random_uuid()`), `name` (text, nullable), `email` (text, unique, not null), `email_verified` (timestamp, nullable), `password_hash` (text, nullable), `image` (text, nullable), `provider` (text, not null, default 'credentials'), `provider_account_id` (text, nullable), `created_at` (timestamp, not null, default now), `updated_at` (timestamp, not null, default now).
#### Scenario: Users table created
- **WHEN** the schema is loaded and migrations are applied
- **THEN** the `users` table exists with all specified columns and the `email` unique constraint
#### Scenario: UUID primary key generation
- **WHEN** a new user is inserted without specifying an ID
- **THEN** a UUID is automatically generated via `gen_random_uuid()`
### Requirement: User data migration script
The project SHALL include a migration script that adds `user_id` to existing tables, creates a default admin user, assigns all existing data to that user, and makes `user_id` NOT NULL.
#### Scenario: Migration adds user_id columns
- **WHEN** the migration runs on a database without `user_id` columns
- **THEN** `user_id` (uuid, nullable) columns are added to `charts`, `annotation_types`, `annotations`, `span_label_types`, `span_annotations`
#### Scenario: Default admin user created
- **WHEN** the migration runs and no users exist
- **THEN** a default admin user is created with email from `DEFAULT_ADMIN_EMAIL` env var (default: `admin@candleannotator.local`) and password from `DEFAULT_ADMIN_PASSWORD` env var (default: `changeme123`)
#### Scenario: Existing data assigned to admin
- **WHEN** the migration runs and existing rows have NULL `user_id`
- **THEN** all rows are updated to set `user_id` to the admin user's ID
#### Scenario: Columns made NOT NULL
- **WHEN** all existing rows have been assigned a `user_id`
- **THEN** the `user_id` columns are altered to NOT NULL with foreign key constraints
#### Scenario: Unique constraints updated
- **WHEN** the migration completes
- **THEN** the unique constraint on `charts.name` is replaced with `(user_id, name)`
- **AND** the unique constraint on `annotation_types.name` is replaced with `(user_id, name)`
- **AND** the unique constraint on `span_label_types.name` is replaced with `(user_id, name)`

View file

@ -0,0 +1,53 @@
## ADDED Requirements
### Requirement: Register page at /register
The system SHALL serve a registration page at `/register` (route `src/app/(public)/register/page.tsx`). The page SHALL match the Lovable design mockup from `lovable_design_html/candles_lovable_design_register_page.html`.
#### Scenario: Register page renders
- **WHEN** an unauthenticated user navigates to `/register`
- **THEN** the register page renders with a centered card containing the registration form
#### Scenario: Authenticated user redirected
- **WHEN** an authenticated user navigates to `/register`
- **THEN** they are redirected to `/app`
### Requirement: Register page navigation
The register page navbar SHALL display a back arrow and the CandleAnnotator logo/name, linking to `/` (landing page).
#### Scenario: Back to landing
- **WHEN** a user clicks the CandleAnnotator logo in the register page navbar
- **THEN** they are navigated to `/`
### Requirement: Registration form
The register card SHALL display a "Create account" heading, "Start annotating charts in seconds" subtitle, and a form with name input, email input, password input (min 8 characters hint), and "Create Account" submit button.
#### Scenario: Successful registration
- **WHEN** a user fills in name, email, and password (8+ chars) and clicks "Create Account"
- **THEN** a POST request is sent to `/api/auth/register` with `{ name, email, password }`
- **AND** on success (HTTP 201), the user is automatically signed in via `signIn("credentials", { email, password })` and redirected to `/app`
#### Scenario: Duplicate email error
- **WHEN** a user registers with an email that already exists
- **THEN** an error message is displayed: "Email already registered"
#### Scenario: Short password error
- **WHEN** a user enters a password shorter than 8 characters
- **THEN** an error message is displayed: "Password must be at least 8 characters"
#### Scenario: Missing fields
- **WHEN** a user clicks "Create Account" with empty required fields
- **THEN** browser-native validation prevents submission (fields are `required`)
### Requirement: Google OAuth registration button
The register form SHALL include a "Continue with Google" button. Clicking it SHALL call `signIn("google", { callbackUrl: "/app" })`. If the user is new, Auth.js creates their account automatically.
#### Scenario: Google registration
- **WHEN** a new user clicks "Continue with Google"
- **THEN** they are redirected to Google OAuth, and on success a user record is created and they land on `/app`
### Requirement: Login link
The register page SHALL display "Already have an account? Sign in" below the form. "Sign in" SHALL link to `/login`.
#### Scenario: Navigate to login
- **WHEN** a user clicks "Sign in"
- **THEN** they are navigated to `/login`

View file

@ -114,3 +114,71 @@ The confidence display in SpanAnnotationList SHALL use `!= null` instead of a fa
#### Scenario: Confidence zero displayed #### Scenario: Confidence zero displayed
- **WHEN** a span annotation has confidence value `0` - **WHEN** a span annotation has confidence value `0`
- **THEN** the list displays "0%" (not hidden as if confidence is absent) - **THEN** the list displays "0%" (not hidden as if confidence is absent)
## MODIFIED Requirements (user-accounts)
### Requirement: Theme toggle in sidebar
The UI shell SHALL include a theme toggle button in the sidebar. The button SHALL be positioned at the bottom of the sidebar, visually separated from the tool buttons.
#### Scenario: Toggle button renders
- **WHEN** the application loads at `/app`
- **THEN** a theme toggle button is visible at the bottom of the sidebar
#### Scenario: Toggle button displays correct icon
- **WHEN** the current theme mode is "system"
- **THEN** the button shows a monitor icon (lucide-react `Monitor`)
- **WHEN** the current theme mode is "light"
- **THEN** the button shows a sun icon (lucide-react `Sun`)
- **WHEN** the current theme mode is "dark"
- **THEN** the button shows a moon icon (lucide-react `Moon`)
#### Scenario: Toggle button has tooltip
- **WHEN** user hovers over the theme toggle button
- **THEN** a tooltip displays the current mode name (e.g., "Theme: System", "Theme: Light", "Theme: Dark")
## ADDED Requirements (user-accounts)
### Requirement: App workspace route at /app
The main workspace page SHALL be served at `/app` (route `src/app/app/page.tsx`). The existing `src/app/page.tsx` content (chart, annotations, toolbox, panels) SHALL move to this new route. The `/app` route SHALL be protected by the auth proxy.
#### Scenario: Workspace at /app
- **WHEN** an authenticated user navigates to `/app`
- **THEN** the full workspace renders (chart, toolbox, panels — same as current `/`)
#### Scenario: Unauthenticated redirect
- **WHEN** an unauthenticated user navigates to `/app`
- **THEN** they are redirected to `/login`
### Requirement: App layout with user menu
The `/app` layout SHALL include a top navigation bar with the CandleAnnotator logo (linking to `/app`), and a user menu on the right side. The user menu SHALL show the user's name/email and provide links to Settings and Sign Out.
#### Scenario: User menu displays identity
- **WHEN** an authenticated user views the app
- **THEN** the top nav shows the user's name or email with an avatar/initial
#### Scenario: User menu dropdown
- **WHEN** a user clicks the user menu
- **THEN** a dropdown shows: "Settings" (links to `/app/settings`) and "Sign Out" (calls `signOut()`)
#### Scenario: Sign out
- **WHEN** a user clicks "Sign Out" in the user menu
- **THEN** the session is destroyed and the user is redirected to `/login`
### Requirement: Settings link in sidebar
The sidebar SHALL include a settings gear icon button that navigates to `/app/settings`.
#### Scenario: Settings button renders
- **WHEN** the app workspace loads
- **THEN** a settings icon button is visible in the sidebar (near the theme toggle)
#### Scenario: Navigate to settings
- **WHEN** a user clicks the settings icon
- **THEN** they are navigated to `/app/settings`
### Requirement: Public page layout
Public pages (`/`, `/login`, `/register`) SHALL use a separate layout (`src/app/(public)/layout.tsx`) without the workspace sidebar, toolbox, or user menu. They SHALL share the same root layout (fonts, ThemeProvider).
#### Scenario: Public layout structure
- **WHEN** a user views a public page
- **THEN** the page renders without sidebar, toolbox, or user menu
- **AND** the root layout fonts and theme provider are still active

View file

@ -0,0 +1,125 @@
## ADDED Requirements
### Requirement: Auth.js v5 configuration
The system SHALL configure Auth.js v5 (next-auth@5) in `src/auth.ts` with JWT session strategy, Credentials provider, and Google OAuth provider. The configuration SHALL export `handlers`, `auth`, `signIn`, and `signOut` functions.
#### Scenario: Auth config exports
- **WHEN** `src/auth.ts` is imported
- **THEN** it exports `handlers` (GET/POST), `auth` (session getter), `signIn`, and `signOut` functions
#### Scenario: JWT session strategy
- **WHEN** a user authenticates successfully
- **THEN** a JWT token is issued containing `user.id`, `user.email`, and `user.name`
- **AND** no database session record is created
#### Scenario: JWT callbacks embed user ID
- **WHEN** the JWT callback fires after sign-in
- **THEN** the `token.id` is set to the user's UUID from the database
- **AND** the session callback maps `token.id` to `session.user.id`
### Requirement: Credentials provider with email/password
The Credentials provider SHALL accept `email` and `password` fields. The `authorize` function SHALL look up the user by email in the `users` table, verify the password against the stored `password_hash` using bcryptjs, and return the user object on success or `null` on failure.
#### Scenario: Valid credentials
- **WHEN** a user submits correct email and password
- **THEN** the authorize function returns the user object with `id`, `email`, `name`
- **AND** a JWT session is created
#### Scenario: Invalid password
- **WHEN** a user submits correct email but wrong password
- **THEN** the authorize function returns `null`
- **AND** no session is created
#### Scenario: Non-existent email
- **WHEN** a user submits an email that does not exist in the database
- **THEN** the authorize function returns `null`
#### Scenario: OAuth-only user attempts password login
- **WHEN** a user who registered via Google (no password_hash) submits their email with any password
- **THEN** the authorize function returns `null`
### Requirement: Google OAuth provider
The Google OAuth provider SHALL be configured with `AUTH_GOOGLE_ID` and `AUTH_GOOGLE_SECRET` environment variables. On first Google sign-in, the system SHALL create a new user record with `provider: 'google'` and `provider_account_id` set to the Google sub ID. On subsequent sign-ins, the system SHALL find the existing user by email.
#### Scenario: First Google sign-in creates user
- **WHEN** a user signs in with Google for the first time
- **THEN** a new user record is created with `provider: 'google'`, `password_hash: null`, `provider_account_id` set to the Google sub ID, and `name`/`email`/`image` from the Google profile
#### Scenario: Returning Google sign-in
- **WHEN** a user signs in with Google and their email already exists in the database
- **THEN** the existing user is returned and no duplicate is created
#### Scenario: Missing Google credentials
- **WHEN** `AUTH_GOOGLE_ID` or `AUTH_GOOGLE_SECRET` environment variables are not set
- **THEN** the Google provider SHALL not be included in the providers list and email/password login remains available
### Requirement: Auth API route handler
The system SHALL create `src/app/api/auth/[...nextauth]/route.ts` that exports the GET and POST handlers from `src/auth.ts`.
#### Scenario: Auth routes respond
- **WHEN** a request is made to `/api/auth/signin`, `/api/auth/signout`, `/api/auth/session`, or `/api/auth/callback/google`
- **THEN** the NextAuth handler processes the request
### Requirement: Registration API endpoint
The system SHALL provide a `POST /api/auth/register` endpoint that creates a new user account. The endpoint SHALL accept `{ name, email, password }` in the request body. The password SHALL be hashed with bcryptjs (salt rounds: 10) before storage. The email SHALL be checked for uniqueness.
#### Scenario: Successful registration
- **WHEN** POST /api/auth/register is called with valid name, email, and password (min 8 characters)
- **THEN** a new user is created with hashed password, `provider: 'credentials'`
- **AND** the response is HTTP 201 with `{ id, email, name }`
#### Scenario: Duplicate email
- **WHEN** POST /api/auth/register is called with an email that already exists
- **THEN** the response is HTTP 409 with `{ error: "Email already registered" }`
#### Scenario: Invalid password length
- **WHEN** POST /api/auth/register is called with a password shorter than 8 characters
- **THEN** the response is HTTP 400 with `{ error: "Password must be at least 8 characters" }`
#### Scenario: Missing required fields
- **WHEN** POST /api/auth/register is called without email or password
- **THEN** the response is HTTP 400 with `{ error: "Email and password are required" }`
### Requirement: Auth proxy for route protection
The system SHALL create a `proxy.ts` file at the project root that uses Auth.js `auth()` to check JWT sessions. The proxy SHALL enforce authentication on protected routes.
#### Scenario: Unauthenticated access to /app
- **WHEN** an unauthenticated user requests any path under `/app` or `/app/*`
- **THEN** the proxy redirects to `/login`
#### Scenario: Unauthenticated access to /api
- **WHEN** an unauthenticated user requests any path under `/api/*` except `/api/auth/*` and `/api/health`
- **THEN** the proxy returns HTTP 401
#### Scenario: Authenticated access to /login
- **WHEN** an authenticated user requests `/login` or `/register`
- **THEN** the proxy redirects to `/app`
#### Scenario: Public routes pass through
- **WHEN** any user requests `/`, `/login`, `/register`, `/api/auth/*`, or `/api/health`
- **THEN** the proxy allows the request to proceed without auth check
### Requirement: Auth helper for API routes
The system SHALL provide a `getAuthUser()` helper function in `src/lib/auth.ts` that extracts the authenticated user from the current session. All protected API routes SHALL call this function.
#### Scenario: Authenticated request
- **WHEN** `getAuthUser()` is called in an API route with a valid JWT session
- **THEN** it returns `{ id, email, name }` from the session
#### Scenario: Unauthenticated request
- **WHEN** `getAuthUser()` is called in an API route without a valid session
- **THEN** it returns `null`
### Requirement: SessionProvider wrapper
The system SHALL wrap the app layout with `next-auth/react` `SessionProvider` so client components can use the `useSession()` hook to access auth state.
#### Scenario: Session available in client components
- **WHEN** a client component calls `useSession()` inside the protected app layout
- **THEN** it receives `{ data: session, status: "authenticated" }` with `session.user.id`, `session.user.email`, `session.user.name`
### Requirement: npm dependencies for auth
The project SHALL add `next-auth@5` (Auth.js v5), `bcryptjs`, and `@types/bcryptjs` to the dependencies.
#### Scenario: Dependencies installed
- **WHEN** `package.json` is inspected
- **THEN** `next-auth`, `bcryptjs` are in `dependencies` and `@types/bcryptjs` is in `devDependencies`

View file

@ -0,0 +1,82 @@
## ADDED Requirements
### Requirement: User-scoped chart creation
When a chart is created (via CSV upload), the system SHALL associate it with the authenticated user's ID. The `charts` table row SHALL have `user_id` set to the current user's UUID.
#### Scenario: Upload creates user-scoped chart
- **WHEN** an authenticated user uploads a CSV file
- **THEN** the created chart record has `user_id` set to the authenticated user's ID
#### Scenario: User sees only their charts
- **WHEN** an authenticated user requests their chart list
- **THEN** only charts with `user_id` matching the authenticated user are returned
### Requirement: User-scoped annotations
All annotation operations (create, read, delete) SHALL be filtered by the authenticated user's ID. Annotations SHALL only be visible to the user who created them.
#### Scenario: Create annotation scoped to user
- **WHEN** an authenticated user creates an annotation
- **THEN** the annotation record has `user_id` set to the authenticated user's ID
#### Scenario: Read annotations filtered by user
- **WHEN** an authenticated user requests annotations for a chart
- **THEN** only annotations belonging to that user (and that chart) are returned
#### Scenario: Delete annotation ownership check
- **WHEN** an authenticated user attempts to delete an annotation
- **THEN** the deletion only succeeds if the annotation belongs to the authenticated user
### Requirement: User-scoped span annotations
All span annotation operations SHALL be filtered by the authenticated user's ID via the chart's `user_id`.
#### Scenario: Create span annotation scoped to user
- **WHEN** an authenticated user creates a span annotation on their chart
- **THEN** the span annotation is created and associated with the user's chart
#### Scenario: Read span annotations filtered by user
- **WHEN** an authenticated user requests span annotations
- **THEN** only span annotations on charts belonging to that user are returned
### Requirement: User-scoped annotation types
Each user SHALL have their own set of annotation types. The `annotation_types` table SHALL be filtered by `user_id`. The unique constraint on `name` SHALL be changed to a composite unique constraint on `(user_id, name)`.
#### Scenario: User-specific annotation types
- **WHEN** an authenticated user requests their annotation types
- **THEN** only annotation types with `user_id` matching the authenticated user are returned
#### Scenario: Two users with same annotation type name
- **WHEN** two different users each create an annotation type named "break_up"
- **THEN** both records are stored without conflict (unique per user, not globally)
### Requirement: User-scoped span label types
Each user SHALL have their own set of span label types. The `span_label_types` table SHALL be filtered by `user_id`. The unique constraint on `name` SHALL be changed to a composite unique constraint on `(user_id, name)`.
#### Scenario: User-specific span label types
- **WHEN** an authenticated user requests their span label types
- **THEN** only span label types with `user_id` matching the authenticated user are returned
### Requirement: Default data seeding on registration
When a new user registers, the system SHALL seed default annotation types and span label types for that user. The defaults SHALL include: annotation types `break_up` (green, marker), `break_down` (red, marker), `line` (blue, line); and a starter set of span label types.
#### Scenario: New user gets default annotation types
- **WHEN** a new user is created (via registration or first Google sign-in)
- **THEN** default annotation types (break_up, break_down, line) are inserted with the new user's ID
#### Scenario: New user gets default span label types
- **WHEN** a new user is created
- **THEN** default span label types are inserted with the new user's ID
### Requirement: ML service user context
When the Next.js API proxies requests to the FastAPI ML service, it SHALL include the `X-User-ID` header with the authenticated user's UUID. The ML service SHALL use this header to scope training runs, model storage, and experiments.
#### Scenario: Training request includes user ID
- **WHEN** an authenticated user starts a training run via `/api/training/start`
- **THEN** the proxy request to FastAPI includes `X-User-ID: <user-uuid>` header
#### Scenario: Prediction request includes user ID
- **WHEN** an authenticated user requests prediction via `/api/predict`
- **THEN** the proxy request to FastAPI includes `X-User-ID: <user-uuid>` header
#### Scenario: ML service scopes by user
- **WHEN** the FastAPI service receives a request with `X-User-ID` header
- **THEN** training runs and model lookups are scoped to that user ID

View file

@ -0,0 +1,108 @@
## ADDED Requirements
### Requirement: Settings page at /app/settings
The system SHALL serve a settings page at `/app/settings` (route `src/app/app/settings/page.tsx`). The page SHALL be accessible only to authenticated users.
#### Scenario: Settings page renders
- **WHEN** an authenticated user navigates to `/app/settings`
- **THEN** the settings page renders with Profile, Security, and Danger Zone sections
#### Scenario: Unauthenticated access
- **WHEN** an unauthenticated user navigates to `/app/settings`
- **THEN** they are redirected to `/login`
### Requirement: Profile section
The Profile section SHALL display the user's name and email. The user SHALL be able to edit their display name via an input field and "Save" button. The email field SHALL be read-only.
#### Scenario: View profile
- **WHEN** the settings page loads
- **THEN** the user's current name and email are displayed
#### Scenario: Update display name
- **WHEN** a user changes their name and clicks "Save"
- **THEN** a PUT request is sent to `/api/auth/profile` with `{ name }`
- **AND** on success, a toast confirms "Profile updated"
#### Scenario: Empty name rejected
- **WHEN** a user clears the name field and clicks "Save"
- **THEN** validation prevents submission with an error message
### Requirement: Security section (credentials users)
The Security section SHALL display a "Change Password" form with current password, new password, and confirm password fields. This section SHALL only be visible to users with `provider: 'credentials'`.
#### Scenario: Change password successfully
- **WHEN** a credentials user enters correct current password, new password (8+ chars), and matching confirmation
- **THEN** a PUT request is sent to `/api/auth/password` with `{ currentPassword, newPassword }`
- **AND** on success, a toast confirms "Password changed"
#### Scenario: Wrong current password
- **WHEN** a user enters incorrect current password
- **THEN** the API returns HTTP 403 and an error message is displayed: "Current password is incorrect"
#### Scenario: New password too short
- **WHEN** a user enters a new password shorter than 8 characters
- **THEN** validation prevents submission with an error message
#### Scenario: Passwords don't match
- **WHEN** the new password and confirmation don't match
- **THEN** client-side validation shows "Passwords do not match"
#### Scenario: OAuth user sees provider info
- **WHEN** an OAuth user views the Security section
- **THEN** instead of the password form, they see "Signed in via Google" with the Google icon
### Requirement: Profile update API endpoint
The system SHALL provide a `PUT /api/auth/profile` endpoint that updates the authenticated user's display name.
#### Scenario: Update name
- **WHEN** PUT /api/auth/profile is called with `{ name: "New Name" }` by an authenticated user
- **THEN** the user's name is updated in the database and HTTP 200 is returned
#### Scenario: Unauthenticated
- **WHEN** PUT /api/auth/profile is called without authentication
- **THEN** HTTP 401 is returned
### Requirement: Password change API endpoint
The system SHALL provide a `PUT /api/auth/password` endpoint that changes the authenticated user's password. The endpoint SHALL verify the current password before updating.
#### Scenario: Successful password change
- **WHEN** PUT /api/auth/password is called with correct `currentPassword` and valid `newPassword`
- **THEN** the password_hash is updated with bcryptjs and HTTP 200 is returned
#### Scenario: Wrong current password
- **WHEN** PUT /api/auth/password is called with incorrect `currentPassword`
- **THEN** HTTP 403 is returned with `{ error: "Current password is incorrect" }`
#### Scenario: OAuth user attempts password change
- **WHEN** an OAuth user calls PUT /api/auth/password
- **THEN** HTTP 400 is returned with `{ error: "Password change not available for OAuth accounts" }`
### Requirement: Danger zone - delete account
The Danger Zone section SHALL display a "Delete Account" button styled in red/destructive. Clicking it SHALL open a confirmation dialog requiring the user to type "DELETE" to confirm.
#### Scenario: Delete account flow
- **WHEN** a user clicks "Delete Account" and types "DELETE" in the confirmation dialog and confirms
- **THEN** a DELETE request is sent to `/api/auth/account`
- **AND** on success, the user is signed out and redirected to `/`
#### Scenario: Cancel deletion
- **WHEN** a user clicks "Delete Account" but dismisses the confirmation dialog
- **THEN** no deletion occurs
### Requirement: Account deletion API endpoint
The system SHALL provide a `DELETE /api/auth/account` endpoint that deletes the authenticated user and all their associated data (charts, annotations, annotation_types, span_annotations, span_label_types).
#### Scenario: Successful deletion
- **WHEN** DELETE /api/auth/account is called by an authenticated user
- **THEN** all user data is deleted (cascade), the user record is deleted, and HTTP 200 is returned
#### Scenario: Unauthenticated
- **WHEN** DELETE /api/auth/account is called without authentication
- **THEN** HTTP 401 is returned
### Requirement: Back to app navigation
The settings page SHALL include a back link/button to return to `/app`.
#### Scenario: Navigate back
- **WHEN** a user clicks the back button on the settings page
- **THEN** they are navigated to `/app`