# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

GearBox is a single-user web app for managing gear collections (bikepacking, sim racing, etc.), tracking weight/price, and planning purchases through research threads. Full-stack TypeScript monolith running on Bun.

## Commands

```bash
# Development
bun run dev                 # Starts both Vite client (:5173) and Hono server (:3000) concurrently
bun run dev:client          # Vite dev server on :5173 (proxies /api to :3000)
bun run dev:server          # Hono server on :3000 with hot reload

# Database
bun run db:generate         # Generate Drizzle migration from schema changes
bun run db:push             # Apply migrations to gearbox.db

# Testing
bun test                    # Run all unit/integration tests
bun test tests/services/item.service.test.ts  # Run single test file
bun run test:e2e            # Run Playwright E2E tests
bun run test:e2e:ui         # Playwright UI mode for debugging

# Lint & Format
bun run lint                # Biome check (tabs, double quotes, organized imports)

# Build
bun run build               # Vite build → dist/client/
```

## Architecture

**Stack**: React 19 + Hono + Drizzle ORM + SQLite, all running on Bun.

### Client (`src/client/`)
- **Routing**: TanStack Router with file-based routes in `src/client/routes/`. Route tree auto-generated to `routeTree.gen.ts` — never edit manually.
- **Data fetching**: TanStack React Query via custom hooks in `src/client/hooks/` (e.g., `useItems`, `useThreads`, `useSetups`). Mutations invalidate related query keys.
- **UI state**: Zustand store (`stores/uiStore.ts`) for panel/dialog state only — server data lives in React Query.
- **API calls**: Thin fetch wrapper in `lib/api.ts` (`apiGet`, `apiPost`, `apiPut`, `apiDelete`, `apiUpload`).
- **Styling**: Tailwind CSS v4.

### Server (`src/server/`)
- **Routes** (`routes/`): Hono handlers with Zod validation via `@hono/zod-validator`. Delegate to services.
- **Services** (`services/`): Pure business logic functions that take a db instance. No HTTP awareness — testable without mocking.
- Route registration in `src/server/index.ts` via `app.route("/api/...", routes)`.

### Shared (`src/shared/`)
- **`schemas.ts`**: Zod schemas for API request validation (source of truth for types).
- **`types.ts`**: Types inferred from Zod schemas + Drizzle table definitions. No manual type duplication.

### Database (`src/db/`)
- **Schema**: `schema.ts` — Drizzle table definitions for SQLite.
- **Prices stored as cents** (`priceCents: integer`) to avoid float rounding.
- **Timestamps**: stored as integers (unix epoch) with `{ mode: "timestamp" }`.
- Tables: `categories`, `items`, `threads`, `threadCandidates`, `setups`, `setupItems`, `settings`, `users`, `sessions`, `apiKeys`.

### Testing (`tests/` and `e2e/`)
- **Unit/integration**: Bun test runner (`bun test`). Tests at service level and route level.
- `tests/helpers/db.ts`: `createTestDb()` creates in-memory SQLite via Drizzle migrations and seeds an "Uncategorized" category.
- **E2E**: Playwright (`bun run test:e2e`). Tests in `e2e/` run against a seeded SQLite database with the server in production mode. Seed script: `e2e/seed.ts`.

## Releasing

Releases are managed by a Gitea Actions workflow (`.gitea/workflows/release.yml`). **Never create tags or releases manually** — always trigger the pipeline.

The workflow runs CI (lint, test, build), computes the next version from the latest tag, generates a changelog, creates the tag, builds and pushes a Docker image, and creates a Gitea release.

Trigger via Gitea API:
```bash
curl -s -X POST "https://gitea.jeanlucmakiola.de/api/v1/repos/makiolaj/GearBox/actions/workflows/release.yml/dispatches" \
  -H "Authorization: token <GITEA_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{"ref": "Develop", "inputs": {"bump": "patch"}}'  # patch | minor | major
```

## Branching

- **Develop** is the main branch. Keep it clean — don't commit large feature work directly.
- For each new brainstorming/implementation session, create a feature branch off Develop (e.g., `feature/setup-impact-preview`, `fix/error-handling`).
- Merge back to Develop via PR or fast-forward merge when the work is complete and verified.

## Path Alias

`@/*` maps to `./src/*` (configured in tsconfig.json).

## Reusable Components

Always use existing components instead of rebuilding with plain HTML. Check `src/client/components/` before creating new form elements or UI patterns.

| Need | Use | Not |
|------|-----|-----|
| Category selection | `CategoryPicker` (icons, search, inline create) | Plain `<select>` |
| Icon selection | `IconPicker` (119 curated Lucide icons, grouped) | Manual icon input |
| Image upload | `ImageUpload` (preview, click-to-upload) | Raw file input |
| Lucide icon rendering | `LucideIcon` from `lib/iconData` | Direct SVG or lucide-react imports |
| Weight/price formatting | `useFormatters()` hook | Manual formatting |

## Key Patterns

