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:
- Enable efficient bulk annotation management (delete all labels, delete all lines)
- Provide visibility into all annotations via sidebar list with search/filter
- Enable one-click production deployment via Docker
- Transform UI to distinctive hacker/terminal aesthetic while maintaining usability
- Create implementation plan detailed enough for Haiku model to execute
Non-Goals:
- Multi-user support or authentication
- Undo/redo functionality (can be added later)
- Import/export of individual annotations (CSV export already exists)
- Real-time collaboration features
- Mobile responsive design (desktop-focused tool)
- 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.tsxworks well, don't disturb it - Labels rendered by
CandleChart.tsxusing lightweight-charts markers API - Parallel state (
selectedLabelIdalongsideselectedLineId) keeps concerns separated
Alternatives Considered:
- Unified selection system: Single
selectedAnnotationIdfor 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
selectedLabelIdstate inpage.tsx - Pass
selectedLabelIdandsetSelectedLabelIdtoCandleChart.tsx - Make markers clickable via
onClickhandler 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: autowith custom scrollbar styling (no virtualization needed initially) - If performance issues with 1000+ items, add
react-windowin 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.tsDELETE 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/dataas 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/andpublic/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)
- Add
selectedLabelIdstate and selection handlers inCandleChart.tsx - Build label list UI in
Toolbox.tsx(collapsible section) - Implement search/filter functionality
- Add keyboard delete handler for labels
- Test with existing database, no schema changes
- Deployment: None (dev only), commit after testing
Phase 2: API Extensions (Backwards Compatible)
- Modify
DELETE /api/annotationsto accept query params - Add bulk delete logic with Drizzle ORM
- Create
GET /api/healthendpoint - Test bulk operations with Postman/curl
- Deployment: None (API changes backwards compatible), commit after testing
Phase 3: Hacker Theme (Visual Only)
- Load JetBrains Mono font in
layout.tsx - Update
globals.csswith new CSS variables - Extend
tailwind.config.tswith custom colors, shadows, animations - Apply theme classes to existing components (Toolbox, buttons, inputs)
- Test visual appearance and contrast ratios
- Deployment: None (CSS/styling only), commit after visual review
Phase 4: Docker Setup (New Deployment Path)
- Update
next.config.jsto enable standalone output - Create
Dockerfilewith multi-stage build - Create
docker-compose.ymlwith volume configuration - Create
.dockerignoreand.env.example - Test local build:
docker-compose up --build - Verify database persistence across container restarts
- Update
DEPLOYMENT.mdwith Docker instructions - 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.cssandtailwind.config.tscommits - 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
-
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
-
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
-
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
-
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
-
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.