candle-annotator/openspec/changes/archive/2026-02-12-enhance-annotation-features-and-deployment/design.md

18 KiB

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 <Collapsible> 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: <count> } 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:

# 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:

/* 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:

// 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 <head>
  • 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:

// 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.