battery_tracker/docs/TSD.md

11 KiB

Battery Tracker - Technical Specification Document

1. Tech Stack Overview

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

-- 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)

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

<RootLayout>
├── <Navigation />
├── <main>
│   └── {children}
└── <Toaster />  (for notifications)

<Dashboard>
├── <StatsCards />           # Available, In Use, Charging totals
├── <QuickActions />         # Common actions
└── <RecentActivity />       # Recent changes (optional)

<BatteryList>
├── <FilterBar />            # Filter by type, brand, status
├── <BatteryCard />[]        # Grid of battery groups
│   ├── <StatusBadges />     # Available/InUse/Charging counts
│   └── <QuickActionButtons />
└── <AddBatteryButton />

<DeviceList>
├── <DeviceCard />[]
│   ├── <DeviceInfo />
│   └── <AssignedBatteries />
└── <AddDeviceButton />

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

// Get aggregated stats
const stats = await db
  .select({
    available: sql<number>`SUM(available_count)`,
    charging: sql<number>`SUM(charging_count)`,
    inUse: sql<number>`SUM(quantity)`,
  })
  .from(batteryGroups)
  .leftJoin(deviceBatteries, eq(batteryGroups.id, deviceBatteries.batteryGroupId));

6.2 Move Batteries Action

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

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

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

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

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