# CLAUDE.md

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

## Project Overview

Vaessl is an AI-powered integration bridge that accepts user text/image inputs, processes them through an LLM pipeline (via LiteLLM), and exports structured data to management systems (Homebox, WikiJS). The backend uses a provider pattern for extensibility. The frontend has a working connection management dashboard.

## Commands

### Backend (Spring Boot + Gradle, inside `backend/`)

```bash
./gradlew build           # compile and package
./gradlew test            # run all tests
./gradlew test --tests com.vaessl.app.connection.ConnectionServiceTest  # single test class
```

### Frontend (React + Vite, inside `frontend/`)

```bash
npm run dev       # start dev server
npm run build     # TypeScript check + Vite build
npm run lint      # ESLint
npm run test      # Vitest watch mode
npm run test:ui   # Vitest visual dashboard
```

## Environment

Copy `.env.local` (not committed) into `backend/` with:

- `DB_URL`, `DB_TEST_URL`, `DB_USERNAME`, `DB_PASSWORD` — PostgreSQL (test container on port 5434)
- `PG_DRIVER_CLASS_NAME` — PostgreSQL JDBC driver class
- `OPENAI_KEY`, `OPENAI_BASE_URL` — LiteLLM gateway (provider-agnostic, configured for gpt-4o-mini)
- `FRONTEND_LOCAL_URL`, `FRONTEND_PUBLIC_URL` — allowed CORS origins for the backend

Frontend (optional, defaults to `/api`):

- `VITE_API_URL` — backend base URL used by `api/client.ts`

## Architecture

### Backend (`backend/src/main/java/com/vaessl/app/`)

Package-by-feature layout. Server context path is `/api`. Main endpoints:

- `POST /api/login` — authenticates a service, stores connection ID in session
- `GET /api/connections/status` — lists connected services for the current session
- `DELETE /api/connections/{serviceType}` — removes a service from the session; invalidates the session if no connections remain
- `POST /api/search` — paged search against the requested service; returns 401 if no active session
- `POST /api/sync` — triggers a vector-store sync for the requested service; returns 401 if no active session. Not yet triggered on login or called from the frontend — manual/internal trigger only for now.

Eight packages:

**`shared/`** — cross-cutting types used by more than one feature package

- `ServiceType` (enum): identifies each integrated app (e.g. `HOMEBOX`); used across `connection/`, `search/`, `sync/`
- `ServiceProvider` (interface): base for `ConnectionProvider`, `SearchProvider`, `SyncProvider`; declares `getServiceType()`
- `ServiceItem`: normalized item shape (`id`, `title`, `description`, `extraData`) returned by both search and sync fetches
- `Endpoint` (enum): API path constants for all external service calls
- `SessionKeys`: builds session attribute names of the form `{SERVICE_TYPE}_CONNECTION_ID`

**`config/`**

- `CorsConfig`: env-driven allowed origins (`FRONTEND_LOCAL_URL`, `FRONTEND_PUBLIC_URL`); `allowCredentials(true)` is required for session cookies to work cross-origin
- `SessionConfig`: JDBC-backed Spring Session with a persistent cookie (`SameSite=Lax`, `HttpOnly`)

**`connection/`** — connecting to and persisting service credentials

- `ConnectionProvider` interface: extends `ServiceProvider`; each integrated app implements `login()` and credential checking
- `ConnectionIdentifiable` interface: `appUrl()`/`username()`/`serviceType()`; implemented by `SearchRequest` and `SyncRequest` so `HomeboxItemClient` can resolve the underlying connection regardless of which feature is calling it
- `ConnectionService`: auto-discovers providers via Spring injection, dispatches login by `ServiceType`
- `ConnectionController`: stores `{serviceType}_CONNECTION_ID` in `HttpSession` after login; reads session attributes to build status responses
- Entity (`ConnectionEntity`) uses **Single Table Inheritance** — one `connections` table with app-specific nullable columns
- `HomeboxConnectionProvider` / `HomeboxConnectionEntity`: Homebox-specific implementation

**`homebox/`** — shared Homebox API access

- `HomeboxItemClient`: fetches a page of items from the Homebox entities API and maps them to `ServiceItem`; used by both `HomeboxSearchProvider` (keyword search) and `HomeboxSyncProvider` (vector-store sync) so the fetch/mapping logic isn't duplicated

**`vector/`** — vectorization

