From ce96895bc39865bf1b7e67129eac88f7f48fe913 Mon Sep 17 00:00:00 2001 From: Marko Djordjevic Date: Thu, 12 Feb 2026 14:49:26 +0100 Subject: [PATCH] feat: create comprehensive design document Technical decisions with rationale: - Label selection: parallel state to avoid disturbing line logic - Label list UI: collapsible section in Toolbox with search/filter - API design: extend DELETE with query params for bulk operations - Docker: standalone output with multi-stage build - Theme: CSS variables + Tailwind extension strategy - Feedback: Toast component with terminal formatting Includes risk mitigation, migration plan, and testing checklist --- .../design.md | 439 ++++++++++++++++++ 1 file changed, 439 insertions(+) create mode 100644 openspec/changes/enhance-annotation-features-and-deployment/design.md diff --git a/openspec/changes/enhance-annotation-features-and-deployment/design.md b/openspec/changes/enhance-annotation-features-and-deployment/design.md new file mode 100644 index 0000000..0c39a08 --- /dev/null +++ b/openspec/changes/enhance-annotation-features-and-deployment/design.md @@ -0,0 +1,439 @@ +# Design: Annotation Features and Deployment Enhancement + +## Context + +### Current State +The candle annotator has a working annotation system with: +- Line drawing with color selection, visual feedback, selection, and endpoint dragging (Phase 1-4 complete) +- Label markers (break_up/break_down) added by clicking candles +- Delete tool that removes annotations by clicking near them +- SQLite database storing all annotations with geometry and color +- Next.js 16 app with lightweight-charts for visualization +- shadcn/ui components with dark slate theme +- Local development workflow only + +### Constraints +- **No breaking changes**: Existing functionality must remain intact +- **No new dependencies**: Use existing Next.js, React, Tailwind, shadcn/ui stack +- **Single-user application**: No authentication or multi-user considerations +- **SQLite database**: Must maintain compatibility with existing schema +- **Lightweight-charts integration**: Chart library API is fixed, work within SVG overlay pattern +- **Performance**: Chart rendering must maintain 60fps during annotation interactions + +### Stakeholders +- **Primary user**: Marko (developer/trader creating training data) +- **Deployment target**: Server environment with Docker support +- **Implementation**: Haiku model must be able to follow the implementation plan + +## Goals / Non-Goals + +**Goals:** +1. Enable efficient bulk annotation management (delete all labels, delete all lines) +2. Provide visibility into all annotations via sidebar list with search/filter +3. Enable one-click production deployment via Docker +4. Transform UI to distinctive hacker/terminal aesthetic while maintaining usability +5. Create implementation plan detailed enough for Haiku model to execute + +**Non-Goals:** +1. Multi-user support or authentication +2. Undo/redo functionality (can be added later) +3. Import/export of individual annotations (CSV export already exists) +4. Real-time collaboration features +5. Mobile responsive design (desktop-focused tool) +6. Internationalization (English only) + +## Decisions + +### Decision 1: Label Selection Architecture + +**Choice**: Use separate state management for label selection parallel to existing line selection + +**Why?** +- Lines and labels are fundamentally different (geometry vs point markers) +- Existing line selection in `SvgOverlay.tsx` works well, don't disturb it +- Labels rendered by `CandleChart.tsx` using lightweight-charts markers API +- Parallel state (`selectedLabelId` alongside `selectedLineId`) keeps concerns separated + +**Alternatives Considered:** +- **Unified selection system**: Single `selectedAnnotationId` for both lines and labels + - ❌ Rejected: Would require refactoring working line selection code, higher risk + - ❌ Different selection behaviors (lines: click geometry, labels: click marker) +- **Event bus pattern**: Pub/sub for selection events + - ❌ Rejected: Over-engineering for single-user app, adds complexity + +**Implementation:** +- Add `selectedLabelId` state in `page.tsx` +- Pass `selectedLabelId` and `setSelectedLabelId` to `CandleChart.tsx` +- Make markers clickable via `onClick` handler in marker creation +- Highlight selected marker with custom scale and color properties +- Deselect on Escape key (global keyboard handler in page component) + +### Decision 2: Label List UI Structure + +**Choice**: Collapsible section within existing Toolbox sidebar with virtualized scrolling + +**Why?** +- Keeps all controls in one place (consistency) +- Toolbox already has vertical space for expansion +- Users may have hundreds/thousands of labels, need efficient rendering +- No need for new layout areas or floating panels + +**Alternatives Considered:** +- **Separate right sidebar**: New panel on right side of chart + - ❌ Rejected: Takes screen real estate from chart, awkward horizontal layout +- **Modal/overlay**: Floating panel over chart + - ❌ Rejected: Blocks chart view, poor UX for browsing annotations while viewing data +- **Bottom panel**: Horizontal list below chart + - ❌ Rejected: Vertical list more natural for timestamped items, less vertical space for chart + +**Implementation:** +- Add `` section in Toolbox below tool buttons, above Export +- Section header: "Label Annotations (X)" with expand/collapse icon +- When expanded: search input, filter dropdown, scrollable list (max-height: 400px) +- Each list item: formatted timestamp, colored badge, delete button +- Click item to select/highlight on chart +- Use CSS `overflow-y: auto` with custom scrollbar styling (no virtualization needed initially) +- If performance issues with 1000+ items, add `react-window` in future iteration + +### Decision 3: API Design for Bulk Operations + +**Choice**: Extend existing DELETE `/api/annotations` with query parameters + +**Why?** +- RESTful approach: DELETE resource with filters +- No new endpoints to maintain +- Backwards compatible (no query params = existing single-delete behavior still works via `/api/annotations/[id]`) +- Simple to implement and test + +**Query Parameter Schema:** +``` +DELETE /api/annotations?type=line // Delete all lines +DELETE /api/annotations?type=break_up // Delete all break_up labels +DELETE /api/annotations?type=break_down // Delete all break_down labels +DELETE /api/annotations?type=break_up,break_down // Delete all labels +DELETE /api/annotations?all=true // Delete everything (nuclear option) +``` + +**Alternatives Considered:** +- **POST /api/annotations/bulk-delete**: Separate endpoint + - ❌ Rejected: Not RESTful (POST for delete), adds complexity +- **DELETE with request body**: `{ types: ['break_up', 'break_down'] }` + - ❌ Rejected: DELETE with body is controversial in HTTP semantics, some proxies strip bodies +- **Separate endpoints per type**: `/api/annotations/lines`, `/api/annotations/labels` + - ❌ Rejected: More endpoints to maintain, inconsistent with current design + +**Implementation:** +- Modify `src/app/api/annotations/route.ts` DELETE handler +- Parse query params: `const { type, all } = request.nextUrl.searchParams` +- Use Drizzle ORM with conditional WHERE clause +- Return `{ success: true, deleted: }` for confirmation UI + +### Decision 4: Docker Strategy - Standalone Output + +**Choice**: Next.js standalone output mode with multi-stage build + +**Why?** +- Minimal production bundle (~50MB for Next.js runtime vs ~500MB with full node_modules) +- Fast container builds (cached layers for dependencies) +- Official Next.js recommendation for containerization +- Better-sqlite3 works in standalone mode (native module properly bundled) + +**Dockerfile Structure:** +```dockerfile +# Stage 1: Build +FROM node:18-alpine AS builder +WORKDIR /app +COPY package*.json ./ +RUN npm ci +COPY . . +RUN npm run build + +# Stage 2: Production +FROM node:18-alpine +WORKDIR /app +RUN addgroup -g 1001 -S nodejs && adduser -S nextjs -u 1001 +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static +COPY --from=builder --chown=nextjs:nodejs /app/public ./public +USER nextjs +EXPOSE 3000 +CMD ["node", "server.js"] +``` + +**Alternatives Considered:** +- **Full node_modules copy**: Copy entire node_modules to production + - ❌ Rejected: 5-10x larger images, slower deployments, unnecessary dev dependencies +- **Distroless base images**: Google distroless/nodejs + - ❌ Rejected: No shell for debugging, alpine is small enough, better-sqlite3 compatibility unclear +- **Docker layer caching with pnpm/yarn**: Different package manager + - ❌ Rejected: Project uses npm, no reason to change, npm v7+ has similar caching + +**Database Persistence:** +- Mount `/app/data` as Docker volume +- SQLite file at `/app/data/candles.db` +- Named volume in docker-compose: `candle-data:/app/data` + +### Decision 5: Theme Implementation - CSS Variables + Tailwind Extension + +**Choice**: Replace existing CSS variables in `globals.css` with hacker theme values, extend Tailwind config with custom colors and animations + +**Why?** +- Minimal refactoring: existing shadcn/ui components already use CSS variables +- Tailwind extension provides utility classes for new theme features (glow effects, animations) +- No component rewrites needed, just style overrides +- Easy to toggle/revert if needed (swap CSS variable values) + +**Color Mapping Strategy:** +```css +/* globals.css - override existing variables */ +:root { + --background: 120 100% 4%; /* #0a0e0a - very dark green */ + --foreground: 120 100% 50%; /* #00ff41 - matrix green */ + --primary: 120 100% 50%; /* #00ff41 */ + --destructive: 348 100% 50%; /* #ff0040 - neon red */ + --border: 120 100% 10%; /* #003311 - dim green */ + /* ... map all existing variables to hacker theme equivalents */ +} +``` + +**Tailwind Extension:** +```typescript +// tailwind.config.ts - extend existing config +theme: { + extend: { + colors: { + matrix: '#00ff41', + matrixDim: '#00cc33', + matrixDark: '#003311', + neonRed: '#ff0040', + neonCyan: '#00d4ff', + // ... keep existing shadcn colors + }, + fontFamily: { + mono: ['JetBrains Mono', 'Fira Code', 'Courier New', 'monospace'], + }, + boxShadow: { + 'glow-sm': '0 0 8px #00ff41', + 'glow': '0 0 15px #00ff41, 0 0 30px rgba(0,255,65,0.5)', + 'glow-lg': '0 0 20px #00ff41, 0 0 40px rgba(0,255,65,0.5)', + }, + keyframes: { + 'glow-pulse': { + '0%, 100%': { boxShadow: '0 0 15px #00ff41' }, + '50%': { boxShadow: '0 0 30px #00ff41, 0 0 50px rgba(0,255,65,0.5)' }, + }, + flicker: { + '0%, 100%': { opacity: '1' }, + '50%': { opacity: '0.8' }, + }, + }, + animation: { + 'glow-pulse': 'glow-pulse 2s ease-in-out infinite', + 'flicker': 'flicker 0.1s ease-in-out', + }, + }, +} +``` + +**Alternatives Considered:** +- **Complete rewrite with different UI library**: Styled-components, Emotion + - ❌ Rejected: Massive effort, no benefit, Tailwind + shadcn works well +- **Theme toggle system**: Support multiple themes with switcher + - ❌ Rejected: Not in requirements, adds complexity, hacker theme is the goal +- **CSS-in-JS for glow effects**: Inline styles or styled-jsx + - ❌ Rejected: Inconsistent with existing Tailwind approach, harder to maintain + +**Font Loading:** +- Add JetBrains Mono via Google Fonts in `layout.tsx` `` +- Fallback chain: JetBrains Mono → Fira Code → Courier New → system monospace +- Set `font-family: var(--font-mono)` on body element + +### Decision 6: Feedback Messages - Toast Component + +**Choice**: Create reusable `Toast` component using shadcn/ui toast primitive, styled for terminal aesthetic + +**Why?** +- Consistent with existing shadcn/ui architecture +- Already have toast primitive in `components/ui/` (likely from shadcn init) +- Position: top-center, auto-dismiss after 4s +- Terminal-style formatting: `> SUCCESS: Operation completed [3 items]` + +**Implementation:** +```typescript +// src/components/ui/toast.tsx - style overrides +// Add terminal-style formatting +className="font-mono border-matrix bg-terminal text-matrix" + +// Usage in components: +import { useToast } from '@/hooks/use-toast' +const { toast } = useToast() + +toast({ + title: "> SUCCESS", + description: "Deleted all labels [15 items]", + variant: "default", // green +}) + +toast({ + title: "> ERROR", + description: "Failed to delete annotations [code: 500]", + variant: "destructive", // red +}) +``` + +**Alternatives Considered:** +- **Custom notification system**: Build from scratch + - ❌ Rejected: Reinventing the wheel, shadcn toast is battle-tested +- **Browser alert()**: Native dialogs + - ❌ Rejected: Ugly, blocks UI, no styling, poor UX +- **Console.log only**: No visual feedback + - ❌ Rejected: User won't see feedback, poor UX + +## Risks / Trade-offs + +### Risk 1: Theme Accessibility +**Risk**: Neon green on black may strain eyes during extended use, contrast issues for colorblind users + +**Mitigation:** +- Ensure WCAG AA contrast ratios (4.5:1 for text, 3:1 for UI elements) +- Use #00ff41 which has sufficient contrast on #0a0e0a background +- Provide option to revert theme in future (environment variable: `THEME=classic`) +- Include prefers-reduced-motion support to disable animations + +**Trade-off**: Distinctive aesthetic vs universal accessibility (accepting some users may prefer different theme) + +### Risk 2: Label List Performance with Large Datasets +**Risk**: 10,000+ labels may cause slow rendering and sluggish scrolling in sidebar list + +**Mitigation:** +- Set reasonable max-height (400px) for scrollable area +- If performance issues arise, add pagination or react-window virtualization in follow-up +- Monitor performance during development with test data (1000, 5000, 10000 labels) + +**Trade-off**: Simple initial implementation vs premature optimization (YAGNI principle - add virtualization only if needed) + +### Risk 3: Docker Image Size for Better-SQLite3 +**Risk**: Native SQLite module may require additional build dependencies in alpine, increasing image size + +**Mitigation:** +- Node:18-alpine includes necessary build tools (python, make, g++) +- Better-sqlite3 compiles during npm install in build stage +- Final image only includes compiled binary, not build tools +- Test build locally: `docker build --progress=plain .` to verify size (<200MB target) + +**Trade-off**: Alpine convenience vs potential build complexity (alpine is still best choice despite native modules) + +### Risk 4: Breaking Existing Line Selection +**Risk**: Adding label selection state and keyboard handlers may interfere with existing line selection logic + +**Mitigation:** +- Keep line and label selection completely separate (different state variables) +- Keyboard handler checks both states: if label selected, delete label; if line selected, delete line +- Thoroughly test all existing line operations (draw, select, drag, delete) after changes +- Use TypeScript strict mode to catch state management issues + +**Trade-off**: Duplicate code for selection logic vs refactoring working code (choose stability) + +### Risk 5: Next.js Standalone Mode Compatibility +**Risk**: Standalone output may not include all necessary files (static assets, environment configs) + +**Mitigation:** +- Follow official Next.js documentation for standalone mode +- Explicitly copy `.next/static/` and `public/` directories in Dockerfile +- Test docker build locally before committing +- Verify all pages, API routes, and static assets work in container + +**Trade-off**: Smaller image size vs potential missing files (standalone is well-tested by community) + +### Risk 6: Confirmation Dialog UX +**Risk**: Users may accidentally confirm bulk delete operations + +**Mitigation:** +- Use clear, explicit confirmation messages: "Delete all X annotations? This cannot be undone." +- Require deliberate click on "Confirm" button (not Enter key, no auto-focus) +- Show count of items to be deleted in confirmation message +- Use destructive button styling (red) to signal danger +- No undo system (out of scope), but users can re-annotate from CSV data + +**Trade-off**: Safety vs speed (prioritize safety for destructive operations) + +## Migration Plan + +### Phase 1: Label Management (No Deployment Impact) +1. Add `selectedLabelId` state and selection handlers in `CandleChart.tsx` +2. Build label list UI in `Toolbox.tsx` (collapsible section) +3. Implement search/filter functionality +4. Add keyboard delete handler for labels +5. Test with existing database, no schema changes +6. **Deployment**: None (dev only), commit after testing + +### Phase 2: API Extensions (Backwards Compatible) +1. Modify `DELETE /api/annotations` to accept query params +2. Add bulk delete logic with Drizzle ORM +3. Create `GET /api/health` endpoint +4. Test bulk operations with Postman/curl +5. **Deployment**: None (API changes backwards compatible), commit after testing + +### Phase 3: Hacker Theme (Visual Only) +1. Load JetBrains Mono font in `layout.tsx` +2. Update `globals.css` with new CSS variables +3. Extend `tailwind.config.ts` with custom colors, shadows, animations +4. Apply theme classes to existing components (Toolbox, buttons, inputs) +5. Test visual appearance and contrast ratios +6. **Deployment**: None (CSS/styling only), commit after visual review + +### Phase 4: Docker Setup (New Deployment Path) +1. Update `next.config.js` to enable standalone output +2. Create `Dockerfile` with multi-stage build +3. Create `docker-compose.yml` with volume configuration +4. Create `.dockerignore` and `.env.example` +5. Test local build: `docker-compose up --build` +6. Verify database persistence across container restarts +7. Update `DEPLOYMENT.md` with Docker instructions +8. **Deployment**: Push image to registry, deploy to production server + +### Rollback Strategy +- **Label Management**: No rollback needed (additive feature, disable by not using) +- **API Extensions**: Backwards compatible, no rollback needed +- **Hacker Theme**: Revert `globals.css` and `tailwind.config.ts` commits +- **Docker**: Rollback to direct Node.js deployment, use existing dev workflow + +### Testing Checklist +- [ ] All existing line drawing features work unchanged +- [ ] Label selection highlights correct marker on chart +- [ ] Label list displays all annotations with correct formatting +- [ ] Search/filter correctly narrows label list +- [ ] Delete individual label removes from chart and list +- [ ] Delete all labels removes all markers after confirmation +- [ ] Bulk delete API endpoints return correct counts +- [ ] Health check endpoint returns 200 +- [ ] Theme maintains WCAG AA contrast ratios +- [ ] All text uses monospace font +- [ ] Glow effects appear on hover/active states +- [ ] Docker container starts successfully +- [ ] Database persists across container restarts +- [ ] Application accessible on http://localhost:3000 in container + +## Open Questions + +1. **Should we add label editing (change type)?** + - Not in current scope, but consider for future + - Would require modal dialog to change break_up ↔ break_down + +2. **Should label list show price information?** + - Specs don't mention it, but could be useful + - Decision: Show timestamp and type only initially, add price in follow-up if requested + +3. **Should we limit the number of labels displayed in sidebar?** + - Pagination vs infinite scroll vs show all + - Decision: Show all initially, add pagination only if performance issues arise + +4. **Should Docker image include sample CSV data?** + - Useful for testing, but increases image size + - Decision: No, keep image minimal, provide sample CSV in repo documentation + +5. **Should we add keyboard shortcuts for tool switching (e.g., 'L' for line tool)?** + - Not in requirements, but could enhance UX + - Decision: Out of scope for this change, consider in future UX iteration + +These questions do not block implementation - proceed with spec-defined behavior and defer enhancements.