feat: add charts table schema and migration with data backfill
- Add charts table with id, name (unique), created_at - Add chart_id FK to candles table with composite unique on (chart_id, time) - Add chart_id FK to annotations table - Custom migration handles existing data: creates 'Imported Data' chart and backfills chart_id - Recreates tables for NOT NULL constraint (SQLite limitation)
This commit is contained in:
parent
178834f3b2
commit
92d3339a48
14 changed files with 913 additions and 3 deletions
74
drizzle/0002_careful_synch.sql
Normal file
74
drizzle/0002_careful_synch.sql
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
-- Create charts table
|
||||
CREATE TABLE `charts` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`name` text NOT NULL,
|
||||
`created_at` integer NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `charts_name_unique` ON `charts` (`name`);
|
||||
--> statement-breakpoint
|
||||
|
||||
-- Insert default chart if candles exist
|
||||
INSERT INTO `charts` (`name`, `created_at`)
|
||||
SELECT 'Imported Data', CAST(strftime('%s', 'now') AS INTEGER)
|
||||
WHERE EXISTS (SELECT 1 FROM `candles` LIMIT 1);
|
||||
--> statement-breakpoint
|
||||
|
||||
-- Drop old unique index on candles.time
|
||||
DROP INDEX IF EXISTS `candles_time_unique`;
|
||||
--> statement-breakpoint
|
||||
|
||||
-- Add chart_id column to candles (nullable first for backfill)
|
||||
ALTER TABLE `candles` ADD `chart_id` integer REFERENCES charts(id);
|
||||
--> statement-breakpoint
|
||||
|
||||
-- Backfill existing candles with the default chart id
|
||||
UPDATE `candles` SET `chart_id` = (SELECT `id` FROM `charts` WHERE `name` = 'Imported Data') WHERE `chart_id` IS NULL;
|
||||
--> statement-breakpoint
|
||||
|
||||
-- Add chart_id column to annotations (nullable first for backfill)
|
||||
ALTER TABLE `annotations` ADD `chart_id` integer REFERENCES charts(id);
|
||||
--> statement-breakpoint
|
||||
|
||||
-- Backfill existing annotations with the default chart id
|
||||
UPDATE `annotations` SET `chart_id` = (SELECT `id` FROM `charts` WHERE `name` = 'Imported Data') WHERE `chart_id` IS NULL;
|
||||
--> statement-breakpoint
|
||||
|
||||
-- Recreate candles table with NOT NULL constraint and composite unique index
|
||||
CREATE TABLE `candles_new` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`chart_id` integer NOT NULL REFERENCES charts(id),
|
||||
`time` integer NOT NULL,
|
||||
`open` real NOT NULL,
|
||||
`high` real NOT NULL,
|
||||
`low` real NOT NULL,
|
||||
`close` real NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
INSERT INTO `candles_new` (`id`, `chart_id`, `time`, `open`, `high`, `low`, `close`)
|
||||
SELECT `id`, `chart_id`, `time`, `open`, `high`, `low`, `close` FROM `candles`;
|
||||
--> statement-breakpoint
|
||||
DROP TABLE `candles`;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE `candles_new` RENAME TO `candles`;
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX `candles_chart_time_unique` ON `candles` (`chart_id`, `time`);
|
||||
--> statement-breakpoint
|
||||
|
||||
-- Recreate annotations table with NOT NULL constraint
|
||||
CREATE TABLE `annotations_new` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
`chart_id` integer NOT NULL REFERENCES charts(id),
|
||||
`timestamp` integer NOT NULL,
|
||||
`label_type` text NOT NULL,
|
||||
`geometry` text,
|
||||
`color` text DEFAULT '#3b82f6',
|
||||
`created_at` integer NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
INSERT INTO `annotations_new` (`id`, `chart_id`, `timestamp`, `label_type`, `geometry`, `color`, `created_at`)
|
||||
SELECT `id`, `chart_id`, `timestamp`, `label_type`, `geometry`, `color`, `created_at` FROM `annotations`;
|
||||
--> statement-breakpoint
|
||||
DROP TABLE `annotations`;
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE `annotations_new` RENAME TO `annotations`;
|
||||
288
drizzle/meta/0002_snapshot.json
Normal file
288
drizzle/meta/0002_snapshot.json
Normal file
|
|
@ -0,0 +1,288 @@
|
|||
{
|
||||
"version": "6",
|
||||
"dialect": "sqlite",
|
||||
"id": "8eb771d5-6d44-473e-8bce-144d195ae2b5",
|
||||
"prevId": "111e1b91-6d7b-45e4-aeb9-9762725d6905",
|
||||
"tables": {
|
||||
"annotation_types": {
|
||||
"name": "annotation_types",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"display_name": {
|
||||
"name": "display_name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"color": {
|
||||
"name": "color",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"category": {
|
||||
"name": "category",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"icon": {
|
||||
"name": "icon",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"is_active": {
|
||||
"name": "is_active",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false,
|
||||
"default": 1
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"annotation_types_name_unique": {
|
||||
"name": "annotation_types_name_unique",
|
||||
"columns": [
|
||||
"name"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"annotations": {
|
||||
"name": "annotations",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"chart_id": {
|
||||
"name": "chart_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"timestamp": {
|
||||
"name": "timestamp",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"label_type": {
|
||||
"name": "label_type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"geometry": {
|
||||
"name": "geometry",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"color": {
|
||||
"name": "color",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false,
|
||||
"default": "'#3b82f6'"
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"annotations_chart_id_charts_id_fk": {
|
||||
"name": "annotations_chart_id_charts_id_fk",
|
||||
"tableFrom": "annotations",
|
||||
"tableTo": "charts",
|
||||
"columnsFrom": [
|
||||
"chart_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"candles": {
|
||||
"name": "candles",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"chart_id": {
|
||||
"name": "chart_id",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"time": {
|
||||
"name": "time",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"open": {
|
||||
"name": "open",
|
||||
"type": "real",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"high": {
|
||||
"name": "high",
|
||||
"type": "real",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"low": {
|
||||
"name": "low",
|
||||
"type": "real",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"close": {
|
||||
"name": "close",
|
||||
"type": "real",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"candles_chart_time_unique": {
|
||||
"name": "candles_chart_time_unique",
|
||||
"columns": [
|
||||
"chart_id",
|
||||
"time"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"candles_chart_id_charts_id_fk": {
|
||||
"name": "candles_chart_id_charts_id_fk",
|
||||
"tableFrom": "candles",
|
||||
"tableTo": "charts",
|
||||
"columnsFrom": [
|
||||
"chart_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
},
|
||||
"charts": {
|
||||
"name": "charts",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "integer",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"autoincrement": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"charts_name_unique": {
|
||||
"name": "charts_name_unique",
|
||||
"columns": [
|
||||
"name"
|
||||
],
|
||||
"isUnique": true
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"checkConstraints": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"enums": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
||||
|
|
@ -15,6 +15,13 @@
|
|||
"when": 1770915891699,
|
||||
"tag": "0001_sticky_shinko_yamashiro",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 2,
|
||||
"version": "6",
|
||||
"when": 1770937855462,
|
||||
"tag": "0002_careful_synch",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
2
openspec/changes/multi-chart-management/.openspec.yaml
Normal file
2
openspec/changes/multi-chart-management/.openspec.yaml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
schema: spec-driven
|
||||
created: 2026-02-12
|
||||
115
openspec/changes/multi-chart-management/design.md
Normal file
115
openspec/changes/multi-chart-management/design.md
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
## Context
|
||||
|
||||
The Candle Annotator is a single-user Next.js app with a SQLite backend (Drizzle ORM). It currently stores candle data in a flat `candles` table and annotations in a flat `annotations` table — both globally scoped. Uploading a CSV wipes all existing candles (`db.delete(candles)`) and replaces them. Annotations have no association to any dataset and persist across uploads, becoming meaningless when the underlying candle data changes.
|
||||
|
||||
The frontend (`page.tsx`) manages state with React hooks — `annotations`, `activeTool`, `selectedLabelId`, etc. Data fetching happens via `GET /api/candles` (all candles) and `GET /api/annotations` (all annotations). There is no concept of a "chart" or "dataset" entity.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Users can upload multiple CSV files, each becoming a distinct named chart
|
||||
- Users can switch between charts without losing data
|
||||
- Annotations are scoped per-chart — switching charts loads only that chart's annotations
|
||||
- Existing data is preserved via a migration that assigns current candles/annotations to a default chart
|
||||
- The "Manage Annotation Types" link matches the app's theme system
|
||||
|
||||
**Non-Goals:**
|
||||
- Chart renaming or metadata editing (keep it simple for now)
|
||||
- Comparing multiple charts side-by-side
|
||||
- Sharing charts between users (single-user app)
|
||||
- Chart data editing after upload
|
||||
- Pagination or lazy-loading of charts list (unlikely to have hundreds)
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. New `charts` table as the parent entity
|
||||
|
||||
Add a `charts` table with `id`, `name`, `created_at`. Both `candles` and `annotations` gain a `chart_id` foreign key column.
|
||||
|
||||
**Rationale**: This is the natural relational model. A chart owns its candles and annotations. The alternative — separate SQLite databases per chart — would complicate queries and make migrations harder.
|
||||
|
||||
**Schema addition:**
|
||||
```
|
||||
charts: id (PK), name (text, unique), created_at (integer)
|
||||
candles: + chart_id (integer, FK → charts.id, NOT NULL)
|
||||
annotations: + chart_id (integer, FK → charts.id, NOT NULL)
|
||||
```
|
||||
|
||||
The `candles.time` unique constraint changes to a composite unique on `(chart_id, time)` since different charts can have candles at the same timestamp.
|
||||
|
||||
### 2. Upload creates a new chart, named from filename
|
||||
|
||||
When a CSV is uploaded, the server:
|
||||
1. Strips the `.csv` extension from the filename to derive the chart name
|
||||
2. If a chart with that name already exists, appends a numeric suffix (e.g., `btc-daily-2`)
|
||||
3. Creates a row in `charts`
|
||||
4. Inserts all candle rows with the new `chart_id`
|
||||
5. Returns the new chart's `id` and `name` in the response
|
||||
|
||||
**Rationale**: Using the filename is the most intuitive naming. Users typically name their CSV files meaningfully (e.g., `BTC-1H.csv`, `ETH-daily.csv`). No extra UI input required.
|
||||
|
||||
**Alternative considered**: Prompting the user for a name before upload. Rejected because it adds friction and the filename is usually sufficient.
|
||||
|
||||
### 3. Chart selector in sidebar, above file upload
|
||||
|
||||
Add a dropdown/select component in the sidebar header area showing the active chart name. Clicking it reveals the list of available charts. Selecting a chart sets `activeChartId` in state, triggering data refetch.
|
||||
|
||||
**Placement**: Between the app title block and the file upload section. This keeps it prominent and logically grouped — you pick your chart, then work with its tools.
|
||||
|
||||
**Rationale**: A sidebar dropdown is consistent with the existing layout. It's always visible and doesn't require navigation. The alternative — a separate page for chart management — adds unnecessary complexity for what is essentially a picker.
|
||||
|
||||
### 4. API scoping via query parameter `?chartId=`
|
||||
|
||||
All existing endpoints gain chart awareness:
|
||||
- `GET /api/candles?chartId=N` — returns candles for chart N
|
||||
- `GET /api/annotations?chartId=N` — returns annotations for chart N
|
||||
- `POST /api/annotations` — body includes `chart_id`
|
||||
- `GET /api/export?chartId=N` — exports annotations for chart N
|
||||
- New `GET /api/charts` — lists all charts
|
||||
- New `DELETE /api/charts/[id]` — deletes chart and cascading candles/annotations
|
||||
|
||||
**Rationale**: Query parameters are the simplest approach. The alternative — nested routes like `/api/charts/[id]/candles` — is more RESTful but would require creating new route files and updating all frontend fetch calls to different URLs. Query params let us modify existing routes in place.
|
||||
|
||||
### 5. Frontend state: `activeChartId` in page.tsx
|
||||
|
||||
Add `activeChartId` and `charts` state to `page.tsx`. When `activeChartId` changes:
|
||||
1. Fetch candles for that chart via `GET /api/candles?chartId=N`
|
||||
2. Fetch annotations for that chart via `GET /api/annotations?chartId=N`
|
||||
3. Re-render the chart and sidebar
|
||||
|
||||
On initial load, select the most recently created chart (or show empty state if none exist).
|
||||
|
||||
**Rationale**: Keeping state in `page.tsx` is consistent with the existing pattern. No need for React Context or a state library — the app is a single page with a flat component hierarchy.
|
||||
|
||||
### 6. Migration strategy: default chart for existing data
|
||||
|
||||
The Drizzle migration will:
|
||||
1. Create the `charts` table
|
||||
2. Add `chart_id` column to `candles` and `annotations` as nullable initially
|
||||
3. Insert a default chart row named "Imported Data" if any candles exist
|
||||
4. Update all existing candles and annotations to reference the default chart's ID
|
||||
5. Make `chart_id` NOT NULL (via table rebuild in SQLite, which Drizzle handles)
|
||||
6. Drop the old unique constraint on `candles.time`, add composite unique on `(chart_id, time)`
|
||||
|
||||
**Rationale**: SQLite doesn't support `ALTER TABLE ADD CONSTRAINT` or `ALTER COLUMN SET NOT NULL` directly. Drizzle's migration tooling handles this by recreating the table. The two-step nullable → backfill → not-null approach ensures no data loss.
|
||||
|
||||
### 7. Theme-aware "Manage Annotation Types" link
|
||||
|
||||
Replace the hardcoded `text-blue-600 hover:text-blue-800 underline` with a styled button or link using the app's theme variables. Use `text-muted-foreground hover:text-foreground` classes and remove the underline to match the sidebar's existing aesthetic.
|
||||
|
||||
**Rationale**: The current blue link is the only element not respecting the theme system. Using theme-aware Tailwind classes (`text-muted-foreground`, `hover:text-foreground`) ensures it works in both light and dark modes.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
**[Migration complexity with SQLite]** → SQLite doesn't support adding NOT NULL columns with foreign keys easily. Drizzle's migration tool handles table rebuilds, but the migration should be tested against a copy of production data before deploying. Keep a backup of `data/candles.db` before running.
|
||||
|
||||
**[Breaking API change]** → Existing API calls without `chartId` will break. Mitigation: If `chartId` is omitted, fall back to the most recently created chart. This provides backward compatibility during the transition and for the export endpoint.
|
||||
|
||||
**[Chart deletion cascading]** → Deleting a chart must also delete its candles and annotations. SQLite foreign key support requires `PRAGMA foreign_keys = ON`. Alternatively, handle cascading deletes in application code (delete candles + annotations + chart in a transaction). Application-level cascading is more reliable since SQLite foreign key enforcement requires per-connection pragma.
|
||||
|
||||
**[No chart limit]** → Users could upload many CSVs and accumulate large amounts of data. For now this is acceptable (single-user app, SQLite handles reasonable sizes). If needed later, add a chart count limit or storage warning.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Should deleting a chart require confirmation? (Likely yes — implement a confirmation dialog in the UI.)
|
||||
- Should the chart selector show metadata like date range or candle count? (Nice to have, not required for v1.)
|
||||
30
openspec/changes/multi-chart-management/proposal.md
Normal file
30
openspec/changes/multi-chart-management/proposal.md
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
## Why
|
||||
|
||||
The application currently supports only a single dataset at a time — uploading a new CSV replaces all existing candle data and leaves annotations orphaned. Users lose their previous work every time they load new data. To support iterative analysis across multiple instruments or timeframes, the app needs to manage multiple charts with independent annotation sets.
|
||||
|
||||
## What Changes
|
||||
|
||||
- **New "charts" entity**: Introduce a `charts` table to represent named datasets. Each chart holds its own set of candles and annotations.
|
||||
- **Upload creates a new chart**: Uploading a CSV creates a new chart (named from the filename) rather than replacing global candle data. **BREAKING** — existing candle/annotation data will need a migration to belong to a default chart.
|
||||
- **Chart selector UI**: A dropdown or list in the sidebar lets users switch between previously uploaded charts. The selected chart's candles and annotations load into the view.
|
||||
- **Per-chart annotations**: Annotations are scoped to a chart via a foreign key. Switching charts loads only that chart's annotations.
|
||||
- **Manage Annotation Types link restyled**: The "Manage Annotation Types" link is restyled to match the app's theme system (uses theme-aware CSS variables instead of hardcoded blue).
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `chart-management`: Managing multiple named charts — creating, selecting, listing, and deleting chart datasets. Includes the chart selector UI component and the charts data model.
|
||||
|
||||
### Modified Capabilities
|
||||
- `data-ingestion`: Upload now creates a new chart instead of replacing all candle data. Candles are associated with a specific chart ID.
|
||||
- `backend-api`: All candle and annotation endpoints gain a `chartId` scope. New endpoints for listing/deleting charts.
|
||||
- `chart-canvas`: Chart rendering loads candles filtered by the active chart ID.
|
||||
- `label-management`: Annotations are scoped to a chart. Switching charts loads/saves the correct annotation set.
|
||||
- `ui-shell`: Sidebar gains a chart selector component. "Manage Annotation Types" link is restyled to use theme-aware styling.
|
||||
|
||||
## Impact
|
||||
|
||||
- **Database schema**: New `charts` table. `candles` and `annotations` tables gain a `chart_id` foreign key column. Requires a migration.
|
||||
- **API routes**: `/api/upload`, `/api/candles`, `/api/annotations` all change to be chart-scoped. New `/api/charts` endpoint for CRUD operations.
|
||||
- **Frontend state**: `page.tsx` needs an `activeChartId` state. Components receive chart context for data fetching.
|
||||
- **Data migration**: Existing candles and annotations are migrated into a default chart so no data is lost on upgrade.
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Upload endpoint
|
||||
The system SHALL provide a `POST /api/upload` endpoint that accepts a CSV file via multipart form data. The endpoint SHALL create a new chart (named from the uploaded filename without extension), parse the CSV using papaparse, validate the format, and insert all candle records into the `candles` table with the new chart's `chart_id` within a single database transaction. On success, the endpoint SHALL return a JSON response with the chart `id`, chart `name`, and the count of inserted records. On failure, it SHALL return an appropriate error status and message without creating a chart.
|
||||
|
||||
#### Scenario: Successful upload
|
||||
- **WHEN** a valid CSV file named "BTC-1H.csv" is sent to POST /api/upload
|
||||
- **THEN** endpoint creates a chart named "BTC-1H", inserts candles with that chart_id, and returns `{ "success": true, "count": <number>, "chart": { "id": <number>, "name": "BTC-1H" } }` with HTTP 200
|
||||
|
||||
#### Scenario: Invalid CSV upload
|
||||
- **WHEN** a CSV with missing or invalid headers is sent to POST /api/upload
|
||||
- **THEN** endpoint returns `{ "error": "<description>" }` with HTTP 400 and does not create a chart
|
||||
|
||||
#### Scenario: No file provided
|
||||
- **WHEN** POST /api/upload is called without a file
|
||||
- **THEN** endpoint returns `{ "error": "No file provided" }` with HTTP 400
|
||||
|
||||
#### Scenario: Duplicate filename
|
||||
- **WHEN** a CSV named "BTC-1H.csv" is uploaded and a chart named "BTC-1H" already exists
|
||||
- **THEN** endpoint creates a chart named "BTC-1H-2" (or next available suffix) and inserts candles under that chart
|
||||
|
||||
### Requirement: Get annotations endpoint
|
||||
The system SHALL provide a `GET /api/annotations` endpoint that accepts an optional `chartId` query parameter. When `chartId` is provided, the endpoint SHALL return only annotations belonging to that chart. When `chartId` is omitted, the endpoint SHALL return annotations for the most recently created chart. Each annotation object SHALL include: `id`, `chart_id`, `timestamp`, `label_type`, `geometry` (parsed from JSON string or null), and `created_at`.
|
||||
|
||||
#### Scenario: Fetch annotations for specific chart
|
||||
- **WHEN** GET /api/annotations?chartId=3 is called
|
||||
- **THEN** endpoint returns a JSON array of annotations where chart_id equals 3, with HTTP 200
|
||||
|
||||
#### Scenario: Fetch annotations without chartId
|
||||
- **WHEN** GET /api/annotations is called without a chartId parameter
|
||||
- **THEN** endpoint returns annotations for the most recently created chart with HTTP 200
|
||||
|
||||
#### Scenario: No annotations exist for chart
|
||||
- **WHEN** GET /api/annotations?chartId=3 is called and no annotations exist for chart 3
|
||||
- **THEN** endpoint returns an empty JSON array `[]` with HTTP 200
|
||||
|
||||
### Requirement: Create annotation endpoint
|
||||
The system SHALL provide a `POST /api/annotations` endpoint that accepts a JSON body with fields: `timestamp` (required, integer), `label_type` (required, string), `chart_id` (required, integer), and `geometry` (optional, object). The endpoint SHALL validate the input, verify the chart exists, serialize geometry to JSON string if present, and insert the record into the `annotations` table. On success, it SHALL return the created annotation object with its assigned `id`.
|
||||
|
||||
#### Scenario: Create a marker annotation
|
||||
- **WHEN** POST /api/annotations is called with `{ "timestamp": 1700000000, "label_type": "break_up", "chart_id": 3 }`
|
||||
- **THEN** endpoint saves the annotation with chart_id 3 and returns the created object with `id` and HTTP 201
|
||||
|
||||
#### Scenario: Create a line annotation
|
||||
- **WHEN** POST /api/annotations is called with `{ "timestamp": 1700000000, "label_type": "line", "chart_id": 3, "geometry": { "startTime": 1700000000, "startPrice": 1.05, "endTime": 1700100000, "endPrice": 1.06 } }`
|
||||
- **THEN** endpoint saves the annotation with chart_id 3 and serialized geometry JSON, returns the created object with HTTP 201
|
||||
|
||||
#### Scenario: Invalid annotation data
|
||||
- **WHEN** POST /api/annotations is called with missing required fields (timestamp, label_type, or chart_id)
|
||||
- **THEN** endpoint returns `{ "error": "<description>" }` with HTTP 400
|
||||
|
||||
#### Scenario: Annotation for non-existent chart
|
||||
- **WHEN** POST /api/annotations is called with a chart_id that does not exist
|
||||
- **THEN** endpoint returns `{ "error": "Chart not found" }` with HTTP 404
|
||||
|
||||
### Requirement: Get candles endpoint
|
||||
The system SHALL provide a `GET /api/candles` endpoint that accepts an optional `chartId` query parameter. When `chartId` is provided, the endpoint SHALL return only candles belonging to that chart. When `chartId` is omitted, the endpoint SHALL return candles for the most recently created chart. Results SHALL be ordered by time ascending. Each object SHALL include: `time`, `open`, `high`, `low`, `close`.
|
||||
|
||||
#### Scenario: Fetch candles for specific chart
|
||||
- **WHEN** GET /api/candles?chartId=3 is called
|
||||
- **THEN** endpoint returns a JSON array of candle objects where chart_id equals 3, ordered by time ascending with HTTP 200
|
||||
|
||||
#### Scenario: Fetch candles without chartId
|
||||
- **WHEN** GET /api/candles is called without a chartId parameter
|
||||
- **THEN** endpoint returns candles for the most recently created chart with HTTP 200
|
||||
|
||||
#### Scenario: No candles exist for chart
|
||||
- **WHEN** GET /api/candles?chartId=3 is called and no candles exist for chart 3
|
||||
- **THEN** endpoint returns an empty JSON array `[]` with HTTP 200
|
||||
|
||||
### Requirement: Export annotations endpoint
|
||||
The system SHALL provide a `GET /api/export` endpoint that accepts an optional `chartId` query parameter. When `chartId` is provided, the endpoint SHALL export only annotations for that chart. When `chartId` is omitted, the endpoint SHALL export annotations for the most recently created chart. The CSV SHALL have columns: `timestamp`, `label_type`, `price`. The response SHALL set `Content-Type: text/csv` and `Content-Disposition: attachment; filename="annotations.csv"` headers.
|
||||
|
||||
#### Scenario: Export for specific chart
|
||||
- **WHEN** GET /api/export?chartId=3 is called and annotations exist for chart 3
|
||||
- **THEN** endpoint returns a CSV file download with only annotations belonging to chart 3
|
||||
|
||||
#### Scenario: Export without chartId
|
||||
- **WHEN** GET /api/export is called without a chartId parameter
|
||||
- **THEN** endpoint exports annotations for the most recently created chart
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Candlestick chart rendering
|
||||
The system SHALL render candle data as a candlestick chart using the `lightweight-charts` library (v4). The chart MUST display OHLC data with candlestick visuals using a black and white color scheme. Bullish candles SHALL have a white interior with black outline and black wicks. Bearish candles SHALL be completely black (black fill and black wicks). The chart SHALL be a client-side React component. The chart SHALL fetch and display candles scoped to the active chart by passing `chartId` to the `GET /api/candles` endpoint.
|
||||
|
||||
#### Scenario: Chart renders candle data for active chart
|
||||
- **WHEN** an active chart is selected and candle data exists for that chart
|
||||
- **THEN** the chart fetches candles via `GET /api/candles?chartId=<activeChartId>` and renders only that chart's candles
|
||||
|
||||
#### Scenario: Empty state
|
||||
- **WHEN** no charts exist or the active chart has no candle data
|
||||
- **THEN** the chart area displays an empty chart with a prompt to upload CSV data
|
||||
|
||||
#### Scenario: Bullish candle appearance
|
||||
- **WHEN** a candle's close price is higher than its open price (bullish)
|
||||
- **THEN** the candle displays with white interior, black border, and black wick
|
||||
|
||||
#### Scenario: Bearish candle appearance
|
||||
- **WHEN** a candle's close price is lower than its open price (bearish)
|
||||
- **THEN** the candle displays as completely black (black fill and black wick)
|
||||
|
||||
#### Scenario: Chart switches when active chart changes
|
||||
- **WHEN** user selects a different chart from the chart selector
|
||||
- **THEN** the chart clears existing data, fetches candles for the newly selected chart, and renders the new dataset
|
||||
|
||||
### Requirement: Annotation markers on chart
|
||||
The chart SHALL display visual markers for existing annotations using the `series.setMarkers()` API. Break Up annotations MUST appear as green upward arrows above the bar. Break Down annotations MUST appear as red downward arrows below the bar. Markers MUST update when annotations are added or deleted. Markers SHALL be scoped to the active chart by fetching annotations via `GET /api/annotations?chartId=<activeChartId>`.
|
||||
|
||||
#### Scenario: Break Up marker display
|
||||
- **WHEN** a Break Up annotation exists for a candle timestamp in the active chart
|
||||
- **THEN** a green upward arrow marker appears above that candle on the chart
|
||||
|
||||
#### Scenario: Break Down marker display
|
||||
- **WHEN** a Break Down annotation exists for a candle timestamp in the active chart
|
||||
- **THEN** a red downward arrow marker appears below that candle on the chart
|
||||
|
||||
#### Scenario: Marker updates on annotation change
|
||||
- **WHEN** user adds or deletes an annotation on the active chart
|
||||
- **THEN** chart markers update immediately without requiring a page reload
|
||||
|
||||
#### Scenario: Markers refresh on chart switch
|
||||
- **WHEN** user switches to a different chart
|
||||
- **THEN** markers from the previous chart are cleared and markers for the new chart's annotations are loaded
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
## ADDED Requirements
|
||||
|
||||
### Requirement: Charts database table
|
||||
The system SHALL store chart datasets in a `charts` table with columns: `id` (integer primary key, auto-increment), `name` (text, unique), `created_at` (integer, Unix timestamp). Each chart represents a distinct uploaded CSV dataset.
|
||||
|
||||
#### Scenario: Schema structure
|
||||
- **WHEN** the database is initialized
|
||||
- **THEN** the `charts` table exists with all required columns and constraints
|
||||
|
||||
#### Scenario: Unique chart names
|
||||
- **WHEN** a chart is created with a name that already exists
|
||||
- **THEN** the system appends a numeric suffix (e.g., "btc-daily-2") to ensure uniqueness
|
||||
|
||||
### Requirement: Chart selector in sidebar
|
||||
The system SHALL display a chart selector component in the sidebar, positioned between the app title/header section and the file upload section. The selector SHALL show the name of the currently active chart and allow the user to switch between all available charts.
|
||||
|
||||
#### Scenario: Chart selector renders with charts
|
||||
- **WHEN** one or more charts exist in the database
|
||||
- **THEN** the sidebar displays a selector showing the active chart name, with a dropdown listing all available charts sorted by creation date (newest first)
|
||||
|
||||
#### Scenario: Chart selector with no charts
|
||||
- **WHEN** no charts exist in the database
|
||||
- **THEN** the selector displays a placeholder message "No charts — upload a CSV to get started"
|
||||
|
||||
#### Scenario: Switch active chart
|
||||
- **WHEN** user selects a different chart from the selector dropdown
|
||||
- **THEN** the system updates `activeChartId` state, fetches candles and annotations for the selected chart, and re-renders the chart and sidebar label list
|
||||
|
||||
#### Scenario: Active chart persistence on page load
|
||||
- **WHEN** the page loads and charts exist
|
||||
- **THEN** the system selects the most recently created chart as the active chart
|
||||
|
||||
### Requirement: Delete chart
|
||||
The system SHALL allow users to delete a chart and all its associated candles and annotations.
|
||||
|
||||
#### Scenario: Delete chart with confirmation
|
||||
- **WHEN** user clicks the delete button next to a chart in the selector
|
||||
- **THEN** the system displays a confirmation dialog: "Delete chart '{name}' and all its candles and annotations? This cannot be undone."
|
||||
|
||||
#### Scenario: Confirm chart deletion
|
||||
- **WHEN** user confirms chart deletion
|
||||
- **THEN** the system deletes the chart, all its candles, and all its annotations from the database, removes the chart from the selector, and switches to the next available chart (or shows empty state if none remain)
|
||||
|
||||
#### Scenario: Cancel chart deletion
|
||||
- **WHEN** user cancels chart deletion
|
||||
- **THEN** no data is deleted and the dialog closes
|
||||
|
||||
#### Scenario: Delete last chart
|
||||
- **WHEN** user deletes the only remaining chart
|
||||
- **THEN** the system shows the empty state with no chart selected and prompts the user to upload a CSV
|
||||
|
||||
### Requirement: List charts API
|
||||
The system SHALL provide a `GET /api/charts` endpoint that returns all charts as a JSON array, ordered by `created_at` descending. Each chart object SHALL include: `id`, `name`, `created_at`.
|
||||
|
||||
#### Scenario: Fetch all charts
|
||||
- **WHEN** GET /api/charts is called
|
||||
- **THEN** endpoint returns a JSON array of chart objects ordered by created_at descending with HTTP 200
|
||||
|
||||
#### Scenario: No charts exist
|
||||
- **WHEN** GET /api/charts is called and no charts exist
|
||||
- **THEN** endpoint returns an empty JSON array `[]` with HTTP 200
|
||||
|
||||
### Requirement: Delete chart API
|
||||
The system SHALL provide a `DELETE /api/charts/[id]` endpoint that deletes a chart and all its associated candles and annotations within a single transaction.
|
||||
|
||||
#### Scenario: Delete existing chart
|
||||
- **WHEN** DELETE /api/charts/5 is called and chart with id 5 exists
|
||||
- **THEN** endpoint deletes the chart, all candles with chart_id 5, and all annotations with chart_id 5, then returns `{ "success": true }` with HTTP 200
|
||||
|
||||
#### Scenario: Delete non-existent chart
|
||||
- **WHEN** DELETE /api/charts/999 is called and no chart with that id exists
|
||||
- **THEN** endpoint returns `{ "error": "Chart not found" }` with HTTP 404
|
||||
|
||||
### Requirement: Auto-select new chart after upload
|
||||
The system SHALL automatically switch to the newly created chart after a successful CSV upload.
|
||||
|
||||
#### Scenario: Upload creates and selects chart
|
||||
- **WHEN** user uploads a CSV and the upload succeeds
|
||||
- **THEN** the system creates a new chart, inserts candle data for that chart, adds the chart to the selector, and sets it as the active chart
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: CSV file upload
|
||||
The system SHALL provide a file upload component that accepts CSV files containing OHLC candle data. The CSV format MUST have columns: `time`, `open`, `high`, `low`, `close`. The `time` column SHALL accept both `YYYY-MM-DD` date strings and Unix timestamps (integer seconds). Uploading a CSV SHALL create a new chart (named from the filename without extension) and insert all candle rows associated with that chart, rather than replacing existing data.
|
||||
|
||||
#### Scenario: Valid CSV upload
|
||||
- **WHEN** user uploads a CSV file with valid headers (time, open, high, low, close) and valid data rows
|
||||
- **THEN** system creates a new chart named from the filename (without .csv extension), parses all rows, and stores them in the `candles` table with the new chart's `chart_id`
|
||||
|
||||
#### Scenario: CSV with Unix timestamps
|
||||
- **WHEN** user uploads a CSV where the `time` column contains Unix timestamps (e.g., 1700000000)
|
||||
- **THEN** system stores the timestamps as integers in the database with the correct `chart_id` and renders candles correctly on the chart
|
||||
|
||||
#### Scenario: CSV with date strings
|
||||
- **WHEN** user uploads a CSV where the `time` column contains date strings (e.g., "2024-01-15")
|
||||
- **THEN** system converts dates to Unix timestamps and stores them in the database with the correct `chart_id`
|
||||
|
||||
#### Scenario: Invalid CSV format
|
||||
- **WHEN** user uploads a CSV missing required headers or containing malformed data
|
||||
- **THEN** system displays an error message describing the issue, does not create a chart, and does not store any partial data
|
||||
|
||||
#### Scenario: Duplicate filename upload
|
||||
- **WHEN** user uploads a CSV whose filename (without extension) matches an existing chart name
|
||||
- **THEN** system appends a numeric suffix to the chart name (e.g., "btc-daily-2") and creates a new chart with the suffixed name
|
||||
|
||||
### Requirement: Candles database table
|
||||
The system SHALL store candle data in a `candles` table with columns: `id` (integer primary key, auto-increment), `chart_id` (integer, foreign key to `charts.id`, NOT NULL), `time` (integer, Unix timestamp), `open` (real), `high` (real), `low` (real), `close` (real). The table MUST have a composite unique constraint on `(chart_id, time)`.
|
||||
|
||||
#### Scenario: Schema structure
|
||||
- **WHEN** the database is initialized
|
||||
- **THEN** the `candles` table exists with all required columns including `chart_id` and the composite unique constraint on `(chart_id, time)`
|
||||
|
||||
#### Scenario: Same timestamp across different charts
|
||||
- **WHEN** two different charts have candles with the same Unix timestamp
|
||||
- **THEN** both records are stored successfully because the unique constraint is per-chart
|
||||
|
|
@ -0,0 +1,62 @@
|
|||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Label list in sidebar
|
||||
The Toolbox SHALL display a collapsible section showing all label annotations for the active chart with interactive controls.
|
||||
|
||||
#### Scenario: Display label list section
|
||||
- **WHEN** Toolbox renders with an active chart selected
|
||||
- **THEN** system displays "Label Annotations" section below the annotation tools with collapse/expand toggle button, showing only annotations belonging to the active chart
|
||||
|
||||
#### Scenario: Label list expanded
|
||||
- **WHEN** "Label Annotations" section is expanded
|
||||
- **THEN** system displays scrollable list of label annotations for the active chart sorted by timestamp (newest first), with each entry showing timestamp, label type badge, and delete button
|
||||
|
||||
#### Scenario: Label list collapsed
|
||||
- **WHEN** user clicks collapse button on "Label Annotations" section
|
||||
- **THEN** system hides the label list but shows count summary "Labels: X break_up, Y break_down" for the active chart
|
||||
|
||||
#### Scenario: Empty label list
|
||||
- **WHEN** no label annotations exist for the active chart
|
||||
- **THEN** section displays message "No labels yet. Click Break Up or Break Down tools to add labels."
|
||||
|
||||
#### Scenario: Label entry format
|
||||
- **WHEN** displaying a label in the list
|
||||
- **THEN** each entry shows formatted timestamp (e.g., "Feb 12, 14:30"), colored badge ("BREAK UP" in green or "BREAK DOWN" in red), and trash icon delete button
|
||||
|
||||
#### Scenario: Label list updates on chart switch
|
||||
- **WHEN** user switches to a different chart via the chart selector
|
||||
- **THEN** the label list clears and reloads with annotations belonging to the newly selected chart
|
||||
|
||||
### Requirement: Label count display
|
||||
The Toolbox SHALL display counts of each label type for the active chart.
|
||||
|
||||
#### Scenario: Display label counts
|
||||
- **WHEN** Toolbox renders and labels exist for the active chart
|
||||
- **THEN** system displays count summary "Break Up: X | Break Down: Y" at top of label section, reflecting only the active chart's annotations
|
||||
|
||||
#### Scenario: Zero labels
|
||||
- **WHEN** no labels exist for the active chart
|
||||
- **THEN** count summary displays "Break Up: 0 | Break Down: 0"
|
||||
|
||||
#### Scenario: Count updates after delete
|
||||
- **WHEN** user deletes a label from the active chart
|
||||
- **THEN** count summary updates immediately to reflect the new totals
|
||||
|
||||
#### Scenario: Counts update on chart switch
|
||||
- **WHEN** user switches to a different chart
|
||||
- **THEN** count summary updates to reflect the new chart's label counts
|
||||
|
||||
### Requirement: Delete all labels with confirmation
|
||||
The system SHALL provide a "Delete All Labels" button with confirmation dialog. The deletion SHALL only affect labels belonging to the active chart.
|
||||
|
||||
#### Scenario: Click Delete All Labels button
|
||||
- **WHEN** user clicks "Delete All Labels" button in Toolbox
|
||||
- **THEN** system displays confirmation dialog with message "Delete all label annotations for this chart (Break Up and Break Down)? This cannot be undone." and Cancel/Confirm buttons
|
||||
|
||||
#### Scenario: User confirms delete all labels
|
||||
- **WHEN** confirmation dialog is open and user clicks Confirm button
|
||||
- **THEN** system deletes only label annotations belonging to the active chart, removes all label markers from chart, clears label list, clears selection state, triggers annotation refresh, and closes dialog
|
||||
|
||||
#### Scenario: User cancels delete all labels
|
||||
- **WHEN** confirmation dialog is open and user clicks Cancel button
|
||||
- **THEN** system closes dialog without making any API calls or removing any labels
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: File upload interface
|
||||
The application SHALL provide a CSV file upload interface accessible from the sidebar. The upload component SHALL trigger the POST /api/upload endpoint. On success, the component SHALL add the newly created chart to the chart selector and set it as the active chart, triggering the chart and annotation data to refresh for the new chart.
|
||||
|
||||
#### Scenario: Upload via UI
|
||||
- **WHEN** user selects a CSV file through the upload interface
|
||||
- **THEN** the file is sent to the upload API, a new chart is created, the chart selector updates to include the new chart, and the new chart becomes active with its candles displayed
|
||||
|
||||
#### Scenario: Upload error display
|
||||
- **WHEN** the upload API returns an error
|
||||
- **THEN** the UI displays the error message to the user and no chart is created
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Theme-aware Manage Annotation Types link
|
||||
The "Manage Annotation Types" link in the sidebar header SHALL use theme-aware styling consistent with the rest of the application. The link SHALL NOT use hardcoded color values. It SHALL use `text-muted-foreground` and `hover:text-foreground` Tailwind classes to respect both light and dark themes.
|
||||
|
||||
#### Scenario: Link styling in dark mode
|
||||
- **WHEN** the application is in dark mode
|
||||
- **THEN** the "Manage Annotation Types" link uses muted foreground color and brightens on hover, consistent with the dark theme
|
||||
|
||||
#### Scenario: Link styling in light mode
|
||||
- **WHEN** the application is in light mode
|
||||
- **THEN** the "Manage Annotation Types" link uses muted foreground color and darkens on hover, consistent with the light theme
|
||||
|
||||
#### Scenario: Link visual consistency
|
||||
- **WHEN** the sidebar renders
|
||||
- **THEN** the "Manage Annotation Types" link does not use underline styling and visually matches other sidebar text elements
|
||||
56
openspec/changes/multi-chart-management/tasks.md
Normal file
56
openspec/changes/multi-chart-management/tasks.md
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
## 1. Database Schema & Migration
|
||||
|
||||
- [x] 1.1 Add `charts` table to Drizzle schema (`src/lib/db/schema.ts`) with columns: `id`, `name` (unique), `created_at`
|
||||
- [x] 1.2 Add `chart_id` foreign key column to `candles` table in schema, replace `time` unique constraint with composite unique on `(chart_id, time)`
|
||||
- [x] 1.3 Add `chart_id` foreign key column to `annotations` table in schema
|
||||
- [x] 1.4 Generate Drizzle migration files (`npx drizzle-kit generate`)
|
||||
- [x] 1.5 Write data migration logic: if existing candles exist, create a default "Imported Data" chart and assign all existing candles and annotations to it
|
||||
- [x] 1.6 Test migration against existing database with data
|
||||
|
||||
## 2. Charts API Endpoints
|
||||
|
||||
- [ ] 2.1 Create `GET /api/charts` endpoint — returns all charts ordered by `created_at` desc
|
||||
- [ ] 2.2 Create `DELETE /api/charts/[id]` endpoint — deletes chart + cascading candles and annotations in a transaction
|
||||
|
||||
## 3. Upload Endpoint Changes
|
||||
|
||||
- [ ] 3.1 Modify `POST /api/upload` to create a new chart named from the uploaded filename (strip `.csv` extension)
|
||||
- [ ] 3.2 Add duplicate name handling — append numeric suffix if chart name already exists
|
||||
- [ ] 3.3 Insert candles with the new chart's `chart_id` instead of deleting all existing candles
|
||||
- [ ] 3.4 Return chart `id` and `name` in the upload response JSON
|
||||
|
||||
## 4. Candles & Annotations API Scoping
|
||||
|
||||
- [ ] 4.1 Modify `GET /api/candles` to accept `?chartId=` query param and filter by chart_id (fall back to most recent chart if omitted)
|
||||
- [ ] 4.2 Modify `GET /api/annotations` to accept `?chartId=` query param and filter by chart_id (fall back to most recent chart if omitted)
|
||||
- [ ] 4.3 Modify `POST /api/annotations` to require `chart_id` in request body and store it
|
||||
- [ ] 4.4 Modify `GET /api/export` to accept `?chartId=` query param and scope exported annotations to that chart
|
||||
|
||||
## 5. Chart Selector UI Component
|
||||
|
||||
- [ ] 5.1 Create a `ChartSelector` component with dropdown listing all charts (name + created date), positioned in the sidebar between header and file upload
|
||||
- [ ] 5.2 Add delete button per chart in the dropdown with confirmation dialog
|
||||
- [ ] 5.3 Show "No charts — upload a CSV to get started" placeholder when no charts exist
|
||||
|
||||
## 6. Frontend State & Data Flow
|
||||
|
||||
- [ ] 6.1 Add `activeChartId` and `charts` state to `page.tsx`
|
||||
- [ ] 6.2 Fetch charts list on mount via `GET /api/charts`, auto-select the most recent chart
|
||||
- [ ] 6.3 Pass `activeChartId` to `CandleChart` component — update it to fetch candles/annotations with `?chartId=` param
|
||||
- [ ] 6.4 Update `handleUploadSuccess` to receive the new chart from the upload response, add it to `charts` state, and set it as `activeChartId`
|
||||
- [ ] 6.5 Update annotation create/delete flows to include `chart_id` in requests
|
||||
- [ ] 6.6 When `activeChartId` changes, refetch candles and annotations for the new chart
|
||||
- [ ] 6.7 Update export handler to include `?chartId=` in the export URL
|
||||
|
||||
## 7. UI Polish
|
||||
|
||||
- [ ] 7.1 Restyle "Manage Annotation Types" link — replace `text-blue-600 hover:text-blue-800 underline` with `text-muted-foreground hover:text-foreground` and remove underline
|
||||
- [ ] 7.2 Verify chart selector and all components render correctly in both light and dark themes
|
||||
|
||||
## 8. Testing & Verification
|
||||
|
||||
- [ ] 8.1 Test full workflow: upload CSV -> chart created -> switch charts -> annotations scoped correctly
|
||||
- [ ] 8.2 Test migration: existing database with candles/annotations migrates to default chart
|
||||
- [ ] 8.3 Test chart deletion: candles and annotations cascade-deleted
|
||||
- [ ] 8.4 Test edge cases: upload duplicate filename, delete last chart, upload with no charts existing
|
||||
- [ ] 8.5 Run build (`npm run build`) and fix any type errors
|
||||
|
|
@ -1,13 +1,22 @@
|
|||
import { sqliteTable, integer, real, text } from 'drizzle-orm/sqlite-core';
|
||||
import { sqliteTable, integer, real, text, uniqueIndex } from 'drizzle-orm/sqlite-core';
|
||||
|
||||
export const charts = sqliteTable('charts', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
name: text('name').notNull().unique(),
|
||||
created_at: integer('created_at').notNull(),
|
||||
});
|
||||
|
||||
export const candles = sqliteTable('candles', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
time: integer('time').notNull().unique(),
|
||||
chart_id: integer('chart_id').notNull().references(() => charts.id),
|
||||
time: integer('time').notNull(),
|
||||
open: real('open').notNull(),
|
||||
high: real('high').notNull(),
|
||||
low: real('low').notNull(),
|
||||
close: real('close').notNull(),
|
||||
});
|
||||
}, (table) => [
|
||||
uniqueIndex('candles_chart_time_unique').on(table.chart_id, table.time),
|
||||
]);
|
||||
|
||||
export const annotationTypes = sqliteTable('annotation_types', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
|
|
@ -22,6 +31,7 @@ export const annotationTypes = sqliteTable('annotation_types', {
|
|||
|
||||
export const annotations = sqliteTable('annotations', {
|
||||
id: integer('id').primaryKey({ autoIncrement: true }),
|
||||
chart_id: integer('chart_id').notNull().references(() => charts.id),
|
||||
timestamp: integer('timestamp').notNull(),
|
||||
label_type: text('label_type').notNull(),
|
||||
geometry: text('geometry'), // JSON string for line coordinates
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue