Documents the homebox/, sync/, and vector/ packages as they actually exist now (the doc still described a since-renamed ai/ package and a VectorStoreConfig class that was never added - PgVectorStore is auto-configured from application.yaml). Also flags what's still pending rather than implying it's done: HomeboxAiSearchProvider is an unimplemented stub, SearchService doesn't route on aiSearch yet, and /sync isn't wired to login or the frontend.
9.2 KiB
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/)
./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/)
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 classOPENAI_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 byapi/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 sessionGET /api/connections/status— lists connected services for the current sessionDELETE /api/connections/{serviceType}— removes a service from the session; invalidates the session if no connections remainPOST /api/search— paged search against the requested service; returns 401 if no active sessionPOST /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 acrossconnection/,search/,sync/ServiceProvider(interface): base forConnectionProvider,SearchProvider,SyncProvider; declaresgetServiceType()ServiceItem: normalized item shape (id,title,description,extraData) returned by both search and sync fetchesEndpoint(enum): API path constants for all external service callsSessionKeys: 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-originSessionConfig: JDBC-backed Spring Session with a persistent cookie (SameSite=Lax,HttpOnly)
connection/ — connecting to and persisting service credentials
ConnectionProviderinterface: extendsServiceProvider; each integrated app implementslogin()and credential checkingConnectionIdentifiableinterface:appUrl()/username()/serviceType(); implemented bySearchRequestandSyncRequestsoHomeboxItemClientcan resolve the underlying connection regardless of which feature is calling itConnectionService: auto-discovers providers via Spring injection, dispatches login byServiceTypeConnectionController: stores{serviceType}_CONNECTION_IDinHttpSessionafter login; reads session attributes to build status responses- Entity (
ConnectionEntity) uses Single Table Inheritance — oneconnectionstable 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 toServiceItem; used by bothHomeboxSearchProvider(keyword search) andHomeboxSyncProvider(vector-store sync) so the fetch/mapping logic isn't duplicated
vector/ — vectorization
EmbeddingService: embedsServiceItems into pgvector'sVectorStoreand 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 injectedVectorStore) since it's a singleton bean — the per-sync ID accumulator used for pruning is owned by the caller (HomeboxSyncProvider), not held as instance statePgVectorStoreitself is auto-configured byspring-ai-starter-vector-store-pgvectorfromapplication.yaml(spring.ai.vectorstore.pgvector.dimensions) — no manual config class in this codebase
search/ — querying connected services (keyword and AI)
SearchProviderinterface: extendsServiceProvider; each integrated app implementsgetSearchResults()SearchService: dispatches byServiceTypevia a single provider map — does not yet route onSearchRequest.aiSearch;HomeboxSearchProviderandHomeboxAiSearchProviderboth currently register forServiceType.HOMEBOX, so only one wins the registration (known gap, pending AI search work)SearchController: guards with session check before delegating toSearchServiceSearchRequest: includesaiSearch: boolean(not yet consumed, see above) and implementsConnectionIdentifiablePagedSearchResponse: includes nullablesummaryfield — populated only for AI search results; null for keyword searchHomeboxSearchProvider: keyword search; delegates the remote fetch toHomeboxItemClientHomeboxAiSearchProvider: stub only —getSearchResultsthrowsUnsupportedOperationException; the actual similarity search + summary generation is pending
sync/ — indexing connected services into the vector store
SyncProviderinterface: extendsServiceProvider; each integrated app implementssyncVectorStore()SyncService: dispatches byServiceTypevia a provider map, same pattern asSearchServiceSyncController: session-gatedPOST /sync; not currently called by the frontend or triggered on loginSyncRequest: implementsConnectionIdentifiableHomeboxSyncProvider: pages through the full Homebox catalog viaHomeboxItemClient, embedding each page throughEmbeddingService.vectorizeDataand collecting every item ID seen along the way. Once the full page loop finishes, callsEmbeddingService.deleteStaleVectorEntriesonce 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— typedapiFetchwrapper; always sendscredentials: 'include'for session cookies; base URL fromVITE_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,ConnectionStatustypes/search.ts—SearchRequest,SearchResponse,PagedSearchResponsecomponents/connections/—Dashboard,ConnectModal,ServiceCard(connection management UI)components/search/SearchModal— search dialog; posts to/api/searchand renders paged resultscomponents/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_URLpoints to LiteLLM, not OpenAI directly, keeping the underlying model provider configurable spring-ai-starter-vector-store-pgvectorprovidesPgVectorStore, auto-configured fromapplication.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.