diff --git a/frontend/.cta.json b/frontend/.cta.json index 9fab49b..6c3ed64 100644 --- a/frontend/.cta.json +++ b/frontend/.cta.json @@ -22,4 +22,4 @@ "t3env", "tanstack-query" ] -} \ No newline at end of file +} diff --git a/frontend/README.md b/frontend/README.md index 3b73340..b7dad78 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -32,7 +32,6 @@ If you prefer not to use Tailwind CSS: ## Linting & Formatting - This project uses [eslint](https://eslint.org/) and [prettier](https://prettier.io/) for linting and formatting. Eslint is configured using [tanstack/eslint-config](https://tanstack.com/config/latest/docs/eslint). The following scripts are available: ```bash @@ -41,7 +40,6 @@ bun --bun run format bun --bun run check ``` - ## Deploy with Nitro This project uses Nitro as a generic server adapter, so it can run on any Node-compatible host. @@ -55,7 +53,6 @@ The build output is a self-contained Node server. To deploy, push the `dist/` di For host-specific presets (Vercel, Netlify, Cloudflare, AWS Lambda, etc.) and tuning, see https://v3.nitro.build/deploy. - ## Shadcn Add components using the latest version of [Shadcn](https://ui.shadcn.com/). @@ -64,7 +61,6 @@ Add components using the latest version of [Shadcn](https://ui.shadcn.com/). pnpm dlx shadcn@latest add button ``` - ## T3Env - You can use T3Env to add type safety to your environment variables. @@ -74,16 +70,11 @@ pnpm dlx shadcn@latest add button ### Usage ```ts -import { env } from "#/env"; +import { env } from '#/env' -console.log(env.VITE_APP_TITLE); +console.log(env.VITE_APP_TITLE) ``` - - - - - ## Routing This project uses [TanStack Router](https://tanstack.com/router) with file-based routing. Routes are managed as files in `src/routes`. @@ -101,7 +92,7 @@ Now that you have two routes you can use a `Link` component to navigate between To use SPA (Single Page Application) navigation you will need to import the `Link` component from `@tanstack/react-router`. ```tsx -import { Link } from "@tanstack/react-router"; +import { Link } from '@tanstack/react-router' ``` Then anywhere in your JSX you can use it like so: @@ -169,11 +160,11 @@ const getServerTime = createServerFn({ // Use in a component function MyComponent() { const [time, setTime] = useState('') - + useEffect(() => { getServerTime().then(setTime) }, []) - + return
Server time: {time}
} ``` @@ -226,8 +217,6 @@ function PeopleComponent() { Loaders simplify your data fetching logic dramatically. Check out more information in the [Loader documentation](https://tanstack.com/router/latest/docs/framework/react/guide/data-loading#loader-parameters). - - # Learn More You can learn more about all of the offerings from TanStack in the [TanStack documentation](https://tanstack.com). diff --git a/frontend/bun.lock b/frontend/bun.lock index 1a6d2be..cf95977 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -5,6 +5,7 @@ "": { "name": "frontend", "dependencies": { + "@microsoft/fetch-event-source": "^2.0.1", "@t3-oss/env-core": "^0.13.10", "@tailwindcss/vite": "^4.1.18", "@tanstack/react-devtools": "latest", @@ -133,6 +134,8 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@microsoft/fetch-event-source": ["@microsoft/fetch-event-source@2.0.1", "", {}, "sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.0", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^2.0.0-alpha.3", "@emnapi/runtime": "^2.0.0-alpha.3" } }, "sha512-kDoONqMa+VnZ4vvvu/ZUurpJ4gkZU57e7g69qpNgWhYcZFPUHZM2CEMKm+cG6ufDVALbjMvfmMjFVqaK7uEMnA=="], "@oozcitak/dom": ["@oozcitak/dom@2.0.2", "", { "dependencies": { "@oozcitak/infra": "^2.0.2", "@oozcitak/url": "^3.0.0", "@oozcitak/util": "^10.0.0" } }, "sha512-GjpKhkSYC3Mj4+lfwEyI1dqnsKTgwGy48ytZEhm4A/xnH/8z9M3ZVXKr/YGQi3uCLs1AEBS+x5T2JPiueEDW8w=="], diff --git a/frontend/components.json b/frontend/components.json index cdcadc6..323cfb2 100644 --- a/frontend/components.json +++ b/frontend/components.json @@ -18,4 +18,4 @@ "hooks": "#/hooks" }, "iconLibrary": "lucide" -} \ No newline at end of file +} diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js index 3f272c0..ae58115 100644 --- a/frontend/eslint.config.js +++ b/frontend/eslint.config.js @@ -15,6 +15,6 @@ export default [ }, }, { - ignores: ['eslint.config.js', 'prettier.config.js'], + ignores: ['eslint.config.js', 'prettier.config.js', '.output/**'], }, ] diff --git a/frontend/package.json b/frontend/package.json index d600c48..a23c85c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -15,6 +15,7 @@ "check": "prettier --check ." }, "dependencies": { + "@microsoft/fetch-event-source": "^2.0.1", "@t3-oss/env-core": "^0.13.10", "@tailwindcss/vite": "^4.1.18", "@tanstack/react-devtools": "latest", @@ -62,4 +63,4 @@ "lightningcss" ] } -} \ No newline at end of file +} diff --git a/frontend/prettier.config.js b/frontend/prettier.config.js index aea1c48..f7279f7 100644 --- a/frontend/prettier.config.js +++ b/frontend/prettier.config.js @@ -4,7 +4,7 @@ const config = { semi: false, singleQuote: true, - trailingComma: "all", -}; + trailingComma: 'all', +} -export default config; +export default config diff --git a/frontend/specs/auth.md b/frontend/specs/auth.md index fed0099..377624c 100644 --- a/frontend/specs/auth.md +++ b/frontend/specs/auth.md @@ -15,11 +15,11 @@ security, cookie, and exception behavior is under `com.meet.server.common`. restated as a controller contract here. - Successful controller responses use `ApiResponse`: - | Field | Type | Meaning | - |---|---|---| - | `success` | `boolean` | Whether the operation succeeded. | - | `message` | `string` | Human-readable result message. | - | `data` | `Optional` | Response payload; logout uses an empty value. | + | Field | Type | Meaning | + | --------- | ------------- | --------------------------------------------- | + | `success` | `boolean` | Whether the operation succeeded. | + | `message` | `string` | Human-readable result message. | + | `data` | `Optional` | Response payload; logout uses an empty value. | - Validation and application errors are also returned through `ApiResponse` by `GlobalExceptionHandler`. - The refresh token is stored in an HTTP-only `refresh_token` cookie with `SameSite=Lax`, `Path=/`, and a seven-day @@ -35,27 +35,32 @@ security, cookie, and exception behavior is under `com.meet.server.common`. - Authentication/authorization: Public (`permitAll`). - Request body: `RegisterRequest`. -| Field | Type | Required | Constraints | -|---|---|---:|---| -| `fullName` | `string` | Yes | `@NotBlank`, maximum 100 characters | -| `username` | `string` | Yes | `@NotBlank`, maximum 50 characters | -| `email` | `string` | Yes | `@NotBlank`, `@Email`, maximum 255 characters | -| `password` | `string` | Yes | `@NotBlank`, 8–100 characters | +| Field | Type | Required | Constraints | +| ---------- | -------- | -------: | --------------------------------------------- | +| `fullName` | `string` | Yes | `@NotBlank`, maximum 100 characters | +| `username` | `string` | Yes | `@NotBlank`, maximum 50 characters | +| `email` | `string` | Yes | `@NotBlank`, `@Email`, maximum 255 characters | +| `password` | `string` | Yes | `@NotBlank`, 8–100 characters | Example: ```json -{"fullName":"Jane Doe","username":"jane_doe","email":"jane@example.com","password":"Str0ngPass!"} +{ + "fullName": "Jane Doe", + "username": "jane_doe", + "email": "jane@example.com", + "password": "Str0ngPass!" +} ``` Responses: -| Status | Body | -|---|---| -| `200 OK` | `ApiResponse` with message `Registration successful`; also sets `refresh_token`. | -| `400 Bad Request` | Validation error envelope containing field/object messages. | -| `409 Conflict` | `ApiResponse` with the duplicate-email or duplicate-username message. | -| `500 Internal Server Error` | Generic `ApiResponse` with message `An unexpected error occurred`. | +| Status | Body | +| --------------------------- | ---------------------------------------------------------------------------------------------- | +| `200 OK` | `ApiResponse` with message `Registration successful`; also sets `refresh_token`. | +| `400 Bad Request` | Validation error envelope containing field/object messages. | +| `409 Conflict` | `ApiResponse` with the duplicate-email or duplicate-username message. | +| `500 Internal Server Error` | Generic `ApiResponse` with message `An unexpected error occurred`. | Domain conflict codes are `EMAIL_ALREADY_EXISTS` and `USERNAME_ALREADY_EXISTS`. @@ -65,25 +70,25 @@ Domain conflict codes are `EMAIL_ALREADY_EXISTS` and `USERNAME_ALREADY_EXISTS`. - Authentication/authorization: Public (`permitAll`). - Request body: `LoginRequest`. -| Field | Type | Required | Constraints | -|---|---|---:|---| -| `email` | `string` | Yes | `@NotBlank`, `@Email` | -| `password` | `string` | Yes | `@NotBlank` | +| Field | Type | Required | Constraints | +| ---------- | -------- | -------: | --------------------- | +| `email` | `string` | Yes | `@NotBlank`, `@Email` | +| `password` | `string` | Yes | `@NotBlank` | Example: ```json -{"email":"jane@example.com","password":"Str0ngPass!"} +{ "email": "jane@example.com", "password": "Str0ngPass!" } ``` Responses: -| Status | Body | -|---|---| -| `200 OK` | `ApiResponse` with message `Login successful`; also sets `refresh_token`. | -| `400 Bad Request` | Validation error envelope containing field/object messages. | -| `401 Unauthorized` | `ApiResponse` with `Invalid email or password`. | -| `500 Internal Server Error` | Generic `ApiResponse`. | +| Status | Body | +| --------------------------- | --------------------------------------------------------------------------------------- | +| `200 OK` | `ApiResponse` with message `Login successful`; also sets `refresh_token`. | +| `400 Bad Request` | Validation error envelope containing field/object messages. | +| `401 Unauthorized` | `ApiResponse` with `Invalid email or password`. | +| `500 Internal Server Error` | Generic `ApiResponse`. | ### POST /api/auth/refresh @@ -91,20 +96,20 @@ Responses: - Authentication/authorization: Public (`permitAll`). - Request cookie: - | Name | Type | Required | Constraints | - |---|---|---:|---| - | `refresh_token` | opaque `string` | Yes | `@CookieValue(required=true)` | + | Name | Type | Required | Constraints | + | --------------- | --------------- | -------: | ----------------------------- | + | `refresh_token` | opaque `string` | Yes | `@CookieValue(required=true)` | - Request body: None. Responses: -| Status | Body | -|---|---| -| `200 OK` | `ApiResponse` with message `Token refreshed`; sets a rotated `refresh_token`. | -| `400 Bad Request` | `ApiResponse` with message `Malformed or incomplete request` when the required cookie is absent. | -| `401 Unauthorized` | `ApiResponse` containing an invalid, expired, revoked, required, or reused-token message. | -| `500 Internal Server Error` | Generic `ApiResponse`. | +| Status | Body | +| --------------------------- | ------------------------------------------------------------------------------------------------------ | +| `200 OK` | `ApiResponse` with message `Token refreshed`; sets a rotated `refresh_token`. | +| `400 Bad Request` | `ApiResponse` with message `Malformed or incomplete request` when the required cookie is absent. | +| `401 Unauthorized` | `ApiResponse` containing an invalid, expired, revoked, required, or reused-token message. | +| `500 Internal Server Error` | Generic `ApiResponse`. | Token messages defined by `RefreshTokenService` include `Refresh token is required`, `Invalid refresh token`, `Refresh token expired`, `Refresh token is revoked`, and `Refresh token reuse detected. All sessions invalidated.` @@ -115,21 +120,21 @@ Token messages defined by `RefreshTokenService` include `Refresh token is requir - Authentication/authorization: Public (`permitAll`). - Request parameters: - | Name | Location | Type | Required | Meaning | - |---|---|---|---:|---| - | `refresh_token` | Cookie | `string` | No | If present, revoke the token owner's sessions. | - | `authentication` | Security context | `Authentication` | No | Used when no refresh cookie exists; its name is parsed as a UUID. | + | Name | Location | Type | Required | Meaning | + | ---------------- | ---------------- | ---------------- | -------: | ----------------------------------------------------------------- | + | `refresh_token` | Cookie | `string` | No | If present, revoke the token owner's sessions. | + | `authentication` | Security context | `Authentication` | No | Used when no refresh cookie exists; its name is parsed as a UUID. | - Request body: None. Responses: -| Status | Body | -|---|---| -| `200 OK` | `ApiResponse` with message `Logout successful`, empty `data`, and a cleared `refresh_token` cookie. | -| `400 Bad Request` | Malformed/incomplete request envelope where applicable. | -| `401 Unauthorized` | Invalid refresh-token failure when revocation is attempted. | -| `500 Internal Server Error` | Generic `ApiResponse` for unhandled failures, including UUID parsing failures. | +| Status | Body | +| --------------------------- | --------------------------------------------------------------------------------------------------------- | +| `200 OK` | `ApiResponse` with message `Logout successful`, empty `data`, and a cleared `refresh_token` cookie. | +| `400 Bad Request` | Malformed/incomplete request envelope where applicable. | +| `401 Unauthorized` | Invalid refresh-token failure when revocation is attempted. | +| `500 Internal Server Error` | Generic `ApiResponse` for unhandled failures, including UUID parsing failures. | ### GET /api/auth/me @@ -140,12 +145,12 @@ Responses: Responses: -| Status | Body | -|---|---| -| `200 OK` | `ApiResponse` with message `Current user retrieved`. | -| `401 Unauthorized` | `ApiResponse` with `Unauthorized`, written by `UnauthorizedResponseHandler`. | -| `404 Not Found` | `ApiResponse` with `User not found`. | -| `500 Internal Server Error` | Generic `ApiResponse` for unhandled failures. | +| Status | Body | +| --------------------------- | ---------------------------------------------------------------------------------- | +| `200 OK` | `ApiResponse` with message `Current user retrieved`. | +| `401 Unauthorized` | `ApiResponse` with `Unauthorized`, written by `UnauthorizedResponseHandler`. | +| `404 Not Found` | `ApiResponse` with `User not found`. | +| `500 Internal Server Error` | Generic `ApiResponse` for unhandled failures. | ### GET /oauth2/authorization/{registrationId} @@ -153,9 +158,9 @@ Responses: - Authentication/authorization: Public through `/oauth2/**`. - Path parameter: - | Name | Type | Required | Values | - |---|---|---:|---| - | `registrationId` | `string` | Yes | Configured registrations: `google`, `github`. | + | Name | Type | Required | Values | + | ---------------- | -------- | -------: | --------------------------------------------- | + | `registrationId` | `string` | Yes | Configured registrations: `google`, `github`. | - Request body: None. - Response: Framework-generated redirect to the selected provider. Exact status and provider URL are not specified in @@ -189,21 +194,21 @@ No auth-feature WebSocket/STOMP handlers or listeners were found in source. ### AuthResponse -| Field | Type | Required | Nullable | Meaning | -|---|---|---:|---:|---| -| `accessToken` | `string` | Yes | Not specified | Generated JWT access token. | -| `user` | `UserResponse` | Yes | Not specified | Public user profile. | +| Field | Type | Required | Nullable | Meaning | +| ------------- | -------------- | -------: | ------------: | --------------------------- | +| `accessToken` | `string` | Yes | Not specified | Generated JWT access token. | +| `user` | `UserResponse` | Yes | Not specified | Public user profile. | ### UserResponse -| Field | Type | Required | Nullable | Meaning | -|---|---|---:|---:|---| -| `id` | `UUID` | Yes | Not specified | User identifier. | -| `fullName` | `string` | Yes | Not specified | Display name. | -| `username` | `string` | Yes | Not specified | Username. | -| `email` | `string` | Yes | Not specified | Email address. | -| `avatarUrl` | `string` | Yes | Not specified | Profile image URL; provider data may be absent. | -| `role` | `UserRole` | Yes | Not specified | `USER` or `ADMIN`. | +| Field | Type | Required | Nullable | Meaning | +| ----------- | ---------- | -------: | ------------: | ----------------------------------------------- | +| `id` | `UUID` | Yes | Not specified | User identifier. | +| `fullName` | `string` | Yes | Not specified | Display name. | +| `username` | `string` | Yes | Not specified | Username. | +| `email` | `string` | Yes | Not specified | Email address. | +| `avatarUrl` | `string` | Yes | Not specified | Profile image URL; provider data may be absent. | +| `role` | `UserRole` | Yes | Not specified | `USER` or `ADMIN`. | ### Error envelopes @@ -214,28 +219,28 @@ and `data` mapping field/property names to validation messages. `MethodArgumentN Custom application errors are returned as `ApiResponse` with `success=false`, the exception message, and empty `data`: -| Exception | Status | -|---|---| -| `AuthException` | Status carried by the exception, including `401`, `404`, and `409`. | -| `InvalidTokenException` | `401 Unauthorized`. | -| `ResponseStatusException` | Status carried by the exception. | -| Malformed request or missing required parameter | `400 Bad Request`. | -| Unhandled `Exception` | `500 Internal Server Error`. | +| Exception | Status | +| ----------------------------------------------- | ------------------------------------------------------------------- | +| `AuthException` | Status carried by the exception, including `401`, `404`, and `409`. | +| `InvalidTokenException` | `401 Unauthorized`. | +| `ResponseStatusException` | Status carried by the exception. | +| Malformed request or missing required parameter | `400 Bad Request`. | +| Unhandled `Exception` | `500 Internal Server Error`. | The `AuthException.errorCode` values are application metadata (`EMAIL_ALREADY_EXISTS`, `USERNAME_ALREADY_EXISTS`, `INVALID_CREDENTIALS`, `OAUTH_EMAIL_MISSING`, and `USER_NOT_FOUND`) but are not serialized by the current handler. ### Refresh-token cookie -| Property | Value | -|---|---| -| Name | `refresh_token` | -| HTTP-only | `true` | -| SameSite | `Lax` | -| Path | `/` | -| Secure | `true` except when `app.env=dev` | -| Max-Age when issued | `604800` seconds (7 days) | -| Max-Age when cleared | `0` | +| Property | Value | +| -------------------- | -------------------------------- | +| Name | `refresh_token` | +| HTTP-only | `true` | +| SameSite | `Lax` | +| Path | `/` | +| Secure | `true` except when `app.env=dev` | +| Max-Age when issued | `604800` seconds (7 days) | +| Max-Age when cleared | `0` | ## Source references diff --git a/frontend/specs/codebase.md b/frontend/specs/codebase.md new file mode 100644 index 0000000..bf6b017 --- /dev/null +++ b/frontend/specs/codebase.md @@ -0,0 +1,261 @@ +# Codebase API Specification + +## Overview + +The codebase feature manages authenticated repository imports, indexing, metadata updates, deletion, reindexing, and +code-aware chat over indexed repositories. The primary source modules are +`com.meet.server.feature.codebase` and `com.meet.server.feature.chat`. + +## Authentication and common conventions + +- All endpoints require authentication because `SecurityConfig` applies `.anyRequest().authenticated()`. +- The authenticated user ID is read from `Authentication.getName()` and parsed as a UUID. +- JSON endpoints use `ApiResponse` with `success`, `message`, and `data` fields. +- Validation and domain failures are mapped by `GlobalExceptionHandler` to the same response envelope. +- Codebase IDs are UUIDs. +- The list endpoint is not paginated in the current source. +- Chat uses `text/event-stream` SSE and does not use `ApiResponse` for the stream itself. + +## HTTP endpoints + +### POST /api/codebases + +- Purpose: Queue a repository import and indexing job. +- Authentication/authorization: Authenticated user required. The new codebase belongs to that user. +- Request headers/path/query parameters: + +| Name | Type | Required | Constraints | Description | +|---|---|---:|---|---| +| `Authorization` | string | Yes | Authentication is enforced by security configuration. | Authenticated principal supplies the user ID. | + +- Request body: `CodebaseImportRequest` + +| Field | Type | Required | Nullability | Constraints | Description | +|---|---|---:|---|---|---| +| `name` | string | Yes | Non-null | `@NotBlank` | Display name. | +| `cloneUrl` | string | Yes | Non-null | `@NotBlank`; HTTPS pattern; service requires HTTPS, a host, and no user info. | Repository clone URL. | +| `branch` | string | No | Nullable | Blank or null becomes `main`. | Branch to clone. | + +Example: + +```json +{ + "name": "Server Repo", + "cloneUrl": "https://github.com/example/server.git", + "branch": "main" +} +``` + +- Responses: + +| Status | Body | Example | +|---|---|---| +| `202 Accepted` | `ApiResponse` | `{"success":true,"message":"Codebase import queued","data":{"codebaseId":"3fa85f64-5717-4562-b3fc-2c963f66afa6","status":"QUEUED","fileCount":0}}` | +| `400 Bad Request` | Validation or clone URL error envelope | `{"success":false,"message":"Validation failed","data":{"cloneUrl":"cloneUrl must use HTTPS"}}` | +| `409 Conflict` | `ApiResponse` | `{"success":false,"message":"A user can have at most 5 codebases","data":null}` | + +- Errors: + - `CODEBASE_LIMIT_REACHED` (`409`) when the user already has five persisted codebases. All statuses count. + - `INVALID_CLONE_URL` (`400`) when service-level URI validation fails. + - Validation failures (`400`) for invalid request fields. + +### GET /api/codebases + +- Purpose: Return all codebases owned by the authenticated user. +- Authentication/authorization: Authenticated user required; results are filtered by the authenticated user ID. +- Request headers/path/query parameters: No path, query, or body parameters. The `Authorization` header is required by security configuration. +- Responses: + +| Status | Body | Example | +|---|---|---| +| `200 OK` | `ApiResponse>` | `{"success":true,"message":"Codebases retrieved","data":[{"codebaseId":"3fa85f64-5717-4562-b3fc-2c963f66afa6","name":"Server Repo","cloneUrl":"https://github.com/example/server.git","branch":"main","status":"INDEXED","lastCommitSha":"abc123","indexedAt":"2026-08-05T10:00:00Z","createdAt":"2026-08-05T09:00:00Z","updatedAt":"2026-08-05T10:00:00Z","fileCount":12}]}` | + +The repository query aggregates file counts in the list query; pagination is not specified in source. + +### PATCH /api/codebases/{codebaseId} + +- Purpose: Update codebase metadata without reindexing. +- Authentication/authorization: Authenticated user required; the codebase must belong to the authenticated user. +- Request headers/path/query parameters: + +| Name | Type | Required | Constraints | Description | +|---|---|---:|---|---| +| `Authorization` | string | Yes | Authentication is enforced by security configuration. | Authenticated principal. | +| `codebaseId` | UUID | Yes | Valid UUID path value. | Target codebase. | + +- Request body: `CodebaseUpdateRequest` + +| Field | Type | Required | Nullability | Constraints | Description | +|---|---|---:|---|---|---| +| `name` | string | Yes | Non-null | `@NotBlank` | New display name. | +| `branch` | string | Yes | Non-null | `@NotBlank` | New branch name. | + +`cloneUrl` cannot be updated by this endpoint. + +- Responses: + +| Status | Body | Example | +|---|---|---| +| `200 OK` | `ApiResponse` | `{"success":true,"message":"Codebase updated","data":{"codebaseId":"3fa85f64-5717-4562-b3fc-2c963f66afa6","name":"Updated Repo","cloneUrl":"https://github.com/example/server.git","branch":"develop","status":"INDEXED","lastCommitSha":"abc123","indexedAt":"2026-08-05T10:00:00Z","createdAt":"2026-08-05T09:00:00Z","updatedAt":"2026-08-05T10:05:00Z","fileCount":12}}` | +| `400 Bad Request` | Validation error envelope | `{"success":false,"message":"Validation failed","data":{"name":"must not be blank"}}` | +| `403 Forbidden` | `ApiResponse` | `{"success":false,"message":"You do not own this codebase","data":null}` | +| `404 Not Found` | `ApiResponse` | `{"success":false,"message":"Codebase not found","data":null}` | + +### DELETE /api/codebases/{codebaseId} + +- Purpose: Permanently delete the codebase and its indexed repository files and chunks. +- Authentication/authorization: Authenticated user required; the codebase must belong to the authenticated user. +- Request headers/path/query parameters: + +| Name | Type | Required | Constraints | Description | +|---|---|---:|---|---| +| `Authorization` | string | Yes | Authentication is enforced by security configuration. | Authenticated principal. | +| `codebaseId` | UUID | Yes | Valid UUID path value. | Target codebase. | + +- Request body: None. +- Responses: + +| Status | Body | Description | +|---|---|---| +| `204 No Content` | Empty body | Codebase and child indexed data were deleted. | +| `403 Forbidden` | `ApiResponse` | Authenticated user does not own the codebase. | +| `404 Not Found` | `ApiResponse` | Codebase does not exist. | +| `409 Conflict` | `ApiResponse` | Codebase is `QUEUED` or `PROCESSING`. | + +### POST /api/codebases/{codebaseId}/reindex + +- Purpose: Remove existing indexed data and queue a fresh clone/index operation. +- Authentication/authorization: Authenticated user required; the codebase must belong to the authenticated user. +- Request headers/path/query parameters: + +| Name | Type | Required | Constraints | Description | +|---|---|---:|---|---| +| `Authorization` | string | Yes | Authentication is enforced by security configuration. | Authenticated principal. | +| `codebaseId` | UUID | Yes | Valid UUID path value. | Target codebase. | + +- Request body: None. +- Responses: + +| Status | Body | Example | +|---|---|---| +| `202 Accepted` | `ApiResponse` | `{"success":true,"message":"Codebase reindex queued","data":{"codebaseId":"3fa85f64-5717-4562-b3fc-2c963f66afa6","status":"QUEUED","fileCount":0}}` | +| `403 Forbidden` | `ApiResponse` | `{"success":false,"message":"You do not own this codebase","data":null}` | +| `404 Not Found` | `ApiResponse` | `{"success":false,"message":"Codebase not found","data":null}` | +| `409 Conflict` | `ApiResponse` | `{"success":false,"message":"Codebase is currently being indexed","data":null}` | + +### POST /api/codebases/{codebaseId}/chat/stream + +- Purpose: Stream an AI response grounded in the selected codebase. +- Authentication/authorization: Authenticated user required; the requester must own the target codebase. +- Request headers/path/query parameters: + +| Name | Type | Required | Constraints | Description | +|---|---|---:|---|---| +| `Authorization` | string | Yes | Authentication is enforced by security configuration. | Authenticated principal. | +| `codebaseId` | UUID | Yes | Valid UUID path value. | Target codebase. | + +- Request body: `CodeChatRequest` + +| Field | Type | Required | Nullability | Constraints | Description | +|---|---|---:|---|---|---| +| `chatId` | string | No | Nullable | Blank or null becomes `default`. | Conversation identifier. | +| `message` | string | Yes | Non-null | `@NotBlank(message = "message is required")`; trimmed before use. | User prompt. | + +Example: + +```json +{ + "chatId": "architecture-1", + "message": "How does clone processing work?" +} +``` + +- Responses: + +| Status | Body | Description | +|---|---|---| +| `200 OK` | `text/event-stream` | Emits `message` events for response text, then `citations`, then `done`. | +| `400 Bad Request` | Validation error envelope | Request validation failed before streaming. | +| `403 Forbidden` | `ApiResponse` | Authenticated user does not own the codebase. | +| `404 Not Found` | `ApiResponse` | Codebase does not exist. | + +- Stream events: + - `message`: JSON string containing an assistant text fragment. + - `citations`: JSON array of `CodeCitation` values. + - `done`: JSON string containing the normalized chat ID. + - `error`: JSON object `{"message":"Unable to complete chat"}` when streaming fails. + +## WebSocket/message contracts + +No WebSocket or STOMP handlers are defined. Realtime delivery is provided through the SSE endpoint documented above. + +## Shared schemas + +### ApiResponse + +| Field | Type | Required | Nullability | Meaning | +|---|---|---:|---|---| +| `success` | boolean | Yes | Non-null | Whether the operation succeeded. | +| `message` | string | Yes | Non-null | Human-readable result or error message. | +| `data` | T | No | Nullable/empty | Operation payload. The Java source models this with `Optional`. | + +### CodebaseResponse + +| Field | Type | Required | Nullability | Meaning | +|---|---|---:|---|---| +| `codebaseId` | UUID | Yes | Non-null | Codebase identity. | +| `name` | string | Yes | Non-null | Display name. | +| `cloneUrl` | string | No | Nullable | Repository clone URL. | +| `branch` | string | No | Nullable | Indexed branch. | +| `status` | CodebaseStatus | Yes | Non-null | Current indexing state. | +| `lastCommitSha` | string | No | Nullable | Last resolved repository commit. | +| `indexedAt` | timestamp | No | Nullable | Time indexing reached `INDEXED`. | +| `createdAt` | timestamp | Yes | Non-null | Audit creation time. | +| `updatedAt` | timestamp | Yes | Non-null | Audit update time. | +| `fileCount` | integer | Yes | Non-null | Number of repository file rows associated with the codebase. | + +### CodebaseImportResponse + +| Field | Type | Required | Nullability | Meaning | +|---|---|---:|---|---| +| `codebaseId` | UUID | Yes | Non-null | Codebase identity. | +| `status` | CodebaseStatus | Yes | Non-null | Current state; queued responses use `QUEUED`. | +| `fileCount` | integer | Yes | Non-null | Processed file count; queued responses use `0`. | + +### CodebaseStatus + +Enum values: `INDEXED`, `QUEUED`, `PROCESSING`, `FAILED`. + +### CodeCitation + +| Field | Type | Required | Nullability | Meaning | +|---|---|---:|---|---| +| `chunkId` | UUID | Yes | Non-null | Referenced code chunk. | +| `path` | string | Yes | Non-null | Repository-relative path. | +| `startLine` | integer | Yes | Nullable | Citation start line when available. | +| `endLine` | integer | Yes | Nullable | Citation end line when available. | +| `language` | string | Yes | Nullable | Detected language when available. | +| `distance` | number | Yes | Non-null | Similarity distance. | + +### Domain and validation errors + +`CodebaseException` errors use the exception’s configured HTTP status and are returned as `ApiResponse` with +`success=false`, the exception message, and empty data. Validation failures use `ApiResponse>`. + +## Source references + +- `src/main/java/com/meet/server/feature/codebase/CodebaseController.java` — import, list, update, delete, and reindex routes. +- `src/main/java/com/meet/server/feature/chat/ChatController.java` — preserved SSE chat route. +- `src/main/java/com/meet/server/feature/codebase/CodebaseService.java` — `startClone`, `getUserCodebases`, `updateCodebase`, `reindexCodebase`, `deleteCodebase`, and indexing lifecycle. +- `src/main/java/com/meet/server/feature/chat/ChatService.java` — chat ownership and SSE event generation. +- `src/main/java/com/meet/server/feature/codebase/dto/CodebaseImportRequest.java` +- `src/main/java/com/meet/server/feature/codebase/dto/CodebaseImportResponse.java` +- `src/main/java/com/meet/server/feature/codebase/dto/CodebaseResponse.java` +- `src/main/java/com/meet/server/feature/codebase/dto/CodebaseUpdateRequest.java` +- `src/main/java/com/meet/server/feature/chat/dto/CodeChatRequest.java` +- `src/main/java/com/meet/server/feature/chat/dto/CodeCitation.java` +- `src/main/java/com/meet/server/feature/codebase/CodebaseStatus.java` +- `src/main/java/com/meet/server/common/api/ApiResponse.java` +- `src/main/java/com/meet/server/common/exception/GlobalExceptionHandler.java` +- `src/main/java/com/meet/server/common/exception/CodebaseException.java` +- `src/main/java/com/meet/server/common/security/config/SecurityConfig.java` diff --git a/frontend/src/components/ui/button.tsx b/frontend/src/components/ui/button.tsx index 189f9e6..80682fc 100644 --- a/frontend/src/components/ui/button.tsx +++ b/frontend/src/components/ui/button.tsx @@ -1,54 +1,55 @@ -import * as React from "react" -import { cva, type VariantProps } from "class-variance-authority" -import { Slot } from "radix-ui" +import * as React from 'react' +import { cva } from 'class-variance-authority' +import type {VariantProps} from 'class-variance-authority'; +import { Slot } from 'radix-ui' -import { cn } from "#/lib/utils.ts" +import { cn } from '#/lib/utils.ts' const buttonVariants = cva( "inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", { variants: { variant: { - default: "bg-primary text-primary-foreground hover:bg-primary/90", + default: 'bg-primary text-primary-foreground hover:bg-primary/90', destructive: - "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40", + 'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40', outline: - "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50", + 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50', secondary: - "bg-secondary text-secondary-foreground hover:bg-secondary/80", + 'bg-secondary text-secondary-foreground hover:bg-secondary/80', ghost: - "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", - link: "text-primary underline-offset-4 hover:underline", + 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50', + link: 'text-primary underline-offset-4 hover:underline', }, size: { - default: "h-9 px-4 py-2 has-[>svg]:px-3", + default: 'h-9 px-4 py-2 has-[>svg]:px-3', xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3", - sm: "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5", - lg: "h-10 rounded-md px-6 has-[>svg]:px-4", - icon: "size-9", - "icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3", - "icon-sm": "size-8", - "icon-lg": "size-10", + sm: 'h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5', + lg: 'h-10 rounded-md px-6 has-[>svg]:px-4', + icon: 'size-9', + 'icon-xs': "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3", + 'icon-sm': 'size-8', + 'icon-lg': 'size-10', }, }, defaultVariants: { - variant: "default", - size: "default", + variant: 'default', + size: 'default', }, - } + }, ) function Button({ className, - variant = "default", - size = "default", + variant = 'default', + size = 'default', asChild = false, ...props -}: React.ComponentProps<"button"> & +}: React.ComponentProps<'button'> & VariantProps & { asChild?: boolean }) { - const Comp = asChild ? Slot.Root : "button" + const Comp = asChild ? Slot.Root : 'button' return ( ) { +function Input({ className, type, ...props }: React.ComponentProps<'input'>) { return ( diff --git a/frontend/src/components/ui/label.tsx b/frontend/src/components/ui/label.tsx index 4fd57dd..4a6f4f0 100644 --- a/frontend/src/components/ui/label.tsx +++ b/frontend/src/components/ui/label.tsx @@ -1,9 +1,9 @@ -"use client" +'use client' -import * as React from "react" -import { Label as LabelPrimitive } from "radix-ui" +import * as React from 'react' +import { Label as LabelPrimitive } from 'radix-ui' -import { cn } from "#/lib/utils.ts" +import { cn } from '#/lib/utils.ts' function Label({ className, @@ -13,8 +13,8 @@ function Label({ diff --git a/frontend/src/components/ui/select.tsx b/frontend/src/components/ui/select.tsx index 3d88fc1..180eaef 100644 --- a/frontend/src/components/ui/select.tsx +++ b/frontend/src/components/ui/select.tsx @@ -1,8 +1,8 @@ -import * as React from "react" -import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react" -import { Select as SelectPrimitive } from "radix-ui" +import * as React from 'react' +import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from 'lucide-react' +import { Select as SelectPrimitive } from 'radix-ui' -import { cn } from "#/lib/utils.ts" +import { cn } from '#/lib/utils.ts' function Select({ ...props @@ -24,11 +24,11 @@ function SelectValue({ function SelectTrigger({ className, - size = "default", + size = 'default', children, ...props }: React.ComponentProps & { - size?: "sm" | "default" + size?: 'sm' | 'default' }) { return ( @@ -51,8 +51,8 @@ function SelectTrigger({ function SelectContent({ className, children, - position = "item-aligned", - align = "center", + position = 'item-aligned', + align = 'center', ...props }: React.ComponentProps) { return ( @@ -60,10 +60,10 @@ function SelectContent({ {children} @@ -92,7 +92,7 @@ function SelectLabel({ return ( ) @@ -108,7 +108,7 @@ function SelectItem({ data-slot="select-item" className={cn( "relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2", - className + className, )} {...props} > @@ -132,7 +132,7 @@ function SelectSeparator({ return ( ) @@ -146,8 +146,8 @@ function SelectScrollUpButton({ @@ -164,8 +164,8 @@ function SelectScrollDownButton({ diff --git a/frontend/src/components/ui/slider.tsx b/frontend/src/components/ui/slider.tsx index 568504c..139633a 100644 --- a/frontend/src/components/ui/slider.tsx +++ b/frontend/src/components/ui/slider.tsx @@ -1,9 +1,9 @@ -"use client" +'use client' -import * as React from "react" -import { Slider as SliderPrimitive } from "radix-ui" +import * as React from 'react' +import { Slider as SliderPrimitive } from 'radix-ui' -import { cn } from "#/lib/utils.ts" +import { cn } from '#/lib/utils.ts' function Slider({ className, @@ -20,7 +20,7 @@ function Slider({ : Array.isArray(defaultValue) ? defaultValue : [min, max], - [value, defaultValue, min, max] + [value, defaultValue, min, max], ) return ( @@ -31,21 +31,21 @@ function Slider({ min={min} max={max} className={cn( - "relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col", - className + 'relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col', + className, )} {...props} > diff --git a/frontend/src/components/ui/switch.tsx b/frontend/src/components/ui/switch.tsx index 99f399a..91598f5 100644 --- a/frontend/src/components/ui/switch.tsx +++ b/frontend/src/components/ui/switch.tsx @@ -1,29 +1,29 @@ -import * as React from "react" -import { Switch as SwitchPrimitive } from "radix-ui" +import * as React from 'react' +import { Switch as SwitchPrimitive } from 'radix-ui' -import { cn } from "#/lib/utils.ts" +import { cn } from '#/lib/utils.ts' function Switch({ className, - size = "default", + size = 'default', ...props }: React.ComponentProps & { - size?: "sm" | "default" + size?: 'sm' | 'default' }) { return ( diff --git a/frontend/src/components/ui/textarea.tsx b/frontend/src/components/ui/textarea.tsx index ffe9858..8564d12 100644 --- a/frontend/src/components/ui/textarea.tsx +++ b/frontend/src/components/ui/textarea.tsx @@ -1,14 +1,14 @@ -import * as React from "react" +import * as React from 'react' -import { cn } from "#/lib/utils.ts" +import { cn } from '#/lib/utils.ts' -function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { +function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) { return (