# Battery Tracker - Technical Specification Document ## 1. Tech Stack Overview ### Recommended Stack | Layer | Technology | Rationale | |-------|------------|-----------| | **Framework** | Next.js 14 (App Router) | Full-stack React, simple deployment, API routes built-in | | **UI Library** | React 18 | Component-based, excellent ecosystem | | **Styling** | Tailwind CSS | Utility-first, rapid development, beautiful defaults | | **Database** | SQLite (via better-sqlite3) | Zero-config, file-based, perfect for personal use | | **ORM** | Drizzle ORM | Type-safe, lightweight, great SQLite support | | **State** | React hooks + Server Components | Minimal client state needed | | **Icons** | Lucide React | Clean, consistent icon set | ### Why This Stack? 1. **Next.js**: Single framework handles both frontend and backend. No separate API server needed. 2. **SQLite**: No database server to manage. Data stored in a single file. Easy to backup. 3. **Tailwind**: Beautiful UI without writing custom CSS. Built-in dark mode support. 4. **Drizzle**: Modern, type-safe ORM that's lighter than Prisma. ## 2. Project Structure ``` battery-tracker/ ├── src/ │ ├── app/ # Next.js App Router │ │ ├── layout.tsx # Root layout │ │ ├── page.tsx # Dashboard │ │ ├── batteries/ │ │ │ ├── page.tsx # Battery list │ │ │ └── [id]/page.tsx # Battery detail/edit │ │ ├── devices/ │ │ │ ├── page.tsx # Device list │ │ │ └── [id]/page.tsx # Device detail/edit │ │ └── api/ # API routes │ │ ├── batteries/ │ │ ├── devices/ │ │ └── types/ │ ├── components/ │ │ ├── ui/ # Reusable UI components │ │ ├── battery/ # Battery-specific components │ │ └── device/ # Device-specific components │ ├── lib/ │ │ ├── db/ │ │ │ ├── index.ts # Database connection │ │ │ ├── schema.ts # Drizzle schema │ │ │ └── migrations/ # Database migrations │ │ └── utils.ts # Utility functions │ └── types/ │ └── index.ts # TypeScript types ├── public/ ├── data/ # SQLite database file ├── package.json ├── tailwind.config.ts ├── drizzle.config.ts └── tsconfig.json ``` ## 3. Database Schema ```sql -- Battery types (AA, AAA, 18650, etc.) CREATE TABLE battery_types ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, is_custom BOOLEAN DEFAULT FALSE, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); -- Brands (Panasonic, Eneloop, etc.) CREATE TABLE brands ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); -- Battery groups (Brand + Type combination with counts) CREATE TABLE battery_groups ( id INTEGER PRIMARY KEY AUTOINCREMENT, brand_id INTEGER NOT NULL REFERENCES brands(id), type_id INTEGER NOT NULL REFERENCES battery_types(id), available_count INTEGER DEFAULT 0, charging_count INTEGER DEFAULT 0, notes TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE(brand_id, type_id) ); -- Devices CREATE TABLE devices ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, description TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ); -- Device-Battery assignments CREATE TABLE device_batteries ( id INTEGER PRIMARY KEY AUTOINCREMENT, device_id INTEGER NOT NULL REFERENCES devices(id) ON DELETE CASCADE, battery_group_id INTEGER NOT NULL REFERENCES battery_groups(id), quantity INTEGER NOT NULL DEFAULT 1, assigned_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE(device_id, battery_group_id) ); ``` ### Drizzle Schema (TypeScript) ```typescript // src/lib/db/schema.ts import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'; export const batteryTypes = sqliteTable('battery_types', { id: integer('id').primaryKey({ autoIncrement: true }), name: text('name').notNull().unique(), isCustom: integer('is_custom', { mode: 'boolean' }).default(false), createdAt: integer('created_at', { mode: 'timestamp' }).defaultNow(), }); export const brands = sqliteTable('brands', { id: integer('id').primaryKey({ autoIncrement: true }), name: text('name').notNull().unique(), createdAt: integer('created_at', { mode: 'timestamp' }).defaultNow(), }); export const batteryGroups = sqliteTable('battery_groups', { id: integer('id').primaryKey({ autoIncrement: true }), brandId: integer('brand_id').notNull().references(() => brands.id), typeId: integer('type_id').notNull().references(() => batteryTypes.id), availableCount: integer('available_count').default(0), chargingCount: integer('charging_count').default(0), notes: text('notes'), createdAt: integer('created_at', { mode: 'timestamp' }).defaultNow(), updatedAt: integer('updated_at', { mode: 'timestamp' }).defaultNow(), }); export const devices = sqliteTable('devices', { id: integer('id').primaryKey({ autoIncrement: true }), name: text('name').notNull(), description: text('description'), createdAt: integer('created_at', { mode: 'timestamp' }).defaultNow(), updatedAt: integer('updated_at', { mode: 'timestamp' }).defaultNow(), }); export const deviceBatteries = sqliteTable('device_batteries', { id: integer('id').primaryKey({ autoIncrement: true }), deviceId: integer('device_id').notNull().references(() => devices.id), batteryGroupId: integer('battery_group_id').notNull().references(() => batteryGroups.id), quantity: integer('quantity').notNull().default(1), assignedAt: integer('assigned_at', { mode: 'timestamp' }).defaultNow(), }); ``` ## 4. API Design ### RESTful Endpoints ``` # Battery Types GET /api/types # List all battery types POST /api/types # Create custom type # Brands GET /api/brands # List all brands POST /api/brands # Create brand # Battery Groups GET /api/batteries # List all battery groups (with brand/type info) POST /api/batteries # Create battery group GET /api/batteries/:id # Get single battery group PATCH /api/batteries/:id # Update battery group DELETE /api/batteries/:id # Delete battery group # Quick Actions POST /api/batteries/:id/charge # Move X batteries to charging POST /api/batteries/:id/available # Move X batteries to available POST /api/batteries/:id/assign # Assign to device # Devices GET /api/devices # List all devices (with battery assignments) POST /api/devices # Create device GET /api/devices/:id # Get single device PATCH /api/devices/:id # Update device DELETE /api/devices/:id # Delete device # Device Battery Management POST /api/devices/:id/batteries # Assign batteries to device DELETE /api/devices/:id/batteries/:gid # Remove batteries from device ``` ## 5. Component Architecture ### Key Components ``` ├── ├──
│ └── {children} └── (for notifications) ├── # Available, In Use, Charging totals ├── # Common actions └── # Recent changes (optional) ├── # Filter by type, brand, status ├── [] # Grid of battery groups │ ├── # Available/InUse/Charging counts │ └── └── ├── [] │ ├── │ └── └── ``` ### Shared UI Components - `Button` - Primary, secondary, danger variants - `Card` - Container with shadow - `Badge` - Status indicators - `Input` / `Select` - Form elements - `Modal` - For forms and confirmations - `Toast` - Non-intrusive notifications ## 6. Key Features Implementation ### 6.1 Dashboard Stats Query ```typescript // Get aggregated stats const stats = await db .select({ available: sql`SUM(available_count)`, charging: sql`SUM(charging_count)`, inUse: sql`SUM(quantity)`, }) .from(batteryGroups) .leftJoin(deviceBatteries, eq(batteryGroups.id, deviceBatteries.batteryGroupId)); ``` ### 6.2 Move Batteries Action ```typescript // Move batteries from available to charging async function startCharging(groupId: number, count: number) { await db .update(batteryGroups) .set({ availableCount: sql`available_count - ${count}`, chargingCount: sql`charging_count + ${count}`, updatedAt: new Date(), }) .where(eq(batteryGroups.id, groupId)); } ``` ### 6.3 Assign to Device ```typescript async function assignToDevice(groupId: number, deviceId: number, quantity: number) { await db.transaction(async (tx) => { // Decrement available count await tx .update(batteryGroups) .set({ availableCount: sql`available_count - ${quantity}` }) .where(eq(batteryGroups.id, groupId)); // Add/update device assignment await tx .insert(deviceBatteries) .values({ deviceId, batteryGroupId: groupId, quantity }) .onConflictDoUpdate({ target: [deviceBatteries.deviceId, deviceBatteries.batteryGroupId], set: { quantity: sql`quantity + ${quantity}` }, }); }); } ``` ## 7. UI/UX Design Details ### Color Scheme ```css /* Status Colors */ --available: #22c55e; /* Green 500 */ --in-use: #3b82f6; /* Blue 500 */ --charging: #f59e0b; /* Amber 500 */ /* Neutral Palette */ --background: #f8fafc; /* Slate 50 */ --card: #ffffff; --text: #0f172a; /* Slate 900 */ --muted: #64748b; /* Slate 500 */ ``` ### Responsive Breakpoints - Mobile: < 640px (single column) - Tablet: 640px - 1024px (2 columns) - Desktop: > 1024px (3-4 columns) ## 8. Development Setup ```bash # Create project npx create-next-app@latest battery-tracker --typescript --tailwind --app # Install dependencies npm install drizzle-orm better-sqlite3 npm install -D drizzle-kit @types/better-sqlite3 # Run development npm run dev # Database migrations npx drizzle-kit generate npx drizzle-kit migrate ``` ## 9. Deployment Options 1. **Self-hosted (Docker)**: Single container with SQLite volume 2. **Vercel + Turso**: If cloud SQLite needed 3. **Local only**: Run on home server/NAS ### Docker Compose (Simple) ```yaml version: '3.8' services: app: build: . ports: - "3000:3000" volumes: - ./data:/app/data # Persist SQLite ``` ## 10. Future Considerations - **PWA Support**: Add service worker for offline access - **Import/Export**: JSON backup/restore - **Dark Mode**: Tailwind dark mode utilities - **Search**: Full-text search across batteries and devices