- `EmbeddingService`: embeds `ServiceItem`s into pgvector's `VectorStore` and prunes entries that no longer exist upstream. Document ID is `{connectionId}:{itemId}`, so re-syncing upserts existing items instead of duplicating them. Deliberately stateless (only field is the injected `VectorStore`) since it's a singleton bean — the per-sync ID accumulator used for pruning is owned by the caller (`HomeboxSyncProvider`), not held as instance state
- `PgVectorStore` itself is auto-configured by `spring-ai-starter-vector-store-pgvector` from `application.yaml` (`spring.ai.vectorstore.pgvector.dimensions`) — no manual config class in this codebase

**`search/`** — querying connected services (keyword and AI)

- `SearchProvider` interface: extends `ServiceProvider`; each integrated app implements `getSearchResults()`
- `SearchService`: dispatches by `ServiceType` via a single provider map — **does not yet route on `SearchRequest.aiSearch`**; `HomeboxSearchProvider` and `HomeboxAiSearchProvider` both currently register for `ServiceType.HOMEBOX`, so only one wins the registration (known gap, pending AI search work)
- `SearchController`: guards with session check before delegating to `SearchService`
- `SearchRequest`: includes `aiSearch: boolean` (not yet consumed, see above) and implements `ConnectionIdentifiable`
- `PagedSearchResponse`: includes nullable `summary` field — populated only for AI search results; null for keyword search
- `HomeboxSearchProvider`: keyword search; delegates the remote fetch to `HomeboxItemClient`
- `HomeboxAiSearchProvider`: **stub only** — `getSearchResults` throws `UnsupportedOperationException`; the actual similarity search + summary generation is pending

**`sync/`** — indexing connected services into the vector store

- `SyncProvider` interface: extends `ServiceProvider`; each integrated app implements `syncVectorStore()`
- `SyncService`: dispatches by `ServiceType` via a provider map, same pattern as `SearchService`
- `SyncController`: session-gated `POST /sync`; not currently called by the frontend or triggered on login
- `SyncRequest`: implements `ConnectionIdentifiable`
- `HomeboxSyncProvider`: pages through the full Homebox catalog via `HomeboxItemClient`, embedding each page through `EmbeddingService.vectorizeData` and collecting every item ID seen along the way. Once the full page loop finishes, calls `EmbeddingService.deleteStaleVectorEntries` **once** with the complete ID set, removing any previously indexed item no longer present upstream. Order matters — pruning per page instead of once at the end would treat items on other pages as stale and delete them too.

**Pending work (not yet implemented):** `HomeboxAiSearchProvider`'s actual similarity search + summary generation; `SearchService` routing two provider maps by `aiSearch`; triggering `/sync` on login or from the frontend; a frontend sync button and AI search UI.

**`exception/`** — `GlobalExceptionHandler` via `@ControllerAdvice`

### Frontend (`frontend/src/`)

React 19 + TypeScript + SCSS, Vite 6 build. Package-by-feature under `components/`.

- `api/client.ts` — typed `apiFetch` wrapper; always sends `credentials: 'include'` for session cookies; base URL from `VITE_API_URL` (defaults to `/api`)
- `api/connections.ts` — connection-specific API calls (`getStatuses`, `login`, `logout`)
- `api/searches.ts` — search API call (`search`)
- `types/connection.ts` — `ServiceType`, `LoginRequest`, `AuthResponse`, `ConnectionStatus`
- `types/search.ts` — `SearchRequest`, `SearchResponse`, `PagedSearchResponse`
- `components/connections/` — `Dashboard`, `ConnectModal`, `ServiceCard` (connection management UI)
- `components/search/SearchModal` — search dialog; posts to `/api/search` and renders paged results
- `components/ui/` — shared UI primitives (`ActionButton`, `Modal`)

### Data & AI

- PostgreSQL + pgvector (semantic search via embeddings); also used as the Spring Session store (JDBC)
- LiteLLM as a unified AI proxy; Spring AI OpenAI starter wired to it — `OPENAI_BASE_URL` points to LiteLLM, not OpenAI directly, keeping the underlying model provider configurable
- `spring-ai-starter-vector-store-pgvector` provides `PgVectorStore`, auto-configured from `application.yaml` (no manual config class)
- Embedding dimensions must stay consistent with the configured LiteLLM embedding model — changing models requires re-syncing all indexed items
- Processing pipeline (Phase 2): stage in DB → LLM inference → refine via UI → export to target app

### Testing Strategy

Integration tests spin up a **mirrored PostgreSQL container** on port 5434 (same schema as production). WireMock mocks external HTTP APIs (Homebox, WikiJS). Do not mock the database in integration tests — the mirrored container strategy exists specifically to catch schema/migration divergence.

## Chat Operations

Don't make code suggestions and changes unless explicitly asked. Treat every prompt as a discussion of latest best practice coding approaches.
