diff --git a/backend/app/api/board_onboarding.py b/backend/app/api/board_onboarding.py index 7f45b20f..5ef21e63 100644 --- a/backend/app/api/board_onboarding.py +++ b/backend/app/api/board_onboarding.py @@ -175,6 +175,7 @@ async def start_onboarding( prompt = ( "BOARD ONBOARDING REQUEST\n\n" f"Board Name: {board.name}\n" + f"Board Description: {board.description or '(not provided)'}\n" "You are the gateway agent. Ask the user 6-10 focused questions total:\n" "- 3-6 questions to clarify the board goal.\n" "- 1 question to choose a unique name for the board lead agent " diff --git a/backend/app/models/boards.py b/backend/app/models/boards.py index 43134260..8731128b 100644 --- a/backend/app/models/boards.py +++ b/backend/app/models/boards.py @@ -23,6 +23,7 @@ class Board(TenantScoped, table=True): organization_id: UUID = Field(foreign_key="organizations.id", index=True) name: str slug: str = Field(index=True) + description: str = Field(default="") gateway_id: UUID | None = Field(default=None, foreign_key="gateways.id", index=True) board_group_id: UUID | None = Field( default=None, diff --git a/backend/app/schemas/boards.py b/backend/app/schemas/boards.py index 7b4afa51..3727cd2a 100644 --- a/backend/app/schemas/boards.py +++ b/backend/app/schemas/boards.py @@ -11,6 +11,7 @@ from sqlmodel import SQLModel _ERR_GOAL_FIELDS_REQUIRED = "Confirmed goal boards require objective and success_metrics" _ERR_GATEWAY_REQUIRED = "gateway_id is required" +_ERR_DESCRIPTION_REQUIRED = "description is required" RUNTIME_ANNOTATION_TYPES = (datetime, UUID) @@ -19,6 +20,7 @@ class BoardBase(SQLModel): name: str slug: str + description: str gateway_id: UUID | None = None board_group_id: UUID | None = None board_type: str = "goal" @@ -37,6 +39,10 @@ class BoardCreate(BoardBase): @model_validator(mode="after") def validate_goal_fields(self) -> Self: """Require gateway and goal details when creating a confirmed goal board.""" + description = self.description.strip() + if not description: + raise ValueError(_ERR_DESCRIPTION_REQUIRED) + self.description = description if self.gateway_id is None: raise ValueError(_ERR_GATEWAY_REQUIRED) if ( @@ -53,6 +59,7 @@ class BoardUpdate(SQLModel): name: str | None = None slug: str | None = None + description: str | None = None gateway_id: UUID | None = None board_group_id: UUID | None = None board_type: str | None = None @@ -68,6 +75,13 @@ class BoardUpdate(SQLModel): # Treat explicit null like "unset" is invalid for patch updates. if "gateway_id" in self.model_fields_set and self.gateway_id is None: raise ValueError(_ERR_GATEWAY_REQUIRED) + if "description" in self.model_fields_set: + if self.description is None: + raise ValueError(_ERR_DESCRIPTION_REQUIRED) + description = self.description.strip() + if not description: + raise ValueError(_ERR_DESCRIPTION_REQUIRED) + self.description = description return self diff --git a/backend/migrations/versions/c3b58a391f2e_add_boards_description.py b/backend/migrations/versions/c3b58a391f2e_add_boards_description.py new file mode 100644 index 00000000..6c598da0 --- /dev/null +++ b/backend/migrations/versions/c3b58a391f2e_add_boards_description.py @@ -0,0 +1,37 @@ +"""Add description field to boards. + +Revision ID: c3b58a391f2e +Revises: b308f2876359 +Create Date: 2026-02-11 00:00:00.000000 + +""" + +from __future__ import annotations + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "c3b58a391f2e" +down_revision = "b308f2876359" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + """Add required board description column.""" + op.add_column( + "boards", + sa.Column( + "description", + sa.String(), + nullable=False, + server_default="", + ), + ) + op.alter_column("boards", "description", server_default=None) + + +def downgrade() -> None: + """Remove board description column.""" + op.drop_column("boards", "description") diff --git a/backend/tests/test_board_schema.py b/backend/tests/test_board_schema.py index d39c05f8..5c27781e 100644 --- a/backend/tests/test_board_schema.py +++ b/backend/tests/test_board_schema.py @@ -6,7 +6,7 @@ from uuid import uuid4 import pytest from app.schemas.board_onboarding import BoardOnboardingConfirm -from app.schemas.boards import BoardCreate +from app.schemas.boards import BoardCreate, BoardUpdate def test_goal_board_requires_objective_and_metrics_when_confirmed() -> None: @@ -18,6 +18,7 @@ def test_goal_board_requires_objective_and_metrics_when_confirmed() -> None: BoardCreate( name="Goal Board", slug="goal", + description="Ship onboarding improvements.", gateway_id=uuid4(), board_type="goal", goal_confirmed=True, @@ -26,6 +27,7 @@ def test_goal_board_requires_objective_and_metrics_when_confirmed() -> None: BoardCreate( name="Goal Board", slug="goal", + description="Ship onboarding improvements.", gateway_id=uuid4(), board_type="goal", goal_confirmed=True, @@ -36,7 +38,13 @@ def test_goal_board_requires_objective_and_metrics_when_confirmed() -> None: def test_goal_board_allows_missing_objective_before_confirmation() -> None: """Draft goal boards may omit objective/success_metrics before confirmation.""" - BoardCreate(name="Draft", slug="draft", gateway_id=uuid4(), board_type="goal") + BoardCreate( + name="Draft", + slug="draft", + description="Iterate on backlog hygiene.", + gateway_id=uuid4(), + board_type="goal", + ) def test_general_board_allows_missing_objective() -> None: @@ -44,11 +52,30 @@ def test_general_board_allows_missing_objective() -> None: BoardCreate( name="General", slug="general", + description="General coordination board.", gateway_id=uuid4(), board_type="general", ) +def test_board_create_requires_description() -> None: + """Board creation should reject empty descriptions.""" + with pytest.raises(ValueError, match="description is required"): + BoardCreate( + name="Goal Board", + slug="goal", + description=" ", + gateway_id=uuid4(), + board_type="goal", + ) + + +def test_board_update_rejects_empty_description_patch() -> None: + """Patch payloads should reject blank descriptions.""" + with pytest.raises(ValueError, match="description is required"): + BoardUpdate(description=" ") + + def test_onboarding_confirm_requires_goal_fields() -> None: """Onboarding confirm should enforce goal fields for goal board types.""" with pytest.raises( diff --git a/frontend/src/api/generated/model/boardCreate.ts b/frontend/src/api/generated/model/boardCreate.ts index effeacea..b4ac78ed 100644 --- a/frontend/src/api/generated/model/boardCreate.ts +++ b/frontend/src/api/generated/model/boardCreate.ts @@ -12,6 +12,7 @@ import type { BoardCreateSuccessMetrics } from "./boardCreateSuccessMetrics"; export interface BoardCreate { name: string; slug: string; + description: string; gateway_id?: string | null; board_group_id?: string | null; board_type?: string; diff --git a/frontend/src/api/generated/model/boardOnboardingAgentComplete.ts b/frontend/src/api/generated/model/boardOnboardingAgentComplete.ts index a5ac5b8a..ff354b4a 100644 --- a/frontend/src/api/generated/model/boardOnboardingAgentComplete.ts +++ b/frontend/src/api/generated/model/boardOnboardingAgentComplete.ts @@ -7,7 +7,6 @@ import type { BoardOnboardingAgentCompleteSuccessMetrics } from "./boardOnboardingAgentCompleteSuccessMetrics"; import type { BoardOnboardingLeadAgentDraft } from "./boardOnboardingLeadAgentDraft"; import type { BoardOnboardingUserProfile } from "./boardOnboardingUserProfile"; -import { BoardOnboardingAgentCompleteStatus } from "./boardOnboardingAgentCompleteStatus"; /** * Complete onboarding draft produced by the onboarding assistant. @@ -17,7 +16,7 @@ export interface BoardOnboardingAgentComplete { objective?: string | null; success_metrics?: BoardOnboardingAgentCompleteSuccessMetrics; target_date?: string | null; - status: BoardOnboardingAgentCompleteStatus; + status: "complete"; user_profile?: BoardOnboardingUserProfile | null; lead_agent?: BoardOnboardingLeadAgentDraft | null; } diff --git a/frontend/src/api/generated/model/boardRead.ts b/frontend/src/api/generated/model/boardRead.ts index 04f6029f..80514f09 100644 --- a/frontend/src/api/generated/model/boardRead.ts +++ b/frontend/src/api/generated/model/boardRead.ts @@ -12,6 +12,7 @@ import type { BoardReadSuccessMetrics } from "./boardReadSuccessMetrics"; export interface BoardRead { name: string; slug: string; + description: string; gateway_id?: string | null; board_group_id?: string | null; board_type?: string; diff --git a/frontend/src/api/generated/model/boardUpdate.ts b/frontend/src/api/generated/model/boardUpdate.ts index 07b3cb5d..4ecfffd8 100644 --- a/frontend/src/api/generated/model/boardUpdate.ts +++ b/frontend/src/api/generated/model/boardUpdate.ts @@ -12,6 +12,7 @@ import type { BoardUpdateSuccessMetrics } from "./boardUpdateSuccessMetrics"; export interface BoardUpdate { name?: string | null; slug?: string | null; + description?: string | null; gateway_id?: string | null; board_group_id?: string | null; board_type?: string | null; diff --git a/frontend/src/api/generated/model/index.ts b/frontend/src/api/generated/model/index.ts index 0f832836..0261ddaa 100644 --- a/frontend/src/api/generated/model/index.ts +++ b/frontend/src/api/generated/model/index.ts @@ -206,3 +206,4 @@ export * from "./updateAgentApiV1AgentsAgentIdPatchParams"; export * from "./userRead"; export * from "./userUpdate"; export * from "./validationError"; +export * from "./validationErrorCtx"; diff --git a/frontend/src/api/generated/model/validationError.ts b/frontend/src/api/generated/model/validationError.ts index 14d2b708..cfb105f7 100644 --- a/frontend/src/api/generated/model/validationError.ts +++ b/frontend/src/api/generated/model/validationError.ts @@ -4,9 +4,12 @@ * Mission Control API * OpenAPI spec version: 0.1.0 */ +import type { ValidationErrorCtx } from "./validationErrorCtx"; export interface ValidationError { loc: (string | number)[]; msg: string; type: string; + input?: unknown; + ctx?: ValidationErrorCtx; } diff --git a/frontend/src/api/generated/model/validationErrorCtx.ts b/frontend/src/api/generated/model/validationErrorCtx.ts new file mode 100644 index 00000000..43cb1c57 --- /dev/null +++ b/frontend/src/api/generated/model/validationErrorCtx.ts @@ -0,0 +1,8 @@ +/** + * Generated by orval v8.2.0 🍺 + * Do not edit manually. + * Mission Control API + * OpenAPI spec version: 0.1.0 + */ + +export type ValidationErrorCtx = { [key: string]: unknown }; diff --git a/frontend/src/app/boards/[boardId]/edit/page.tsx b/frontend/src/app/boards/[boardId]/edit/page.tsx index 4fbd2b17..6a5e561d 100644 --- a/frontend/src/app/boards/[boardId]/edit/page.tsx +++ b/frontend/src/app/boards/[boardId]/edit/page.tsx @@ -65,6 +65,7 @@ export default function EditBoardPage() { const [board, setBoard] = useState(null); const [name, setName] = useState(undefined); + const [description, setDescription] = useState(undefined); const [gatewayId, setGatewayId] = useState(undefined); const [boardGroupId, setBoardGroupId] = useState( undefined, @@ -182,6 +183,7 @@ export default function EditBoardPage() { const baseBoard = board ?? loadedBoard; const resolvedName = name ?? baseBoard?.name ?? ""; + const resolvedDescription = description ?? baseBoard?.description ?? ""; const resolvedGatewayId = gatewayId ?? baseBoard?.gateway_id ?? ""; const resolvedBoardGroupId = boardGroupId ?? baseBoard?.board_group_id ?? "none"; @@ -209,7 +211,9 @@ export default function EditBoardPage() { boardQuery.error?.message ?? null; - const isFormReady = Boolean(resolvedName.trim() && displayGatewayId); + const isFormReady = Boolean( + resolvedName.trim() && resolvedDescription.trim() && displayGatewayId, + ); const gatewayOptions = useMemo( () => @@ -231,6 +235,7 @@ export default function EditBoardPage() { const handleOnboardingConfirmed = (updated: BoardRead) => { setBoard(updated); + setDescription(updated.description ?? ""); setBoardType(updated.board_type ?? "goal"); setObjective(updated.objective ?? ""); setSuccessMetrics( @@ -256,6 +261,11 @@ export default function EditBoardPage() { setError("Select a gateway before saving."); return; } + const trimmedDescription = resolvedDescription.trim(); + if (!trimmedDescription) { + setError("Board description is required."); + return; + } setError(null); setMetricsError(null); @@ -276,6 +286,7 @@ export default function EditBoardPage() { const payload: BoardUpdate = { name: trimmedName, slug: slugify(trimmedName), + description: trimmedDescription, gateway_id: resolvedGatewayId || null, board_group_id: resolvedBoardGroupId === "none" ? null : resolvedBoardGroupId, @@ -410,6 +421,19 @@ export default function EditBoardPage() { +
+ +