- **Thread resolution**: Resolving a thread copies the winning candidate's data into a new item in the collection, sets `resolvedCandidateId`, and changes status to "resolved".
- **Setup item sync**: `PUT /api/setups/:id/items` replaces all setup_items atomically (delete all, re-insert).
- **Image uploads**: `POST /api/images` saves to S3-compatible storage (Garage/R2), returned as `imageFilename` on item/candidate records.
- **Image URL fetching**: `POST /api/images/from-url` fetches an image from a URL, saves to S3, returns `{ filename, sourceUrl }`.
- **Aggregates** (weight/cost totals): Computed via SQL on read, not stored on records.
- **Authentication**: Public-read, authenticated-write. Cookie sessions for web UI, API keys (`X-API-Key` header) for programmatic access. `POST /api/auth/setup` for first-time account creation. Auth middleware protects all POST/PUT/DELETE on `/api/*` except `/api/auth/*`.

## Authentication

- **OIDC via Logto**: Authentication is handled by an external Logto instance via `@hono/oidc-auth`. Users are redirected to Logto for login, and sessions are managed via OIDC cookies.
- **Programmatic access**: API keys created in Settings > API Keys. Pass via `X-API-Key` header.
- **Public read**: All GET endpoints work without auth. POST/PUT/DELETE require auth.
- **Auth routes**: `/api/auth/me`, `/api/auth/keys`, `/api/auth/profile`.
- **MCP OAuth**: OAuth 2.1 + PKCE for Claude mobile/web. Endpoints at `/oauth/*`. Uses existing GearBox credentials.

### Logto Setup

The Logto application must be configured with the correct scopes. In the Logto admin console, go to the application settings and ensure the following **User Scopes** are granted: `openid`, `profile`, `email` (matching the `OIDC_SCOPES` env var).

**Required env vars:**
```bash
OIDC_ISSUER=https://your-logto-domain/oidc    # Logto OIDC issuer URL
OIDC_CLIENT_ID=<client-id>                     # From Logto app settings
OIDC_CLIENT_SECRET=<client-secret>             # From Logto app settings
OIDC_AUTH_SECRET=<random-32-char-hex>          # Session encryption key
OIDC_SCOPES="openid profile email"             # Must match Logto app scopes
OIDC_REDIRECT_URI=https://your-app/callback    # Must match Logto redirect URI
```

## MCP Server

GearBox includes a built-in MCP server for integration with Claude Code and Claude Desktop. Enabled by default, disable with `GEARBOX_MCP=false`. Authenticated via API key or OAuth 2.1 Bearer token.

### Tools (19 total)

| Tool | Description |
|------|-------------|
| `list_items` | List all items, optionally filter by category |
| `get_item` | Get item details by ID |
| `create_item` | Add item to collection (for decided items; use `create_thread` for research) |
| `update_item` | Update item details |
| `delete_item` | Remove item from collection |
| `list_categories` | List all categories |
| `create_category` | Create a new category |
| `list_threads` | List research threads (recommended workflow for gear purchases) |
| `get_thread` | Get thread with candidates |
| `create_thread` | Start a research thread to compare gear options |
| `resolve_thread` | Pick winning candidate, adds it to collection |
| `add_candidate` | Add candidate to a research thread |
| `update_candidate` | Update candidate details |
| `remove_candidate` | Remove candidate from thread |
| `list_setups` | List gear setups |
| `get_setup` | Get setup with items and totals |
| `create_setup` | Create a new setup |
| `update_setup` | Update setup name or items |
| `upload_image_from_url` | Fetch image from URL, save locally |

### Resources

- `gearbox://collection/summary` — Overview of collection: totals, items per category, active threads.

### Configuration

**Claude Code** (`.claude/settings.json`):
```json
{
  "mcpServers": {
    "gearbox": {
      "type": "streamable-http",
      "url": "http://localhost:3000/mcp",
      "headers": {
        "X-API-Key": "<your-api-key>"
      }
    }
  }
}
```

**Claude Desktop** (`claude_desktop_config.json`):
```json
{
  "mcpServers": {
    "gearbox": {
      "type": "streamable-http",
      "url": "http://localhost:3000/mcp",
      "headers": {
        "X-API-Key": "<your-api-key>"
      }
    }
  }
}
```

Generate an API key from Settings > API Keys after logging in.

### OAuth Authentication (Claude Mobile / claude.ai)

GearBox supports OAuth 2.1 Authorization Code + PKCE for MCP connections from Claude mobile app and claude.ai. The OAuth flow is automatic — Claude handles discovery and token exchange.

**Required environment variable for production:**
```bash
GEARBOX_URL=https://your-gearbox-domain.com  # Used as OAuth issuer URL
```

**OAuth endpoints:**
- `GET /.well-known/oauth-authorization-server` — Discovery metadata
- `POST /oauth/register` — Dynamic Client Registration
- `GET/POST /oauth/authorize` — Authorization with login form
- `POST /oauth/token` — Token exchange and refresh

**Both auth methods work simultaneously:**
- **API key** (`X-API-Key` header) — Claude Code, scripts, programmatic access
- **OAuth Bearer token** (`Authorization: Bearer` header) — Claude mobile, claude.ai