# Projects & Approvals — Implementation Reference ## Projects ### Database Schema **Migration 006 + 009** ```sql -- projects table CREATE TABLE projects ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), code VARCHAR(10) UNIQUE NOT NULL, name TEXT, description TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), created_by TEXT -- added in migration 009 ); -- many-to-many junction CREATE TABLE item_projects ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), item_id UUID NOT NULL REFERENCES items(id) ON DELETE CASCADE, project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), UNIQUE(item_id, project_id) ); ``` Items can belong to multiple projects. Project codes are immutable after creation. ### API Endpoints All gated by `RequireModule("projects")`. | Method | Path | Auth | Description | |--------|------|------|-------------| | GET | `/api/projects` | viewer | List all projects | | POST | `/api/projects` | editor | Create project (code: 2-10 alphanum) | | GET | `/api/projects/{code}` | viewer | Get single project | | PUT | `/api/projects/{code}` | editor | Update name/description | | DELETE | `/api/projects/{code}` | editor | Delete project (cascades associations) | | GET | `/api/projects/{code}/items` | viewer | List non-archived items in project | | GET | `/api/projects/{code}/sheet.ods` | viewer | Export ODS workbook (items + BOMs) | | GET | `/api/items/{pn}/projects` | viewer | Get projects for an item | | POST | `/api/items/{pn}/projects` | editor | Add item to projects (`{"projects":["A","B"]}`) | | DELETE | `/api/items/{pn}/projects/{code}` | editor | Remove item from project | ### Backend Files | File | Contents | |------|----------| | `internal/db/projects.go` | `ProjectRepository` — 13 methods: List, GetByCode, GetByID, Create, Update, Delete, AddItemToProject, AddItemToProjectByCode, RemoveItemFromProject, RemoveItemFromProjectByCode, GetProjectsForItem, GetProjectCodesForItem, GetItemsForProject, SetItemProjects | | `internal/api/handlers.go` | Handlers: HandleListProjects, HandleCreateProject, HandleGetProject, HandleUpdateProject, HandleDeleteProject, HandleGetProjectItems, HandleGetItemProjects, HandleAddItemProjects, HandleRemoveItemProject | | `internal/api/ods.go` | HandleProjectSheetODS — multi-sheet ODS export with items list + per-assembly BOM sheets | | `internal/api/routes.go` | Route registration under `/api/projects` and `/api/items/{pn}/projects` | ### Frontend **Page:** `web/src/pages/ProjectsPage.tsx` at route `/projects` Features: - Sortable table (code, name, description, item count, created date) - Create form with code validation (2-10 chars, auto-uppercase) - Inline edit (name/description only, code immutable) - Delete with confirmation dialog - Item count fetched per-project in parallel - Catppuccin Mocha theme, conditionally shown in sidebar when module enabled ### Config ```yaml modules: projects: enabled: true # default true ``` ### Not Yet Implemented - Project-level permissions / ownership (all projects visible to all viewers) - Project-based filtering in the main items list page - Bulk item operations across projects - Project archival / soft-delete - Project hierarchies or team assignment --- ## Approvals & ECO Workflows ### Database Schema **Migration 018 + 019** ```sql -- item_approvals table CREATE TABLE item_approvals ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), item_id UUID NOT NULL REFERENCES items(id) ON DELETE CASCADE, workflow_name TEXT, -- added in migration 019 eco_number TEXT, state TEXT NOT NULL DEFAULT 'draft', updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_by TEXT ); -- approval_signatures table CREATE TABLE approval_signatures ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), approval_id UUID NOT NULL REFERENCES item_approvals(id) ON DELETE CASCADE, username TEXT NOT NULL, role TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'pending', signed_at TIMESTAMPTZ, comment TEXT ); ``` ### State Machine **Approval states:** `draft` | `pending` | `approved` | `rejected` **Signature states:** `pending` | `approved` | `rejected` **Auto-transitions** (evaluated after each signature): 1. If any signature is `rejected` and workflow defines `any_reject` rule -> approval becomes `rejected` 2. If all signatures on required gates are `approved` and workflow defines `all_required_approve` rule -> approval becomes `approved` 3. Otherwise -> stays `pending` ### Workflow Definitions YAML files in `workflows/` directory, loaded at server startup. **`workflows/engineering-change.yaml`:** ```yaml workflow: name: engineering-change version: 1 description: "Standard engineering change order with peer review and manager approval" states: [draft, pending, approved, rejected] gates: - role: engineer label: "Peer Review" required: true - role: manager label: "Manager Approval" required: true - role: quality label: "Quality Sign-off" required: false rules: any_reject: rejected all_required_approve: approved ``` **`workflows/quick-review.yaml`:** ```yaml workflow: name: quick-review version: 1 description: "Single reviewer approval for minor changes" states: [draft, pending, approved, rejected] gates: - role: reviewer label: "Review" required: true rules: any_reject: rejected all_required_approve: approved ``` Custom workflows: add a `.yaml` file to the workflows directory with the same structure. ### API Endpoints Not module-gated (always available when server is running). | Method | Path | Auth | Description | |--------|------|------|-------------| | GET | `/api/workflows` | viewer | List all loaded workflow definitions | | GET | `/api/items/{pn}/approvals` | viewer | List approvals for item (with signatures) | | POST | `/api/items/{pn}/approvals` | editor | Create approval with signers | | POST | `/api/items/{pn}/approvals/{id}/sign` | editor | Record signature (approve/reject) | **Create approval request:** ```json { "workflow": "engineering-change", "eco_number": "ECO-2026-042", "signers": [ { "username": "alice", "role": "engineer" }, { "username": "bob", "role": "manager" }, { "username": "carol", "role": "quality" } ] } ``` Validation: workflow must exist, all signer roles must be defined in the workflow, all required gates must have at least one signer. **Sign request:** ```json { "status": "approved", "comment": "Looks good" } ``` Validation: approval must be in `pending` state, caller must be a listed signer, caller must not have already signed. ### SSE Events Published on the existing `GET /api/events` stream. | Event | Payload | Trigger | |-------|---------|---------| | `approval.created` | `{part_number, approval_id, workflow, eco_number}` | Approval created | | `approval.signed` | `{part_number, approval_id, username, status}` | Signature recorded | | `approval.completed` | `{part_number, approval_id, state}` | All required gates resolved | ### Backend Files | File | Contents | |------|----------| | `internal/db/item_approvals.go` | `ItemApprovalRepository` — Create, AddSignature, GetWithSignatures, ListByItemWithSignatures, UpdateState, GetSignatureForUser, UpdateSignature | | `internal/api/approval_handlers.go` | HandleGetApprovals, HandleCreateApproval, HandleSignApproval, HandleListWorkflows, evaluateApprovalState | | `internal/workflow/workflow.go` | Workflow/Gate/Rules types, Load, LoadAll, Validate, RequiredGates, HasRole | | `workflows/engineering-change.yaml` | 3-gate ECO workflow | | `workflows/quick-review.yaml` | 1-gate quick review workflow | ### Config ```yaml workflows: directory: /etc/silo/workflows # path to workflow YAML files ``` ### Frontend **No approval UI exists.** The backend API is fully functional but has no web interface. Issue #147 includes an approvals page scaffold as part of the .kc metadata web UI phase. ### Not Yet Implemented - Web UI for creating, viewing, and signing approvals - Approval history / audit log view - Lifecycle state transitions tied to approval outcomes (e.g. auto-release on approval) - Email or notification integration for pending signatures - Delegation / proxy signing - Approval templates (pre-filled signer lists per workflow) - Bulk approval operations