# Projects

**Projects** are organizational containers that group channels,
knowledge bases, notes, and files. They follow the scope model
(personal, team, global).

All endpoints require authentication (`Authorization: Bearer <token>`).
Admin endpoints additionally require `role=admin`.

## Project Object

```json
{
  "id": "uuid",
  "name": "Q3 Research",
  "description": "...",
  "color": "#3b82f6",
  "icon": "folder",
  "scope": "personal|team|global",
  "owner_id": "uuid",
  "team_id": "uuid|null",
  "workspace_id": "uuid|null",
  "is_archived": false,
  "settings": {},
  "channel_count": 5,
  "kb_count": 2,
  "note_count": 3,
  "created_at": "2025-01-15T10:30:00Z",
  "updated_at": "2025-01-15T10:30:00Z"
}
```

| Field | Type | Notes |
|-------|------|-------|
| `id` | uuid | PK, auto-generated |
| `name` | string | Required, max 200 chars |
| `description` | string | Optional |
| `color` | string? | CSS hex color, max 7 chars |
| `icon` | string? | Icon identifier, max 50 chars |
| `scope` | enum | `personal`, `team`, `global` |
| `owner_id` | uuid | Set from JWT, immutable |
| `team_id` | uuid? | Set for team-scoped projects |
| `workspace_id` | uuid? | Linked workspace |
| `is_archived` | bool | Default `false` |
| `settings` | object | JSONB, default `{}` |
| `channel_count` | int | Computed, omitted when 0 |
| `kb_count` | int | Computed, omitted when 0 |
| `note_count` | int | Computed, omitted when 0 |
| `created_at` | datetime | Auto-set |
| `updated_at` | datetime | Auto-updated on change |

## Project CRUD

### List Projects

```
GET /projects                        → { "data": [...] }
```

Query params: `?include_archived=true` to include archived projects.

Returns projects the user can access: personal (own), team (member),
and global. Ordered by name.

### Create Project

```
POST /projects                       → 201, project object
```

```json
{
  "name": "My Project",
  "description": "Optional description",
  "color": "#3b82f6",
  "icon": "folder"
}
```

| Field | Required | Notes |
|-------|----------|-------|
| `name` | yes | Max 200 chars |
| `description` | no | |
| `color` | no | CSS hex |
| `icon` | no | Icon identifier |

Scope is set to `personal` and `owner_id` is taken from the JWT.
Returns the created project object (bare, no envelope).

### Get Project

```
GET /projects/:id                    → project object
```

Returns bare project object (no envelope). 404 if not found or
not accessible.

### Update Project

```
PUT /projects/:id                    → updated project object
```

Partial update — only supplied fields are changed.

```json
{
  "name": "New Name",
  "description": "Updated",
  "color": "#ef4444",
  "icon": "star",
  "is_archived": true,
  "workspace_id": "uuid|null",
  "settings": { "key": "value" }
}
```

Settings are merged (overlay, not replace). Returns the refreshed
project object after update.

### Delete Project

```
DELETE /projects/:id                 → { "message": "project deleted" }
```

Only the project owner or an admin can delete. Channels get
`project_id` set to NULL; junction rows cascade.

---

## Channel Association

```
GET    /projects/:id/channels                   → { "data": [...] }
POST   /projects/:id/channels                   ← { "channel_id", "position" }
DELETE /projects/:id/channels/:channelId         → { "message": "channel removed" }
PUT    /projects/:id/channels/reorder            ← { "channel_ids": ["uuid1", "uuid2"] }
```

A channel can only belong to one project. `POST` performs an atomic
move if the channel is already in a different project. The user must
own the channel being added.

**Channel association object:**

```json
{
  "project_id": "uuid",
  "channel_id": "uuid",
  "position": 0,
  "folder": "",
  "added_at": "2025-01-15T10:30:00Z"
}
```

**POST request:**

| Field | Required | Notes |
|-------|----------|-------|
| `channel_id` | yes | UUID of channel to add |
| `position` | no | Sort order (default 0) |

