feat: add description field to boards and update related components for onboarding

This commit is contained in:
Abhimanyu Saharan
2026-02-11 18:19:29 +05:30
parent 25eb45bf54
commit c6417bcffb
15 changed files with 148 additions and 6 deletions

View File

@@ -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 "

View File

@@ -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,

View File

@@ -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

View File

@@ -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")

View File

@@ -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(

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -206,3 +206,4 @@ export * from "./updateAgentApiV1AgentsAgentIdPatchParams";
export * from "./userRead";
export * from "./userUpdate";
export * from "./validationError";
export * from "./validationErrorCtx";

View File

@@ -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;
}

View File

@@ -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 };

View File

@@ -65,6 +65,7 @@ export default function EditBoardPage() {
const [board, setBoard] = useState<BoardRead | null>(null);
const [name, setName] = useState<string | undefined>(undefined);
const [description, setDescription] = useState<string | undefined>(undefined);
const [gatewayId, setGatewayId] = useState<string | undefined>(undefined);
const [boardGroupId, setBoardGroupId] = useState<string | undefined>(
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() {
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-slate-900">
Description <span className="text-red-500">*</span>
</label>
<Textarea
value={resolvedDescription}
onChange={(event) => setDescription(event.target.value)}
placeholder="What context should the lead agent know?"
className="min-h-[120px]"
disabled={isLoading}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-slate-900">
Objective

View File

@@ -24,6 +24,7 @@ import { DashboardPageLayout } from "@/components/templates/DashboardPageLayout"
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import SearchableSelect from "@/components/ui/searchable-select";
import { Textarea } from "@/components/ui/textarea";
const slugify = (value: string) =>
value
@@ -39,6 +40,7 @@ export default function NewBoardPage() {
const { isAdmin } = useOrganizationMembership(isSignedIn);
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const [gatewayId, setGatewayId] = useState<string>("");
const [boardGroupId, setBoardGroupId] = useState<string>("none");
@@ -95,7 +97,9 @@ export default function NewBoardPage() {
const errorMessage =
error ?? gatewaysQuery.error?.message ?? groupsQuery.error?.message ?? null;
const isFormReady = Boolean(name.trim() && displayGatewayId);
const isFormReady = Boolean(
name.trim() && description.trim() && displayGatewayId,
);
const gatewayOptions = useMemo(
() =>
@@ -124,6 +128,11 @@ export default function NewBoardPage() {
setError("Select a gateway before creating a board.");
return;
}
const trimmedDescription = description.trim();
if (!trimmedDescription) {
setError("Board description is required.");
return;
}
setError(null);
@@ -131,6 +140,7 @@ export default function NewBoardPage() {
data: {
name: trimmedName,
slug: slugify(trimmedName),
description: trimmedDescription,
gateway_id: resolvedGatewayId,
board_group_id: boardGroupId === "none" ? null : boardGroupId,
},
@@ -208,6 +218,19 @@ export default function NewBoardPage() {
</p>
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-medium text-slate-900">
Description <span className="text-red-500">*</span>
</label>
<Textarea
value={description}
onChange={(event) => setDescription(event.target.value)}
placeholder="What context should the lead agent know before onboarding?"
className="min-h-[120px]"
disabled={isLoading}
/>
</div>
</div>
{gateways.length === 0 ? (

View File

@@ -37,6 +37,7 @@ const buildBoard = (overrides: Partial<BoardRead> = {}): BoardRead => ({
id: "board-1",
name: "Ops Board",
slug: "ops-board",
description: "Operations board context.",
organization_id: "org-1",
created_at: "2026-01-01T00:00:00Z",
updated_at: "2026-01-01T00:00:00Z",