This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/docs/ICD/projects.md
gobha 96a4f16bc5 Changeset 0.37.17 (#229)
Co-authored-by: gobha <jasafpro@gmail.com>
Co-committed-by: gobha <jasafpro@gmail.com>
2026-03-24 16:50:00 +00:00

337 lines
7.8 KiB
Markdown

# 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).
---