---

## KB Association

```
GET    /projects/:id/knowledge-bases             → { "data": [...] }
POST   /projects/:id/knowledge-bases             ← { "kb_id", "auto_search" }
DELETE /projects/:id/knowledge-bases/:kbId        → { "message": "KB removed" }
```

KBs bound to a project are automatically available to every channel
in that project (resolution chain injection at completion time).
Upsert on conflict — re-posting updates `auto_search`.

**KB association object:**

```json
{
  "project_id": "uuid",
  "kb_id": "uuid",
  "auto_search": true,
  "added_at": "2025-01-15T10:30:00Z",
  "name": "KB Name"
}
```

`name` is enriched via JOIN from `knowledge_bases.name`.

**POST request:**

| Field | Required | Notes |
|-------|----------|-------|
| `kb_id` | yes | UUID of knowledge base |
| `auto_search` | no | Default `false` |

---

## Note Association

```
GET    /projects/:id/notes                       → { "data": [...] }
POST   /projects/:id/notes                       ← { "note_id" }
DELETE /projects/:id/notes/:noteId                → { "message": "note removed" }
```

**Note association object:**

```json
{
  "project_id": "uuid",
  "note_id": "uuid",
  "added_at": "2025-01-15T10:30:00Z",
  "title": "Note Title"
}
```

`title` is enriched via JOIN from `notes.title`.
Insert uses ON CONFLICT DO NOTHING (idempotent).

---

## Project Files (v0.37.17 — workspace-backed)

Project files are stored in the project's workspace (auto-created on
first upload). The response uses `"files"` key, not `"data"`. Files
are `WorkspaceFile` objects with tree structure (directories + files).

### List Files

```
GET  /projects/:id/files                         → { "files": [...], "count": N }
     ?path=/docs&recursive=true
```

| Param | Default | Notes |
|-------|---------|-------|
| `path` | `""` | Directory to list (empty = root) |
| `recursive` | `true` | Include subdirectories |

### Upload File

```
POST /projects/:id/files                         ← multipart/form-data
     ?path=/docs
```

Multipart form field: `file`. Optional `path` query param to upload
into a subdirectory. Auto-creates the project workspace on first upload.
Returns `201` with `{ path, filename, content_type, size_bytes }`.

**Errors:** `413` if file exceeds size limit or workspace quota.

### Download File

```
GET  /projects/:id/files/download?path=/docs/readme.md
```

Streams raw file content with `Content-Disposition: attachment`.

### Delete File

```
DELETE /projects/:id/files?path=/docs/readme.md
       &recursive=false
```

`recursive=true` to delete directories with contents.

### Create Directory

```
POST /projects/:id/files/mkdir                   ← { "path": "/docs/images" }
```

Path also accepted via `?path=` query param. Returns `201`.

### Upload Archive

```
POST /projects/:id/archive/upload                ← multipart/form-data
```

Multipart field: `file`. Accepts `.zip`, `.tar.gz`, `.tgz`.
Extracts contents into the workspace root.
Returns `{ ok: true, files_extracted: N }`.

### Download Archive

```
GET  /projects/:id/archive/download?format=zip
```

Bundles all project files into a zip (or `tar.gz`).
Streams with `Content-Disposition: attachment`.

---

## Admin Project Management

```
GET    /admin/projects                           → { "data": [...] }
DELETE /admin/projects/:id                       → { "message": "project deleted" }
```

Query params: `?include_archived=true`.

Cross-instance visibility for platform admins. Admin list returns
an enriched object with extra fields:

```json
{
  "id": "uuid",
  "name": "...",
  "description": "...",
  "scope": "personal",
  "owner_id": "uuid",
  "team_id": null,
  "is_archived": false,
  "created_at": "...",
  "updated_at": "...",
  "channel_count": 5,
  "kb_count": 2,
  "note_count": 3,
  "owner_name": "jdoe"
}
```

Note: admin list object omits `color`, `icon`, `workspace_id`,
and `settings` (uses a local struct, not the full model).

---
