diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..ca640a4a --- /dev/null +++ b/Makefile @@ -0,0 +1,93 @@ +.DEFAULT_GOAL := help + +SHELL := /usr/bin/env bash +.SHELLFLAGS := -euo pipefail -c + +BACKEND_DIR := backend +FRONTEND_DIR := frontend + +.PHONY: help +help: ## Show available targets + @grep -E '^[a-zA-Z0-9_.-]+:.*?## ' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*## "}; {printf " %-26s %s\n", $$1, $$2}' + +.PHONY: setup +setup: backend-sync frontend-sync ## Install/sync backend + frontend deps + +.PHONY: backend-sync +backend-sync: ## uv sync backend deps (includes dev extra) + cd $(BACKEND_DIR) && uv sync --extra dev + +.PHONY: frontend-sync +frontend-sync: ## npm install frontend deps + cd $(FRONTEND_DIR) && npm install + +.PHONY: format +format: backend-format frontend-format ## Format backend + frontend + +.PHONY: backend-format +backend-format: ## Format backend (isort + black) + cd $(BACKEND_DIR) && uv run isort . + cd $(BACKEND_DIR) && uv run black . + +.PHONY: frontend-format +frontend-format: ## Format frontend (prettier) + cd $(FRONTEND_DIR) && npx prettier --write "src/**/*.{ts,tsx,js,jsx,json,css,md}" "*.{ts,js,json,md,mdx}" + +.PHONY: format-check +format-check: backend-format-check frontend-format-check ## Check formatting (no changes) + +.PHONY: backend-format-check +backend-format-check: ## Check backend formatting (isort + black) + cd $(BACKEND_DIR) && uv run isort . --check-only --diff + cd $(BACKEND_DIR) && uv run black . --check --diff + +.PHONY: frontend-format-check +frontend-format-check: ## Check frontend formatting (prettier) + cd $(FRONTEND_DIR) && npx prettier --check "src/**/*.{ts,tsx,js,jsx,json,css,md}" "*.{ts,js,json,md,mdx}" + +.PHONY: lint +lint: backend-lint frontend-lint ## Lint backend + frontend + +.PHONY: backend-lint +backend-lint: ## Lint backend (flake8) + cd $(BACKEND_DIR) && uv run flake8 --config .flake8 + +.PHONY: frontend-lint +frontend-lint: ## Lint frontend (eslint) + cd $(FRONTEND_DIR) && npm run lint + +.PHONY: typecheck +typecheck: backend-typecheck frontend-typecheck ## Typecheck backend + frontend + +.PHONY: backend-typecheck +backend-typecheck: ## Typecheck backend (mypy --strict) + cd $(BACKEND_DIR) && uv run mypy + +.PHONY: frontend-typecheck +frontend-typecheck: ## Typecheck frontend (tsc) + cd $(FRONTEND_DIR) && npx tsc -p tsconfig.json --noEmit + +.PHONY: test +test: backend-test ## Run tests + +.PHONY: backend-test +backend-test: ## Backend tests (pytest) + cd $(BACKEND_DIR) && uv run pytest + +.PHONY: backend-migrate +backend-migrate: ## Apply backend DB migrations (alembic upgrade head) + cd $(BACKEND_DIR) && uv run alembic upgrade head + +.PHONY: build +build: frontend-build ## Build artifacts + +.PHONY: frontend-build +frontend-build: ## Build frontend (next build) + cd $(FRONTEND_DIR) && npm run build + +.PHONY: api-gen +api-gen: ## Regenerate TS API client (requires backend running at 127.0.0.1:8000) + cd $(FRONTEND_DIR) && npm run api:gen + +.PHONY: check +check: lint typecheck test build ## Run lint + typecheck + tests + build diff --git a/backend/.gitignore b/backend/.gitignore index 1de69714..c76baa67 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -3,3 +3,7 @@ __pycache__/ .venv/ .env .runlogs/ + +# Generated on demand from uv.lock (single source of truth is pyproject.toml + uv.lock). +requirements.txt +requirements-dev.txt diff --git a/backend/alembic/versions/1d844b04ee06_add_approvals_task_id.py b/backend/alembic/versions/1d844b04ee06_add_approvals_task_id.py new file mode 100644 index 00000000..30954f39 --- /dev/null +++ b/backend/alembic/versions/1d844b04ee06_add_approvals_task_id.py @@ -0,0 +1,101 @@ +"""add approvals task_id + +Revision ID: 1d844b04ee06 +Revises: a5aab244d32d +Create Date: 2026-02-06 17:26:43.336466 + +""" + +from __future__ import annotations + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "1d844b04ee06" +down_revision = "a5aab244d32d" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # This migration may run in databases where the column/index/constraint were created via + # SQLModel `create_all()` (or a previous hotfix). Make it idempotent to avoid blocking + # upgrades in dev environments. + bind = op.get_bind() + inspector = sa.inspect(bind) + + approval_cols = {c["name"] for c in inspector.get_columns("approvals")} + if "task_id" not in approval_cols: + op.add_column("approvals", sa.Column("task_id", sa.Uuid(), nullable=True)) + + approval_index_names = {i["name"] for i in inspector.get_indexes("approvals")} + if "ix_approvals_task_id" not in approval_index_names: + op.create_index("ix_approvals_task_id", "approvals", ["task_id"], unique=False) + + # Backfill from legacy JSON payload keys when they contain a valid UUID. + op.execute( + """ + WITH src AS ( + SELECT + id, + COALESCE( + payload->>'task_id', + payload->>'taskId', + payload->>'taskID' + ) AS task_id_str + FROM approvals + WHERE task_id IS NULL + ), + valid AS ( + SELECT + id, + task_id_str::uuid AS task_id + FROM src + WHERE task_id_str ~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' + ), + existing AS ( + SELECT v.id, v.task_id + FROM valid AS v + JOIN tasks AS t ON t.id = v.task_id + ) + UPDATE approvals AS a + SET task_id = existing.task_id + FROM existing + WHERE a.id = existing.id; + """ + ) + + # Avoid FK failures if any approvals point at deleted tasks. + op.execute( + """ + UPDATE approvals AS a + SET task_id = NULL + WHERE task_id IS NOT NULL + AND NOT EXISTS ( + SELECT 1 FROM tasks AS t WHERE t.id = a.task_id + ); + """ + ) + + approval_fks = inspector.get_foreign_keys("approvals") + has_task_fk = any( + (fk.get("referred_table") == "tasks" and "task_id" in (fk.get("constrained_columns") or [])) + for fk in approval_fks + ) + if not has_task_fk: + op.create_foreign_key( + "fk_approvals_task_id_tasks", + "approvals", + "tasks", + ["task_id"], + ["id"], + ondelete="SET NULL", + ) + + +def downgrade() -> None: + op.drop_constraint("fk_approvals_task_id_tasks", "approvals", type_="foreignkey") + op.drop_index("ix_approvals_task_id", table_name="approvals") + op.drop_column("approvals", "task_id") diff --git a/backend/alembic/versions/a5aab244d32d_add_board_memory_is_chat.py b/backend/alembic/versions/a5aab244d32d_add_board_memory_is_chat.py new file mode 100644 index 00000000..da579577 --- /dev/null +++ b/backend/alembic/versions/a5aab244d32d_add_board_memory_is_chat.py @@ -0,0 +1,60 @@ +"""add board memory is_chat + +Revision ID: a5aab244d32d +Revises: 3b9b2f1a6c2d +Create Date: 2026-02-06 17:57:02.110572 + +""" + +from __future__ import annotations + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "a5aab244d32d" +down_revision = "3b9b2f1a6c2d" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # Idempotent: the column/indexes might already exist if the table was created via + # SQLModel `create_all()`. + bind = op.get_bind() + inspector = sa.inspect(bind) + + memory_cols = {c["name"] for c in inspector.get_columns("board_memory")} + if "is_chat" not in memory_cols: + op.add_column( + "board_memory", + sa.Column("is_chat", sa.Boolean(), server_default=sa.text("false"), nullable=False), + ) + + memory_index_names = {i["name"] for i in inspector.get_indexes("board_memory")} + if "ix_board_memory_is_chat" not in memory_index_names: + op.create_index("ix_board_memory_is_chat", "board_memory", ["is_chat"], unique=False) + if "ix_board_memory_board_id_is_chat_created_at" not in memory_index_names: + op.create_index( + "ix_board_memory_board_id_is_chat_created_at", + "board_memory", + ["board_id", "is_chat", "created_at"], + unique=False, + ) + + # Backfill from existing tags arrays. + op.execute( + """ + UPDATE board_memory + SET is_chat = TRUE + WHERE tags IS NOT NULL + AND tags::jsonb @> '["chat"]'::jsonb; + """ + ) + + +def downgrade() -> None: + op.drop_index("ix_board_memory_board_id_is_chat_created_at", table_name="board_memory") + op.drop_index("ix_board_memory_is_chat", table_name="board_memory") + op.drop_column("board_memory", "is_chat") diff --git a/backend/app/api/activity.py b/backend/app/api/activity.py index c61c15b0..168fe902 100644 --- a/backend/app/api/activity.py +++ b/backend/app/api/activity.py @@ -1,27 +1,27 @@ from __future__ import annotations -from fastapi import APIRouter, Depends, Query +from fastapi import APIRouter, Depends from sqlalchemy import desc from sqlmodel import col, select from sqlmodel.ext.asyncio.session import AsyncSession from app.api.deps import ActorContext, require_admin_or_agent +from app.db.pagination import paginate from app.db.session import get_session from app.models.activity_events import ActivityEvent from app.schemas.activity_events import ActivityEventRead +from app.schemas.pagination import DefaultLimitOffsetPage router = APIRouter(prefix="/activity", tags=["activity"]) -@router.get("", response_model=list[ActivityEventRead]) +@router.get("", response_model=DefaultLimitOffsetPage[ActivityEventRead]) async def list_activity( - limit: int = Query(50, ge=1, le=200), - offset: int = Query(0, ge=0), session: AsyncSession = Depends(get_session), actor: ActorContext = Depends(require_admin_or_agent), -) -> list[ActivityEvent]: +) -> DefaultLimitOffsetPage[ActivityEventRead]: statement = select(ActivityEvent) if actor.actor_type == "agent" and actor.agent: statement = statement.where(ActivityEvent.agent_id == actor.agent.id) - statement = statement.order_by(desc(col(ActivityEvent.created_at))).offset(offset).limit(limit) - return list(await session.exec(statement)) + statement = statement.order_by(desc(col(ActivityEvent.created_at))) + return await paginate(session, statement) diff --git a/backend/app/api/agent.py b/backend/app/api/agent.py index 3c556c1f..4c47f342 100644 --- a/backend/app/api/agent.py +++ b/backend/app/api/agent.py @@ -1,9 +1,11 @@ from __future__ import annotations +from collections.abc import Sequence +from typing import Any, cast from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, Query, status -from sqlmodel import select +from sqlmodel import col, select from sqlmodel.ext.asyncio.session import AsyncSession from app.api import agents as agents_api @@ -13,6 +15,7 @@ from app.api import board_onboarding as onboarding_api from app.api import tasks as tasks_api from app.api.deps import ActorContext, get_board_or_404, get_task_or_404 from app.core.agent_auth import AgentAuthContext, get_agent_auth_context +from app.db.pagination import paginate from app.db.session import get_session from app.integrations.openclaw_gateway import GatewayConfig as GatewayClientConfig from app.integrations.openclaw_gateway import OpenClawGatewayError, ensure_session, send_message @@ -30,6 +33,7 @@ from app.schemas.board_memory import BoardMemoryCreate, BoardMemoryRead from app.schemas.board_onboarding import BoardOnboardingAgentUpdate, BoardOnboardingRead from app.schemas.boards import BoardRead from app.schemas.common import OkResponse +from app.schemas.pagination import DefaultLimitOffsetPage from app.schemas.tasks import TaskCommentCreate, TaskCommentRead, TaskCreate, TaskRead, TaskUpdate from app.services.activity_log import record_activity @@ -54,15 +58,16 @@ async def _gateway_config(session: AsyncSession, board: Board) -> GatewayClientC return GatewayClientConfig(url=gateway.url, token=gateway.token) -@router.get("/boards", response_model=list[BoardRead]) +@router.get("/boards", response_model=DefaultLimitOffsetPage[BoardRead]) async def list_boards( session: AsyncSession = Depends(get_session), agent_ctx: AgentAuthContext = Depends(get_agent_auth_context), -) -> list[Board]: +) -> DefaultLimitOffsetPage[BoardRead]: + statement = select(Board) if agent_ctx.agent.board_id: - board = await session.get(Board, agent_ctx.agent.board_id) - return [board] if board else [] - return list(await session.exec(select(Board))) + statement = statement.where(col(Board.id) == agent_ctx.agent.board_id) + statement = statement.order_by(col(Board.created_at).desc()) + return await paginate(session, statement) @router.get("/boards/{board_id}", response_model=BoardRead) @@ -74,13 +79,12 @@ def get_board( return board -@router.get("/agents", response_model=list[AgentRead]) +@router.get("/agents", response_model=DefaultLimitOffsetPage[AgentRead]) async def list_agents( board_id: UUID | None = Query(default=None), - limit: int | None = Query(default=None, ge=1, le=200), session: AsyncSession = Depends(get_session), agent_ctx: AgentAuthContext = Depends(get_agent_auth_context), -) -> list[AgentRead]: +) -> DefaultLimitOffsetPage[AgentRead]: statement = select(Agent) if agent_ctx.agent.board_id: if board_id and board_id != agent_ctx.agent.board_id: @@ -88,32 +92,33 @@ async def list_agents( statement = statement.where(Agent.board_id == agent_ctx.agent.board_id) elif board_id: statement = statement.where(Agent.board_id == board_id) - if limit is not None: - statement = statement.limit(limit) - agents = list(await session.exec(statement)) main_session_keys = await agents_api._get_gateway_main_session_keys(session) - return [ - agents_api._to_agent_read(agents_api._with_computed_status(agent), main_session_keys) - for agent in agents - ] + statement = statement.order_by(col(Agent.created_at).desc()) + + def _transform(items: Sequence[Any]) -> Sequence[Any]: + agents = cast(Sequence[Agent], items) + return [ + agents_api._to_agent_read(agents_api._with_computed_status(agent), main_session_keys) + for agent in agents + ] + + return await paginate(session, statement, transformer=_transform) -@router.get("/boards/{board_id}/tasks", response_model=list[TaskRead]) +@router.get("/boards/{board_id}/tasks", response_model=DefaultLimitOffsetPage[TaskRead]) async def list_tasks( status_filter: str | None = Query(default=None, alias="status"), assigned_agent_id: UUID | None = None, unassigned: bool | None = None, - limit: int | None = Query(default=None, ge=1, le=200), board: Board = Depends(get_board_or_404), session: AsyncSession = Depends(get_session), agent_ctx: AgentAuthContext = Depends(get_agent_auth_context), -) -> list[Task]: +) -> DefaultLimitOffsetPage[TaskRead]: _guard_board_access(agent_ctx, board) return await tasks_api.list_tasks( status_filter=status_filter, assigned_agent_id=assigned_agent_id, unassigned=unassigned, - limit=limit, board=board, session=session, actor=_actor(agent_ctx), @@ -185,12 +190,15 @@ async def update_task( ) -@router.get("/boards/{board_id}/tasks/{task_id}/comments", response_model=list[TaskCommentRead]) +@router.get( + "/boards/{board_id}/tasks/{task_id}/comments", + response_model=DefaultLimitOffsetPage[TaskCommentRead], +) async def list_task_comments( task: Task = Depends(get_task_or_404), session: AsyncSession = Depends(get_session), agent_ctx: AgentAuthContext = Depends(get_agent_auth_context), -) -> list[ActivityEvent]: +) -> DefaultLimitOffsetPage[TaskCommentRead]: if agent_ctx.agent.board_id and task.board_id and agent_ctx.agent.board_id != task.board_id: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) return await tasks_api.list_task_comments( @@ -217,18 +225,14 @@ async def create_task_comment( ) -@router.get("/boards/{board_id}/memory", response_model=list[BoardMemoryRead]) +@router.get("/boards/{board_id}/memory", response_model=DefaultLimitOffsetPage[BoardMemoryRead]) async def list_board_memory( - limit: int = Query(default=50, ge=1, le=200), - offset: int = Query(default=0, ge=0), board: Board = Depends(get_board_or_404), session: AsyncSession = Depends(get_session), agent_ctx: AgentAuthContext = Depends(get_agent_auth_context), -) -> list[BoardMemory]: +) -> DefaultLimitOffsetPage[BoardMemoryRead]: _guard_board_access(agent_ctx, board) return await board_memory_api.list_board_memory( - limit=limit, - offset=offset, board=board, session=session, actor=_actor(agent_ctx), @@ -251,13 +255,16 @@ async def create_board_memory( ) -@router.get("/boards/{board_id}/approvals", response_model=list[ApprovalRead]) +@router.get( + "/boards/{board_id}/approvals", + response_model=DefaultLimitOffsetPage[ApprovalRead], +) async def list_approvals( status_filter: ApprovalStatus | None = Query(default=None, alias="status"), board: Board = Depends(get_board_or_404), session: AsyncSession = Depends(get_session), agent_ctx: AgentAuthContext = Depends(get_agent_auth_context), -) -> list[Approval]: +) -> DefaultLimitOffsetPage[ApprovalRead]: _guard_board_access(agent_ctx, board) return await approvals_api.list_approvals( status_filter=status_filter, diff --git a/backend/app/api/agents.py b/backend/app/api/agents.py index 61e32879..2c3f78ec 100644 --- a/backend/app/api/agents.py +++ b/backend/app/api/agents.py @@ -3,8 +3,9 @@ from __future__ import annotations import asyncio import json import re -from collections.abc import AsyncIterator +from collections.abc import AsyncIterator, Sequence from datetime import datetime, timedelta, timezone +from typing import Any, cast from uuid import UUID, uuid4 from fastapi import APIRouter, Depends, HTTPException, Query, Request, status @@ -17,6 +18,7 @@ from app.api.deps import ActorContext, require_admin_auth, require_admin_or_agen from app.core.agent_tokens import generate_agent_token, hash_agent_token from app.core.auth import AuthContext from app.core.time import utcnow +from app.db.pagination import paginate from app.db.session import async_session_maker, get_session from app.integrations.openclaw_gateway import GatewayConfig as GatewayClientConfig from app.integrations.openclaw_gateway import OpenClawGatewayError, ensure_session, send_message @@ -33,6 +35,7 @@ from app.schemas.agents import ( AgentRead, AgentUpdate, ) +from app.schemas.pagination import DefaultLimitOffsetPage from app.services.activity_log import record_activity from app.services.agent_provisioning import ( DEFAULT_HEARTBEAT_CONFIG, @@ -231,14 +234,28 @@ async def _send_wakeup_message( await send_message(message, session_key=session_key, config=config, deliver=True) -@router.get("", response_model=list[AgentRead]) +@router.get("", response_model=DefaultLimitOffsetPage[AgentRead]) async def list_agents( + board_id: UUID | None = Query(default=None), + gateway_id: UUID | None = Query(default=None), session: AsyncSession = Depends(get_session), auth: AuthContext = Depends(require_admin_auth), -) -> list[AgentRead]: - agents = list(await session.exec(select(Agent))) +) -> DefaultLimitOffsetPage[AgentRead]: main_session_keys = await _get_gateway_main_session_keys(session) - return [_to_agent_read(_with_computed_status(agent), main_session_keys) for agent in agents] + statement = select(Agent) + if board_id is not None: + statement = statement.where(col(Agent.board_id) == board_id) + if gateway_id is not None: + statement = statement.join(Board, col(Agent.board_id) == col(Board.id)).where( + col(Board.gateway_id) == gateway_id + ) + statement = statement.order_by(col(Agent.created_at).desc()) + + def _transform(items: Sequence[Any]) -> Sequence[Any]: + agents = cast(Sequence[Agent], items) + return [_to_agent_read(_with_computed_status(agent), main_session_keys) for agent in agents] + + return await paginate(session, statement, transformer=_transform) @router.get("/stream") diff --git a/backend/app/api/approvals.py b/backend/app/api/approvals.py index 600cf9c5..16fcf3a6 100644 --- a/backend/app/api/approvals.py +++ b/backend/app/api/approvals.py @@ -7,7 +7,7 @@ from datetime import datetime, timezone from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, Query, Request, status -from sqlalchemy import asc, or_ +from sqlalchemy import asc, case, func, or_ from sqlmodel import col, select from sqlmodel.ext.asyncio.session import AsyncSession from sse_starlette.sse import EventSourceResponse @@ -15,13 +15,32 @@ from sse_starlette.sse import EventSourceResponse from app.api.deps import ActorContext, get_board_or_404, require_admin_auth, require_admin_or_agent from app.core.auth import AuthContext from app.core.time import utcnow +from app.db.pagination import paginate from app.db.session import async_session_maker, get_session from app.models.approvals import Approval from app.models.boards import Board from app.schemas.approvals import ApprovalCreate, ApprovalRead, ApprovalStatus, ApprovalUpdate +from app.schemas.pagination import DefaultLimitOffsetPage router = APIRouter(prefix="/boards/{board_id}/approvals", tags=["approvals"]) +TASK_ID_KEYS: tuple[str, ...] = ("task_id", "taskId", "taskID") + + +def _extract_task_id(payload: dict[str, object] | None) -> UUID | None: + if not payload: + return None + for key in TASK_ID_KEYS: + value = payload.get(key) + if isinstance(value, UUID): + return value + if isinstance(value, str): + try: + return UUID(value) + except ValueError: + continue + return None + def _parse_since(value: str | None) -> datetime | None: if not value: @@ -66,13 +85,13 @@ async def _fetch_approval_events( return list(await session.exec(statement)) -@router.get("", response_model=list[ApprovalRead]) +@router.get("", response_model=DefaultLimitOffsetPage[ApprovalRead]) async def list_approvals( status_filter: ApprovalStatus | None = Query(default=None, alias="status"), board: Board = Depends(get_board_or_404), session: AsyncSession = Depends(get_session), actor: ActorContext = Depends(require_admin_or_agent), -) -> list[Approval]: +) -> DefaultLimitOffsetPage[ApprovalRead]: if actor.actor_type == "agent" and actor.agent: if actor.agent.board_id and actor.agent.board_id != board.id: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) @@ -80,7 +99,7 @@ async def list_approvals( if status_filter: statement = statement.where(col(Approval.status) == status_filter) statement = statement.order_by(col(Approval.created_at).desc()) - return list(await session.exec(statement)) + return await paginate(session, statement) @router.get("/stream") @@ -103,11 +122,53 @@ async def stream_approvals( break async with async_session_maker() as session: approvals = await _fetch_approval_events(session, board.id, last_seen) + pending_approvals_count = int( + ( + await session.exec( + select(func.count(col(Approval.id))) + .where(col(Approval.board_id) == board.id) + .where(col(Approval.status) == "pending") + ) + ).one() + ) + task_ids = {approval.task_id for approval in approvals if approval.task_id is not None} + counts_by_task_id: dict[UUID, tuple[int, int]] = {} + if task_ids: + rows = list( + await session.exec( + select( + col(Approval.task_id), + func.count(col(Approval.id)).label("total"), + func.sum( + case((col(Approval.status) == "pending", 1), else_=0) + ).label("pending"), + ) + .where(col(Approval.board_id) == board.id) + .where(col(Approval.task_id).in_(task_ids)) + .group_by(col(Approval.task_id)) + ) + ) + for task_id, total, pending in rows: + if task_id is None: + continue + counts_by_task_id[task_id] = (int(total or 0), int(pending or 0)) for approval in approvals: updated_at = _approval_updated_at(approval) if updated_at > last_seen: last_seen = updated_at - payload = {"approval": _serialize_approval(approval)} + payload: dict[str, object] = { + "approval": _serialize_approval(approval), + "pending_approvals_count": pending_approvals_count, + } + if approval.task_id is not None: + counts = counts_by_task_id.get(approval.task_id) + if counts is not None: + total, pending = counts + payload["task_counts"] = { + "task_id": str(approval.task_id), + "approvals_count": total, + "approvals_pending_count": pending, + } yield {"event": "approval", "data": json.dumps(payload)} await asyncio.sleep(2) @@ -124,8 +185,10 @@ async def create_approval( if actor.actor_type == "agent" and actor.agent: if actor.agent.board_id and actor.agent.board_id != board.id: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) + task_id = payload.task_id or _extract_task_id(payload.payload) approval = Approval( board_id=board.id, + task_id=task_id, agent_id=payload.agent_id, action_type=payload.action_type, payload=payload.payload, diff --git a/backend/app/api/board_memory.py b/backend/app/api/board_memory.py index e8ad5d68..fd7bc39e 100644 --- a/backend/app/api/board_memory.py +++ b/backend/app/api/board_memory.py @@ -8,6 +8,7 @@ from datetime import datetime, timezone from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, Query, Request, status +from sqlalchemy import func from sqlmodel import col, select from sqlmodel.ext.asyncio.session import AsyncSession from sse_starlette.sse import EventSourceResponse @@ -15,6 +16,7 @@ from sse_starlette.sse import EventSourceResponse from app.api.deps import ActorContext, get_board_or_404, require_admin_or_agent from app.core.config import settings from app.core.time import utcnow +from app.db.pagination import paginate from app.db.session import async_session_maker, get_session from app.integrations.openclaw_gateway import GatewayConfig as GatewayClientConfig from app.integrations.openclaw_gateway import OpenClawGatewayError, ensure_session, send_message @@ -23,6 +25,7 @@ from app.models.board_memory import BoardMemory from app.models.boards import Board from app.models.gateways import Gateway from app.schemas.board_memory import BoardMemoryCreate, BoardMemoryRead +from app.schemas.pagination import DefaultLimitOffsetPage router = APIRouter(prefix="/boards/{board_id}/memory", tags=["board-memory"]) @@ -90,11 +93,19 @@ async def _fetch_memory_events( session: AsyncSession, board_id: UUID, since: datetime, + is_chat: bool | None = None, ) -> list[BoardMemory]: statement = ( select(BoardMemory) .where(col(BoardMemory.board_id) == board_id) - .where(col(BoardMemory.created_at) >= since) + # Old/invalid rows (empty/whitespace-only content) can exist; exclude them to + # satisfy the NonEmptyStr response schema. + .where(func.length(func.trim(col(BoardMemory.content))) > 0) + ) + if is_chat is not None: + statement = statement.where(col(BoardMemory.is_chat) == is_chat) + statement = ( + statement.where(col(BoardMemory.created_at) >= since) .order_by(col(BoardMemory.created_at)) ) return list(await session.exec(statement)) @@ -159,25 +170,27 @@ async def _notify_chat_targets( continue -@router.get("", response_model=list[BoardMemoryRead]) +@router.get("", response_model=DefaultLimitOffsetPage[BoardMemoryRead]) async def list_board_memory( - limit: int = Query(default=50, ge=1, le=200), - offset: int = Query(default=0, ge=0), + is_chat: bool | None = Query(default=None), board: Board = Depends(get_board_or_404), session: AsyncSession = Depends(get_session), actor: ActorContext = Depends(require_admin_or_agent), -) -> list[BoardMemory]: +) -> DefaultLimitOffsetPage[BoardMemoryRead]: if actor.actor_type == "agent" and actor.agent: if actor.agent.board_id and actor.agent.board_id != board.id: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) statement = ( select(BoardMemory) .where(col(BoardMemory.board_id) == board.id) - .order_by(col(BoardMemory.created_at).desc()) - .offset(offset) - .limit(limit) + # Old/invalid rows (empty/whitespace-only content) can exist; exclude them to + # satisfy the NonEmptyStr response schema. + .where(func.length(func.trim(col(BoardMemory.content))) > 0) ) - return list(await session.exec(statement)) + if is_chat is not None: + statement = statement.where(col(BoardMemory.is_chat) == is_chat) + statement = statement.order_by(col(BoardMemory.created_at).desc()) + return await paginate(session, statement) @router.get("/stream") @@ -186,6 +199,7 @@ async def stream_board_memory( board: Board = Depends(get_board_or_404), actor: ActorContext = Depends(require_admin_or_agent), since: str | None = Query(default=None), + is_chat: bool | None = Query(default=None), ) -> EventSourceResponse: if actor.actor_type == "agent" and actor.agent: if actor.agent.board_id and actor.agent.board_id != board.id: @@ -199,7 +213,12 @@ async def stream_board_memory( if await request.is_disconnected(): break async with async_session_maker() as session: - memories = await _fetch_memory_events(session, board.id, last_seen) + memories = await _fetch_memory_events( + session, + board.id, + last_seen, + is_chat=is_chat, + ) for memory in memories: if memory.created_at > last_seen: last_seen = memory.created_at @@ -231,6 +250,7 @@ async def create_board_memory( board_id=board.id, content=payload.content, tags=payload.tags, + is_chat=is_chat, source=source, ) session.add(memory) diff --git a/backend/app/api/board_onboarding.py b/backend/app/api/board_onboarding.py index f43cec3d..6b7bb7cb 100644 --- a/backend/app/api/board_onboarding.py +++ b/backend/app/api/board_onboarding.py @@ -2,7 +2,6 @@ from __future__ import annotations import logging import re -from datetime import datetime from uuid import uuid4 from fastapi import APIRouter, Depends, HTTPException, status diff --git a/backend/app/api/boards.py b/backend/app/api/boards.py index 1ca67ad9..7a87a4b0 100644 --- a/backend/app/api/boards.py +++ b/backend/app/api/boards.py @@ -1,10 +1,11 @@ from __future__ import annotations import re -from uuid import uuid4 +from uuid import UUID, uuid4 -from fastapi import APIRouter, Depends, HTTPException, status +from fastapi import APIRouter, Depends, HTTPException, Query, status from sqlalchemy import delete +from sqlalchemy import func from sqlmodel import col, select from sqlmodel.ext.asyncio.session import AsyncSession @@ -12,6 +13,7 @@ from app.api.deps import ActorContext, get_board_or_404, require_admin_auth, req from app.core.auth import AuthContext from app.core.time import utcnow from app.db import crud +from app.db.pagination import paginate from app.db.session import get_session from app.integrations.openclaw_gateway import GatewayConfig as GatewayClientConfig from app.integrations.openclaw_gateway import ( @@ -31,6 +33,9 @@ from app.models.task_fingerprints import TaskFingerprint from app.models.tasks import Task from app.schemas.common import OkResponse from app.schemas.boards import BoardCreate, BoardRead, BoardUpdate +from app.schemas.pagination import DefaultLimitOffsetPage +from app.schemas.view_models import BoardSnapshot +from app.services.board_snapshot import build_board_snapshot router = APIRouter(prefix="/boards", tags=["boards"]) @@ -149,12 +154,17 @@ async def _cleanup_agent_on_gateway( ) -@router.get("", response_model=list[BoardRead]) +@router.get("", response_model=DefaultLimitOffsetPage[BoardRead]) async def list_boards( + gateway_id: UUID | None = Query(default=None), session: AsyncSession = Depends(get_session), actor: ActorContext = Depends(require_admin_or_agent), -) -> list[Board]: - return list(await session.exec(select(Board))) +) -> DefaultLimitOffsetPage[BoardRead]: + statement = select(Board) + if gateway_id is not None: + statement = statement.where(col(Board.gateway_id) == gateway_id) + statement = statement.order_by(func.lower(col(Board.name)).asc(), col(Board.created_at).desc()) + return await paginate(session, statement) @router.post("", response_model=BoardRead) @@ -175,6 +185,18 @@ def get_board( return board +@router.get("/{board_id}/snapshot", response_model=BoardSnapshot) +async def get_board_snapshot( + board: Board = Depends(get_board_or_404), + session: AsyncSession = Depends(get_session), + actor: ActorContext = Depends(require_admin_or_agent), +) -> BoardSnapshot: + if actor.actor_type == "agent" and actor.agent: + if actor.agent.board_id and actor.agent.board_id != board.id: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) + return await build_board_snapshot(session, board) + + @router.patch("/{board_id}", response_model=BoardRead) async def update_board( payload: BoardUpdate, diff --git a/backend/app/api/gateways.py b/backend/app/api/gateways.py index 2918b65c..b1c76f08 100644 --- a/backend/app/api/gateways.py +++ b/backend/app/api/gateways.py @@ -1,15 +1,15 @@ from __future__ import annotations -from datetime import datetime from uuid import UUID from fastapi import APIRouter, Depends, HTTPException, status -from sqlmodel import select +from sqlmodel import col, select from sqlmodel.ext.asyncio.session import AsyncSession from app.core.agent_tokens import generate_agent_token, hash_agent_token from app.core.auth import AuthContext, get_auth_context from app.core.time import utcnow +from app.db.pagination import paginate from app.db.session import get_session from app.integrations.openclaw_gateway import GatewayConfig as GatewayClientConfig from app.integrations.openclaw_gateway import OpenClawGatewayError, ensure_session, send_message @@ -17,6 +17,7 @@ from app.models.agents import Agent from app.models.gateways import Gateway from app.schemas.common import OkResponse from app.schemas.gateways import GatewayCreate, GatewayRead, GatewayUpdate +from app.schemas.pagination import DefaultLimitOffsetPage from app.services.agent_provisioning import DEFAULT_HEARTBEAT_CONFIG, provision_main_agent router = APIRouter(prefix="/gateways", tags=["gateways"]) @@ -362,12 +363,13 @@ async def _send_skyll_disable_message(gateway: Gateway) -> None: ) -@router.get("", response_model=list[GatewayRead]) +@router.get("", response_model=DefaultLimitOffsetPage[GatewayRead]) async def list_gateways( session: AsyncSession = Depends(get_session), auth: AuthContext = Depends(get_auth_context), -) -> list[Gateway]: - return list(await session.exec(select(Gateway))) +) -> DefaultLimitOffsetPage[GatewayRead]: + statement = select(Gateway).order_by(col(Gateway.created_at).desc()) + return await paginate(session, statement) @router.post("", response_model=GatewayRead) diff --git a/backend/app/api/tasks.py b/backend/app/api/tasks.py index 9a09a3b4..e728e075 100644 --- a/backend/app/api/tasks.py +++ b/backend/app/api/tasks.py @@ -25,16 +25,19 @@ from app.api.deps import ( ) from app.core.auth import AuthContext from app.core.time import utcnow +from app.db.pagination import paginate from app.db.session import async_session_maker, get_session from app.integrations.openclaw_gateway import GatewayConfig as GatewayClientConfig from app.integrations.openclaw_gateway import OpenClawGatewayError, ensure_session, send_message from app.models.activity_events import ActivityEvent from app.models.agents import Agent +from app.models.approvals import Approval from app.models.boards import Board from app.models.gateways import Gateway from app.models.task_fingerprints import TaskFingerprint from app.models.tasks import Task from app.schemas.common import OkResponse +from app.schemas.pagination import DefaultLimitOffsetPage from app.schemas.tasks import TaskCommentCreate, TaskCommentRead, TaskCreate, TaskRead, TaskUpdate from app.services.activity_log import record_activity @@ -410,16 +413,18 @@ async def stream_tasks( return EventSourceResponse(event_generator(), ping=15) -@router.get("", response_model=list[TaskRead]) +@router.get("", response_model=DefaultLimitOffsetPage[TaskRead]) async def list_tasks( status_filter: str | None = Query(default=None, alias="status"), assigned_agent_id: UUID | None = None, unassigned: bool | None = None, - limit: int | None = Query(default=None, ge=1, le=200), board: Board = Depends(get_board_or_404), session: AsyncSession = Depends(get_session), actor: ActorContext = Depends(require_admin_or_agent), -) -> list[Task]: +) -> DefaultLimitOffsetPage[TaskRead]: + if actor.actor_type == "agent" and actor.agent: + if actor.agent.board_id and actor.agent.board_id != board.id: + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) statement = select(Task).where(Task.board_id == board.id) if status_filter: statuses = [s.strip() for s in status_filter.split(",") if s.strip()] @@ -434,9 +439,8 @@ async def list_tasks( statement = statement.where(col(Task.assigned_agent_id) == assigned_agent_id) if unassigned: statement = statement.where(col(Task.assigned_agent_id).is_(None)) - if limit is not None: - statement = statement.limit(limit) - return list(await session.exec(statement)) + statement = statement.order_by(col(Task.created_at).desc()) + return await paginate(session, statement) @router.post("", response_model=TaskRead) @@ -661,17 +665,18 @@ async def delete_task( ) -> OkResponse: await session.execute(delete(ActivityEvent).where(col(ActivityEvent.task_id) == task.id)) await session.execute(delete(TaskFingerprint).where(col(TaskFingerprint.task_id) == task.id)) + await session.execute(delete(Approval).where(col(Approval.task_id) == task.id)) await session.delete(task) await session.commit() return OkResponse() -@router.get("/{task_id}/comments", response_model=list[TaskCommentRead]) +@router.get("/{task_id}/comments", response_model=DefaultLimitOffsetPage[TaskCommentRead]) async def list_task_comments( task: Task = Depends(get_task_or_404), session: AsyncSession = Depends(get_session), actor: ActorContext = Depends(require_admin_or_agent), -) -> list[ActivityEvent]: +) -> DefaultLimitOffsetPage[TaskCommentRead]: if actor.actor_type == "agent" and actor.agent: if actor.agent.board_id and task.board_id and actor.agent.board_id != task.board_id: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) @@ -681,7 +686,7 @@ async def list_task_comments( .where(col(ActivityEvent.event_type) == "task.comment") .order_by(asc(col(ActivityEvent.created_at))) ) - return list(await session.exec(statement)) + return await paginate(session, statement) @router.post("/{task_id}/comments", response_model=TaskCommentRead) diff --git a/backend/app/core/config.py b/backend/app/core/config.py index 75426ee6..02a40b2d 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -1,11 +1,20 @@ from __future__ import annotations +from pathlib import Path +from typing import Self + +from pydantic import model_validator from pydantic_settings import BaseSettings, SettingsConfigDict +BACKEND_ROOT = Path(__file__).resolve().parents[2] +DEFAULT_ENV_FILE = BACKEND_ROOT / ".env" + class Settings(BaseSettings): model_config = SettingsConfigDict( - env_file=".env", + # Load `backend/.env` regardless of current working directory. + # (Important when running uvicorn from repo root or via a process manager.) + env_file=[DEFAULT_ENV_FILE, ".env"], env_file_encoding="utf-8", extra="ignore", ) @@ -30,5 +39,13 @@ class Settings(BaseSettings): log_format: str = "text" log_use_utc: bool = False + @model_validator(mode="after") + def _defaults(self) -> Self: + # In dev, default to applying Alembic migrations at startup to avoid schema drift + # (e.g. missing newly-added columns). + if "db_auto_migrate" not in self.model_fields_set and self.environment == "dev": + self.db_auto_migrate = True + return self + settings = Settings() diff --git a/backend/app/core/time.py b/backend/app/core/time.py index 969fa853..a8533b29 100644 --- a/backend/app/core/time.py +++ b/backend/app/core/time.py @@ -8,4 +8,3 @@ def utcnow() -> datetime: # Keep naive UTC values for compatibility with existing DB schema/queries. return datetime.now(UTC).replace(tzinfo=None) - diff --git a/backend/app/db/pagination.py b/backend/app/db/pagination.py new file mode 100644 index 00000000..311fc477 --- /dev/null +++ b/backend/app/db/pagination.py @@ -0,0 +1,28 @@ +from __future__ import annotations + +from collections.abc import Awaitable, Callable, Sequence +from typing import Any, TypeVar, cast + +from fastapi_pagination.ext.sqlalchemy import paginate as _paginate +from sqlmodel.ext.asyncio.session import AsyncSession +from sqlmodel.sql.expression import Select, SelectOfScalar + +from app.schemas.pagination import DefaultLimitOffsetPage + +T = TypeVar("T") + +Transformer = Callable[[Sequence[Any]], Sequence[Any] | Awaitable[Sequence[Any]]] + + +async def paginate( + session: AsyncSession, + statement: Select[Any] | SelectOfScalar[Any], + *, + transformer: Transformer | None = None, +) -> DefaultLimitOffsetPage[T]: + # fastapi-pagination is not fully typed (it returns Any), but response_model validation + # ensures runtime correctness. Centralize casts here to keep strict mypy clean. + return cast( + DefaultLimitOffsetPage[T], + await _paginate(session, statement, transformer=transformer), + ) diff --git a/backend/app/main.py b/backend/app/main.py index 6e205a94..b785f43a 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -5,6 +5,7 @@ from contextlib import asynccontextmanager from fastapi import APIRouter, FastAPI from fastapi.middleware.cors import CORSMiddleware +from fastapi_pagination import add_pagination from app.api.activity import router as activity_router from app.api.agent import router as agent_router @@ -75,3 +76,5 @@ api_v1.include_router(approvals_router) api_v1.include_router(tasks_router) api_v1.include_router(users_router) app.include_router(api_v1) + +add_pagination(app) diff --git a/backend/app/models/approvals.py b/backend/app/models/approvals.py index 17fec2b5..8c762dc0 100644 --- a/backend/app/models/approvals.py +++ b/backend/app/models/approvals.py @@ -14,6 +14,7 @@ class Approval(SQLModel, table=True): id: UUID = Field(default_factory=uuid4, primary_key=True) board_id: UUID = Field(foreign_key="boards.id", index=True) + task_id: UUID | None = Field(default=None, foreign_key="tasks.id", index=True) agent_id: UUID | None = Field(default=None, foreign_key="agents.id", index=True) action_type: str payload: dict[str, object] | None = Field(default=None, sa_column=Column(JSON)) diff --git a/backend/app/models/board_memory.py b/backend/app/models/board_memory.py index 0df77269..296340e4 100644 --- a/backend/app/models/board_memory.py +++ b/backend/app/models/board_memory.py @@ -16,5 +16,6 @@ class BoardMemory(SQLModel, table=True): board_id: UUID = Field(foreign_key="boards.id", index=True) content: str tags: list[str] | None = Field(default=None, sa_column=Column(JSON)) + is_chat: bool = Field(default=False, index=True) source: str | None = None created_at: datetime = Field(default_factory=utcnow) diff --git a/backend/app/schemas/approvals.py b/backend/app/schemas/approvals.py index b32b2538..bb4df773 100644 --- a/backend/app/schemas/approvals.py +++ b/backend/app/schemas/approvals.py @@ -13,6 +13,7 @@ ApprovalStatus = Literal["pending", "approved", "rejected"] class ApprovalBase(SQLModel): action_type: str + task_id: UUID | None = None payload: dict[str, object] | None = None confidence: int rubric_scores: dict[str, int] | None = None diff --git a/backend/app/schemas/board_memory.py b/backend/app/schemas/board_memory.py index a02820e7..e1d66a8e 100644 --- a/backend/app/schemas/board_memory.py +++ b/backend/app/schemas/board_memory.py @@ -9,12 +9,18 @@ from app.schemas.common import NonEmptyStr class BoardMemoryCreate(SQLModel): + # For writes, reject blank/whitespace-only content. content: NonEmptyStr tags: list[str] | None = None source: str | None = None -class BoardMemoryRead(BoardMemoryCreate): +class BoardMemoryRead(SQLModel): id: UUID board_id: UUID + # For reads, allow legacy rows that may have empty content (avoid response validation 500s). + content: str + tags: list[str] | None = None + source: str | None = None + is_chat: bool = False created_at: datetime diff --git a/backend/app/schemas/pagination.py b/backend/app/schemas/pagination.py new file mode 100644 index 00000000..d1f8c644 --- /dev/null +++ b/backend/app/schemas/pagination.py @@ -0,0 +1,21 @@ +from __future__ import annotations + +from typing import TypeVar + +from fastapi import Query +from fastapi_pagination.customization import CustomizedPage, UseParamsFields +from fastapi_pagination.limit_offset import LimitOffsetPage + +T = TypeVar("T") + + +# Project-wide default pagination response model. +# - Keep `limit` / `offset` naming (matches existing API conventions). +# - Cap list endpoints to 200 items per request (matches prior route-level constraints). +DefaultLimitOffsetPage = CustomizedPage[ + LimitOffsetPage[T], + UseParamsFields( + limit=Query(200, ge=1, le=200), + offset=Query(0, ge=0), + ), +] diff --git a/backend/app/schemas/view_models.py b/backend/app/schemas/view_models.py new file mode 100644 index 00000000..b7fb7945 --- /dev/null +++ b/backend/app/schemas/view_models.py @@ -0,0 +1,24 @@ +from __future__ import annotations + +from sqlmodel import SQLModel + +from app.schemas.agents import AgentRead +from app.schemas.approvals import ApprovalRead +from app.schemas.board_memory import BoardMemoryRead +from app.schemas.boards import BoardRead +from app.schemas.tasks import TaskRead + + +class TaskCardRead(TaskRead): + assignee: str | None = None + approvals_count: int = 0 + approvals_pending_count: int = 0 + + +class BoardSnapshot(SQLModel): + board: BoardRead + tasks: list[TaskCardRead] + agents: list[AgentRead] + approvals: list[ApprovalRead] + chat_messages: list[BoardMemoryRead] + pending_approvals_count: int = 0 diff --git a/backend/app/services/board_snapshot.py b/backend/app/services/board_snapshot.py new file mode 100644 index 00000000..b091e36c --- /dev/null +++ b/backend/app/services/board_snapshot.py @@ -0,0 +1,158 @@ +from __future__ import annotations + +from datetime import timedelta +from uuid import UUID + +from sqlalchemy import case, func +from sqlmodel import col, select +from sqlmodel.ext.asyncio.session import AsyncSession + +from app.core.time import utcnow +from app.models.agents import Agent +from app.models.approvals import Approval +from app.models.board_memory import BoardMemory +from app.models.boards import Board +from app.models.gateways import Gateway +from app.models.tasks import Task +from app.schemas.agents import AgentRead +from app.schemas.approvals import ApprovalRead +from app.schemas.board_memory import BoardMemoryRead +from app.schemas.boards import BoardRead +from app.schemas.view_models import BoardSnapshot, TaskCardRead + +OFFLINE_AFTER = timedelta(minutes=10) + + +def _computed_agent_status(agent: Agent) -> str: + now = utcnow() + if agent.status in {"deleting", "updating"}: + return agent.status + if agent.last_seen_at is None: + return "provisioning" + if now - agent.last_seen_at > OFFLINE_AFTER: + return "offline" + return agent.status + + +async def _gateway_main_session_keys(session: AsyncSession) -> set[str]: + keys = (await session.exec(select(Gateway.main_session_key))).all() + return {key for key in keys if key} + + +def _agent_to_read(agent: Agent, main_session_keys: set[str]) -> AgentRead: + model = AgentRead.model_validate(agent, from_attributes=True) + computed_status = _computed_agent_status(agent) + is_gateway_main = bool(agent.openclaw_session_id and agent.openclaw_session_id in main_session_keys) + return model.model_copy(update={"status": computed_status, "is_gateway_main": is_gateway_main}) + + +def _memory_to_read(memory: BoardMemory) -> BoardMemoryRead: + return BoardMemoryRead.model_validate(memory, from_attributes=True) + + +def _approval_to_read(approval: Approval) -> ApprovalRead: + return ApprovalRead.model_validate(approval, from_attributes=True) + + +def _task_to_card( + task: Task, + *, + agent_name_by_id: dict[UUID, str], + counts_by_task_id: dict[UUID, tuple[int, int]], +) -> TaskCardRead: + card = TaskCardRead.model_validate(task, from_attributes=True) + approvals_count, approvals_pending_count = counts_by_task_id.get(task.id, (0, 0)) + assignee = ( + agent_name_by_id.get(task.assigned_agent_id) if task.assigned_agent_id is not None else None + ) + return card.model_copy( + update={ + "assignee": assignee, + "approvals_count": approvals_count, + "approvals_pending_count": approvals_pending_count, + } + ) + + +async def build_board_snapshot(session: AsyncSession, board: Board) -> BoardSnapshot: + board_read = BoardRead.model_validate(board, from_attributes=True) + + tasks = list( + await session.exec( + select(Task).where(col(Task.board_id) == board.id).order_by(col(Task.created_at).desc()) + ) + ) + + main_session_keys = await _gateway_main_session_keys(session) + agents = list( + await session.exec( + select(Agent).where(col(Agent.board_id) == board.id).order_by(col(Agent.created_at).desc()) + ) + ) + agent_reads = [_agent_to_read(agent, main_session_keys) for agent in agents] + agent_name_by_id = {agent.id: agent.name for agent in agents} + + pending_approvals_count = int( + (await session.exec( + select(func.count(col(Approval.id))) + .where(col(Approval.board_id) == board.id) + .where(col(Approval.status) == "pending") + )).one() + ) + + approvals = list( + await session.exec( + select(Approval) + .where(col(Approval.board_id) == board.id) + .order_by(col(Approval.created_at).desc()) + .limit(200) + ) + ) + approval_reads = [_approval_to_read(approval) for approval in approvals] + + counts_by_task_id: dict[UUID, tuple[int, int]] = {} + rows = list( + await session.exec( + select( + col(Approval.task_id), + func.count(col(Approval.id)).label("total"), + func.sum(case((col(Approval.status) == "pending", 1), else_=0)).label("pending"), + ) + .where(col(Approval.board_id) == board.id) + .where(col(Approval.task_id).is_not(None)) + .group_by(col(Approval.task_id)) + ) + ) + for task_id, total, pending in rows: + if task_id is None: + continue + counts_by_task_id[task_id] = (int(total or 0), int(pending or 0)) + + task_cards = [ + _task_to_card(task, agent_name_by_id=agent_name_by_id, counts_by_task_id=counts_by_task_id) + for task in tasks + ] + + chat_messages = list( + await session.exec( + select(BoardMemory) + .where(col(BoardMemory.board_id) == board.id) + .where(col(BoardMemory.is_chat).is_(True)) + # Old/invalid rows (empty/whitespace-only content) can exist; exclude them to + # satisfy the NonEmptyStr response schema. + .where(func.length(func.trim(col(BoardMemory.content))) > 0) + .order_by(col(BoardMemory.created_at).desc()) + .limit(200) + ) + ) + chat_messages.sort(key=lambda item: item.created_at) + chat_reads = [_memory_to_read(memory) for memory in chat_messages] + + return BoardSnapshot( + board=board_read, + tasks=task_cards, + agents=agent_reads, + approvals=approval_reads, + chat_messages=chat_reads, + pending_approvals_count=pending_approvals_count, + ) diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 35a5bf90..b18b206e 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -12,7 +12,7 @@ name = "openclaw-agency-backend" version = "0.1.0" requires-python = ">=3.12" dependencies = [ - "fastapi==0.115.4", + "fastapi==0.128.0", "uvicorn[standard]==0.30.6", "sqlmodel==0.0.22", "sqlalchemy==2.0.34", @@ -25,7 +25,8 @@ dependencies = [ "redis==5.1.1", "fastapi-clerk-auth==0.0.9", "sse-starlette==2.1.3", - "jinja2" + "jinja2==3.1.6", + "fastapi-pagination==0.15.9", ] [project.optional-dependencies] diff --git a/backend/requirements-dev.txt b/backend/requirements-dev.txt deleted file mode 100644 index c4afd042..00000000 --- a/backend/requirements-dev.txt +++ /dev/null @@ -1,4 +0,0 @@ -pytest==8.3.3 -pytest-asyncio==0.24.0 -ruff==0.6.9 -mypy==1.11.2 diff --git a/backend/requirements.txt b/backend/requirements.txt deleted file mode 100644 index bd9cf75e..00000000 --- a/backend/requirements.txt +++ /dev/null @@ -1,14 +0,0 @@ -fastapi==0.115.4 -uvicorn[standard]==0.30.6 -sqlmodel==0.0.22 -sqlalchemy==2.0.34 -alembic==1.13.2 -psycopg[binary]==3.2.1 -pydantic-settings==2.5.2 -python-dotenv==1.0.1 -websockets==12.0 -rq==1.16.2 -redis==5.1.1 -fastapi-clerk-auth==0.0.9 -sse-starlette==2.1.3 -jinja2==3.1.6 diff --git a/backend/uv.lock b/backend/uv.lock index a7813bd4..e2e11337 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -16,6 +16,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/df/ed/c884465c33c25451e4a5cd4acad154c29e5341e3214e220e7f3478aa4b0d/alembic-1.13.2-py3-none-any.whl", hash = "sha256:6b8733129a6224a9a711e17c99b08462dbf7cc9670ba8f2e2ae9af860ceb1953", size = 232990, upload-time = "2024-06-26T15:46:21.088Z" }, ] +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, +] + [[package]] name = "annotated-types" version = "0.7.0" @@ -177,16 +186,17 @@ wheels = [ [[package]] name = "fastapi" -version = "0.115.4" +version = "0.128.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "annotated-doc" }, { name = "pydantic" }, { name = "starlette" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a9/db/5781f19bd30745885e0737ff3fdd4e63e7bc691710f9da691128bb0dc73b/fastapi-0.115.4.tar.gz", hash = "sha256:db653475586b091cb8b2fec2ac54a680ac6a158e07406e1abae31679e8826349", size = 300737, upload-time = "2024-10-27T22:02:04.678Z" } +sdist = { url = "https://files.pythonhosted.org/packages/52/08/8c8508db6c7b9aae8f7175046af41baad690771c9bcde676419965e338c7/fastapi-0.128.0.tar.gz", hash = "sha256:1cc179e1cef10a6be60ffe429f79b829dce99d8de32d7acb7e6c8dfdf7f2645a", size = 365682, upload-time = "2025-12-27T15:21:13.714Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/99/f6/af0d1f58f86002be0cf1e2665cdd6f7a4a71cdc8a7a9438cdc9e3b5375fe/fastapi-0.115.4-py3-none-any.whl", hash = "sha256:0b504a063ffb3cf96a5e27dc1bc32c80ca743a2528574f9cdc77daa2d31b4742", size = 94732, upload-time = "2024-10-27T22:02:00.974Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5cbb59154b093548acd0f4c7c474a118eda06da25aa75c616b72d8fcd92a/fastapi-0.128.0-py3-none-any.whl", hash = "sha256:aebd93f9716ee3b4f4fcfe13ffb7cf308d99c9f3ab5622d8877441072561582d", size = 103094, upload-time = "2025-12-27T15:21:12.154Z" }, ] [[package]] @@ -203,6 +213,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/4e/058ecbe4fa0d470c3979f1272c0199cc47afb0ed935edb07b55441be8994/fastapi_clerk_auth-0.0.9-py3-none-any.whl", hash = "sha256:f9a47cfc65a2562c144a798ce0022a288799dac1149001b5a109865d578b2647", size = 6464, upload-time = "2025-11-11T06:12:35.655Z" }, ] +[[package]] +name = "fastapi-pagination" +version = "0.15.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastapi" }, + { name = "pydantic" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/48/4b/057fea634912ba285e71fa9b65594c9cc90f589ad25ccbdc7549202c12a2/fastapi_pagination-0.15.9.tar.gz", hash = "sha256:e24b0419a6077a75f38970ada2a57e277845fb177cc2da7300374ee8be32e8b5", size = 574790, upload-time = "2026-02-02T18:29:20.863Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/7e/7d1a66b618309ef3d1d969ccab187a8c09444e6eb8d3abcd3bc12f145578/fastapi_pagination-0.15.9-py3-none-any.whl", hash = "sha256:21f5ab465fb75e21a3454234603071711b679b1973ba2428e7a63db2221abc09", size = 60100, upload-time = "2026-02-02T18:29:19.363Z" }, +] + [[package]] name = "flake8" version = "7.1.1" @@ -226,7 +250,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f9/c8/9d76a66421d1ae24340dfae7e79c313957f6e3195c144d2c73333b5bfe34/greenlet-3.3.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:7e806ca53acf6d15a888405880766ec84721aa4181261cd11a457dfe9a7a4975", size = 276443, upload-time = "2026-01-23T15:30:10.066Z" }, { url = "https://files.pythonhosted.org/packages/81/99/401ff34bb3c032d1f10477d199724f5e5f6fbfb59816ad1455c79c1eb8e7/greenlet-3.3.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d842c94b9155f1c9b3058036c24ffb8ff78b428414a19792b2380be9cecf4f36", size = 597359, upload-time = "2026-01-23T16:00:57.394Z" }, { url = "https://files.pythonhosted.org/packages/2b/bc/4dcc0871ed557792d304f50be0f7487a14e017952ec689effe2180a6ff35/greenlet-3.3.1-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:20fedaadd422fa02695f82093f9a98bad3dab5fcda793c658b945fcde2ab27ba", size = 607805, upload-time = "2026-01-23T16:05:28.068Z" }, - { url = "https://files.pythonhosted.org/packages/3b/cd/7a7ca57588dac3389e97f7c9521cb6641fd8b6602faf1eaa4188384757df/greenlet-3.3.1-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c620051669fd04ac6b60ebc70478210119c56e2d5d5df848baec4312e260e4ca", size = 622363, upload-time = "2026-01-23T16:15:54.754Z" }, { url = "https://files.pythonhosted.org/packages/cf/05/821587cf19e2ce1f2b24945d890b164401e5085f9d09cbd969b0c193cd20/greenlet-3.3.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14194f5f4305800ff329cbf02c5fcc88f01886cadd29941b807668a45f0d2336", size = 609947, upload-time = "2026-01-23T15:32:51.004Z" }, { url = "https://files.pythonhosted.org/packages/a4/52/ee8c46ed9f8babaa93a19e577f26e3d28a519feac6350ed6f25f1afee7e9/greenlet-3.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7b2fe4150a0cf59f847a67db8c155ac36aed89080a6a639e9f16df5d6c6096f1", size = 1567487, upload-time = "2026-01-23T16:04:22.125Z" }, { url = "https://files.pythonhosted.org/packages/8f/7c/456a74f07029597626f3a6db71b273a3632aecb9afafeeca452cfa633197/greenlet-3.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:49f4ad195d45f4a66a0eb9c1ba4832bb380570d361912fa3554746830d332149", size = 1636087, upload-time = "2026-01-23T15:33:47.486Z" }, @@ -235,7 +258,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/ab/d26750f2b7242c2b90ea2ad71de70cfcd73a948a49513188a0fc0d6fc15a/greenlet-3.3.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:7ab327905cabb0622adca5971e488064e35115430cec2c35a50fd36e72a315b3", size = 275205, upload-time = "2026-01-23T15:30:24.556Z" }, { url = "https://files.pythonhosted.org/packages/10/d3/be7d19e8fad7c5a78eeefb2d896a08cd4643e1e90c605c4be3b46264998f/greenlet-3.3.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:65be2f026ca6a176f88fb935ee23c18333ccea97048076aef4db1ef5bc0713ac", size = 599284, upload-time = "2026-01-23T16:00:58.584Z" }, { url = "https://files.pythonhosted.org/packages/ae/21/fe703aaa056fdb0f17e5afd4b5c80195bbdab701208918938bd15b00d39b/greenlet-3.3.1-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7a3ae05b3d225b4155bda56b072ceb09d05e974bc74be6c3fc15463cf69f33fd", size = 610274, upload-time = "2026-01-23T16:05:29.312Z" }, - { url = "https://files.pythonhosted.org/packages/06/00/95df0b6a935103c0452dad2203f5be8377e551b8466a29650c4c5a5af6cc/greenlet-3.3.1-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:12184c61e5d64268a160226fb4818af4df02cfead8379d7f8b99a56c3a54ff3e", size = 624375, upload-time = "2026-01-23T16:15:55.915Z" }, { url = "https://files.pythonhosted.org/packages/cb/86/5c6ab23bb3c28c21ed6bebad006515cfe08b04613eb105ca0041fecca852/greenlet-3.3.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6423481193bbbe871313de5fd06a082f2649e7ce6e08015d2a76c1e9186ca5b3", size = 612904, upload-time = "2026-01-23T15:32:52.317Z" }, { url = "https://files.pythonhosted.org/packages/c2/f3/7949994264e22639e40718c2daf6f6df5169bf48fb038c008a489ec53a50/greenlet-3.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:33a956fe78bbbda82bfc95e128d61129b32d66bcf0a20a1f0c08aa4839ffa951", size = 1567316, upload-time = "2026-01-23T16:04:23.316Z" }, { url = "https://files.pythonhosted.org/packages/8d/6e/d73c94d13b6465e9f7cd6231c68abde838bb22408596c05d9059830b7872/greenlet-3.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b065d3284be43728dd280f6f9a13990b56470b81be20375a207cdc814a983f2", size = 1636549, upload-time = "2026-01-23T15:33:48.643Z" }, @@ -244,7 +266,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ae/fb/011c7c717213182caf78084a9bea51c8590b0afda98001f69d9f853a495b/greenlet-3.3.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:bd59acd8529b372775cd0fcbc5f420ae20681c5b045ce25bd453ed8455ab99b5", size = 275737, upload-time = "2026-01-23T15:32:16.889Z" }, { url = "https://files.pythonhosted.org/packages/41/2e/a3a417d620363fdbb08a48b1dd582956a46a61bf8fd27ee8164f9dfe87c2/greenlet-3.3.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b31c05dd84ef6871dd47120386aed35323c944d86c3d91a17c4b8d23df62f15b", size = 646422, upload-time = "2026-01-23T16:01:00.354Z" }, { url = "https://files.pythonhosted.org/packages/b4/09/c6c4a0db47defafd2d6bab8ddfe47ad19963b4e30f5bed84d75328059f8c/greenlet-3.3.1-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:02925a0bfffc41e542c70aa14c7eda3593e4d7e274bfcccca1827e6c0875902e", size = 658219, upload-time = "2026-01-23T16:05:30.956Z" }, - { url = "https://files.pythonhosted.org/packages/e2/89/b95f2ddcc5f3c2bc09c8ee8d77be312df7f9e7175703ab780f2014a0e781/greenlet-3.3.1-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3e0f3878ca3a3ff63ab4ea478585942b53df66ddde327b59ecb191b19dbbd62d", size = 671455, upload-time = "2026-01-23T16:15:57.232Z" }, { url = "https://files.pythonhosted.org/packages/80/38/9d42d60dffb04b45f03dbab9430898352dba277758640751dc5cc316c521/greenlet-3.3.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34a729e2e4e4ffe9ae2408d5ecaf12f944853f40ad724929b7585bca808a9d6f", size = 660237, upload-time = "2026-01-23T15:32:53.967Z" }, { url = "https://files.pythonhosted.org/packages/96/61/373c30b7197f9e756e4c81ae90a8d55dc3598c17673f91f4d31c3c689c3f/greenlet-3.3.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:aec9ab04e82918e623415947921dea15851b152b822661cce3f8e4393c3df683", size = 1615261, upload-time = "2026-01-23T16:04:25.066Z" }, { url = "https://files.pythonhosted.org/packages/fd/d3/ca534310343f5945316f9451e953dcd89b36fe7a19de652a1dc5a0eeef3f/greenlet-3.3.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:71c767cf281a80d02b6c1bdc41c9468e1f5a494fb11bc8688c360524e273d7b1", size = 1683719, upload-time = "2026-01-23T15:33:50.61Z" }, @@ -253,7 +274,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/28/24/cbbec49bacdcc9ec652a81d3efef7b59f326697e7edf6ed775a5e08e54c2/greenlet-3.3.1-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:3e63252943c921b90abb035ebe9de832c436401d9c45f262d80e2d06cc659242", size = 282706, upload-time = "2026-01-23T15:33:05.525Z" }, { url = "https://files.pythonhosted.org/packages/86/2e/4f2b9323c144c4fe8842a4e0d92121465485c3c2c5b9e9b30a52e80f523f/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:76e39058e68eb125de10c92524573924e827927df5d3891fbc97bd55764a8774", size = 651209, upload-time = "2026-01-23T16:01:01.517Z" }, { url = "https://files.pythonhosted.org/packages/d9/87/50ca60e515f5bb55a2fbc5f0c9b5b156de7d2fc51a0a69abc9d23914a237/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9f9d5e7a9310b7a2f416dd13d2e3fd8b42d803968ea580b7c0f322ccb389b97", size = 654300, upload-time = "2026-01-23T16:05:32.199Z" }, - { url = "https://files.pythonhosted.org/packages/7c/25/c51a63f3f463171e09cb586eb64db0861eb06667ab01a7968371a24c4f3b/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b9721549a95db96689458a1e0ae32412ca18776ed004463df3a9299c1b257ab", size = 662574, upload-time = "2026-01-23T16:15:58.364Z" }, { url = "https://files.pythonhosted.org/packages/1d/94/74310866dfa2b73dd08659a3d18762f83985ad3281901ba0ee9a815194fb/greenlet-3.3.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92497c78adf3ac703b57f1e3813c2d874f27f71a178f9ea5887855da413cd6d2", size = 653842, upload-time = "2026-01-23T15:32:55.671Z" }, { url = "https://files.pythonhosted.org/packages/97/43/8bf0ffa3d498eeee4c58c212a3905dd6146c01c8dc0b0a046481ca29b18c/greenlet-3.3.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ed6b402bc74d6557a705e197d47f9063733091ed6357b3de33619d8a8d93ac53", size = 1614917, upload-time = "2026-01-23T16:04:26.276Z" }, { url = "https://files.pythonhosted.org/packages/89/90/a3be7a5f378fc6e84abe4dcfb2ba32b07786861172e502388b4c90000d1b/greenlet-3.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:59913f1e5ada20fde795ba906916aea25d442abcc0593fba7e26c92b7ad76249", size = 1676092, upload-time = "2026-01-23T15:33:52.176Z" }, @@ -456,6 +476,7 @@ dependencies = [ { name = "alembic" }, { name = "fastapi" }, { name = "fastapi-clerk-auth" }, + { name = "fastapi-pagination" }, { name = "jinja2" }, { name = "psycopg", extra = ["binary"] }, { name = "pydantic-settings" }, @@ -484,11 +505,12 @@ dev = [ requires-dist = [ { name = "alembic", specifier = "==1.13.2" }, { name = "black", marker = "extra == 'dev'", specifier = "==24.10.0" }, - { name = "fastapi", specifier = "==0.115.4" }, + { name = "fastapi", specifier = "==0.128.0" }, { name = "fastapi-clerk-auth", specifier = "==0.0.9" }, + { name = "fastapi-pagination", specifier = "==0.15.9" }, { name = "flake8", marker = "extra == 'dev'", specifier = "==7.1.1" }, { name = "isort", marker = "extra == 'dev'", specifier = "==5.13.2" }, - { name = "jinja2" }, + { name = "jinja2", specifier = "==3.1.6" }, { name = "mypy", marker = "extra == 'dev'", specifier = "==1.11.2" }, { name = "psycopg", extras = ["binary"], specifier = "==3.2.1" }, { name = "pydantic-settings", specifier = "==2.5.2" }, diff --git a/frontend/src/api/generated/activity/activity.ts b/frontend/src/api/generated/activity/activity.ts index 11d36331..a568c0b7 100644 --- a/frontend/src/api/generated/activity/activity.ts +++ b/frontend/src/api/generated/activity/activity.ts @@ -18,8 +18,8 @@ import type { } from "@tanstack/react-query"; import type { - ActivityEventRead, HTTPValidationError, + LimitOffsetPageTypeVarCustomizedActivityEventRead, ListActivityApiV1ActivityGetParams, } from ".././model"; @@ -31,7 +31,7 @@ type SecondParameter unknown> = Parameters[1]; * @summary List Activity */ export type listActivityApiV1ActivityGetResponse200 = { - data: ActivityEventRead[]; + data: LimitOffsetPageTypeVarCustomizedActivityEventRead; status: 200; }; diff --git a/frontend/src/api/generated/agent/agent.ts b/frontend/src/api/generated/agent/agent.ts index 38b41761..404ac34a 100644 --- a/frontend/src/api/generated/agent/agent.ts +++ b/frontend/src/api/generated/agent/agent.ts @@ -34,9 +34,17 @@ import type { BoardOnboardingRead, BoardRead, HTTPValidationError, + LimitOffsetPageTypeVarCustomizedAgentRead, + LimitOffsetPageTypeVarCustomizedApprovalRead, + LimitOffsetPageTypeVarCustomizedBoardMemoryRead, + LimitOffsetPageTypeVarCustomizedBoardRead, + LimitOffsetPageTypeVarCustomizedTaskCommentRead, + LimitOffsetPageTypeVarCustomizedTaskRead, ListAgentsApiV1AgentAgentsGetParams, ListApprovalsApiV1AgentBoardsBoardIdApprovalsGetParams, ListBoardMemoryApiV1AgentBoardsBoardIdMemoryGetParams, + ListBoardsApiV1AgentBoardsGetParams, + ListTaskCommentsApiV1AgentBoardsBoardIdTasksTaskIdCommentsGetParams, ListTasksApiV1AgentBoardsBoardIdTasksGetParams, OkResponse, TaskCommentCreate, @@ -54,7 +62,7 @@ type SecondParameter unknown> = Parameters[1]; * @summary List Boards */ export type listBoardsApiV1AgentBoardsGetResponse200 = { - data: BoardRead[]; + data: LimitOffsetPageTypeVarCustomizedBoardRead; status: 200; }; @@ -76,15 +84,30 @@ export type listBoardsApiV1AgentBoardsGetResponse = | listBoardsApiV1AgentBoardsGetResponseSuccess | listBoardsApiV1AgentBoardsGetResponseError; -export const getListBoardsApiV1AgentBoardsGetUrl = () => { - return `/api/v1/agent/boards`; +export const getListBoardsApiV1AgentBoardsGetUrl = ( + params?: ListBoardsApiV1AgentBoardsGetParams, +) => { + const normalizedParams = new URLSearchParams(); + + Object.entries(params || {}).forEach(([key, value]) => { + if (value !== undefined) { + normalizedParams.append(key, value === null ? "null" : value.toString()); + } + }); + + const stringifiedParams = normalizedParams.toString(); + + return stringifiedParams.length > 0 + ? `/api/v1/agent/boards?${stringifiedParams}` + : `/api/v1/agent/boards`; }; export const listBoardsApiV1AgentBoardsGet = async ( + params?: ListBoardsApiV1AgentBoardsGetParams, options?: RequestInit, ): Promise => { return customFetch( - getListBoardsApiV1AgentBoardsGetUrl(), + getListBoardsApiV1AgentBoardsGetUrl(params), { ...options, method: "GET", @@ -92,32 +115,37 @@ export const listBoardsApiV1AgentBoardsGet = async ( ); }; -export const getListBoardsApiV1AgentBoardsGetQueryKey = () => { - return [`/api/v1/agent/boards`] as const; +export const getListBoardsApiV1AgentBoardsGetQueryKey = ( + params?: ListBoardsApiV1AgentBoardsGetParams, +) => { + return [`/api/v1/agent/boards`, ...(params ? [params] : [])] as const; }; export const getListBoardsApiV1AgentBoardsGetQueryOptions = < TData = Awaited>, TError = HTTPValidationError, ->(options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; -}) => { +>( + params?: ListBoardsApiV1AgentBoardsGetParams, + options?: { + query?: Partial< + UseQueryOptions< + Awaited>, + TError, + TData + > + >; + request?: SecondParameter; + }, +) => { const { query: queryOptions, request: requestOptions } = options ?? {}; const queryKey = - queryOptions?.queryKey ?? getListBoardsApiV1AgentBoardsGetQueryKey(); + queryOptions?.queryKey ?? getListBoardsApiV1AgentBoardsGetQueryKey(params); const queryFn: QueryFunction< Awaited> > = ({ signal }) => - listBoardsApiV1AgentBoardsGet({ signal, ...requestOptions }); + listBoardsApiV1AgentBoardsGet(params, { signal, ...requestOptions }); return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< Awaited>, @@ -135,6 +163,7 @@ export function useListBoardsApiV1AgentBoardsGet< TData = Awaited>, TError = HTTPValidationError, >( + params: undefined | ListBoardsApiV1AgentBoardsGetParams, options: { query: Partial< UseQueryOptions< @@ -161,6 +190,7 @@ export function useListBoardsApiV1AgentBoardsGet< TData = Awaited>, TError = HTTPValidationError, >( + params?: ListBoardsApiV1AgentBoardsGetParams, options?: { query?: Partial< UseQueryOptions< @@ -187,6 +217,7 @@ export function useListBoardsApiV1AgentBoardsGet< TData = Awaited>, TError = HTTPValidationError, >( + params?: ListBoardsApiV1AgentBoardsGetParams, options?: { query?: Partial< UseQueryOptions< @@ -209,6 +240,7 @@ export function useListBoardsApiV1AgentBoardsGet< TData = Awaited>, TError = HTTPValidationError, >( + params?: ListBoardsApiV1AgentBoardsGetParams, options?: { query?: Partial< UseQueryOptions< @@ -223,7 +255,10 @@ export function useListBoardsApiV1AgentBoardsGet< ): UseQueryResult & { queryKey: DataTag; } { - const queryOptions = getListBoardsApiV1AgentBoardsGetQueryOptions(options); + const queryOptions = getListBoardsApiV1AgentBoardsGetQueryOptions( + params, + options, + ); const query = useQuery(queryOptions, queryClient) as UseQueryResult< TData, @@ -439,7 +474,7 @@ export function useGetBoardApiV1AgentBoardsBoardIdGet< * @summary List Agents */ export type listAgentsApiV1AgentAgentsGetResponse200 = { - data: AgentRead[]; + data: LimitOffsetPageTypeVarCustomizedAgentRead; status: 200; }; @@ -766,7 +801,7 @@ export const useCreateAgentApiV1AgentAgentsPost = < * @summary List Tasks */ export type listTasksApiV1AgentBoardsBoardIdTasksGetResponse200 = { - data: TaskRead[]; + data: LimitOffsetPageTypeVarCustomizedTaskRead; status: 200; }; @@ -1265,7 +1300,7 @@ export const useUpdateTaskApiV1AgentBoardsBoardIdTasksTaskIdPatch = < */ export type listTaskCommentsApiV1AgentBoardsBoardIdTasksTaskIdCommentsGetResponse200 = { - data: TaskCommentRead[]; + data: LimitOffsetPageTypeVarCustomizedTaskCommentRead; status: 200; }; @@ -1290,20 +1325,41 @@ export type listTaskCommentsApiV1AgentBoardsBoardIdTasksTaskIdCommentsGetRespons | listTaskCommentsApiV1AgentBoardsBoardIdTasksTaskIdCommentsGetResponseError; export const getListTaskCommentsApiV1AgentBoardsBoardIdTasksTaskIdCommentsGetUrl = - (boardId: string, taskId: string) => { - return `/api/v1/agent/boards/${boardId}/tasks/${taskId}/comments`; + ( + boardId: string, + taskId: string, + params?: ListTaskCommentsApiV1AgentBoardsBoardIdTasksTaskIdCommentsGetParams, + ) => { + const normalizedParams = new URLSearchParams(); + + Object.entries(params || {}).forEach(([key, value]) => { + if (value !== undefined) { + normalizedParams.append( + key, + value === null ? "null" : value.toString(), + ); + } + }); + + const stringifiedParams = normalizedParams.toString(); + + return stringifiedParams.length > 0 + ? `/api/v1/agent/boards/${boardId}/tasks/${taskId}/comments?${stringifiedParams}` + : `/api/v1/agent/boards/${boardId}/tasks/${taskId}/comments`; }; export const listTaskCommentsApiV1AgentBoardsBoardIdTasksTaskIdCommentsGet = async ( boardId: string, taskId: string, + params?: ListTaskCommentsApiV1AgentBoardsBoardIdTasksTaskIdCommentsGetParams, options?: RequestInit, ): Promise => { return customFetch( getListTaskCommentsApiV1AgentBoardsBoardIdTasksTaskIdCommentsGetUrl( boardId, taskId, + params, ), { ...options, @@ -1313,9 +1369,14 @@ export const listTaskCommentsApiV1AgentBoardsBoardIdTasksTaskIdCommentsGet = }; export const getListTaskCommentsApiV1AgentBoardsBoardIdTasksTaskIdCommentsGetQueryKey = - (boardId: string, taskId: string) => { + ( + boardId: string, + taskId: string, + params?: ListTaskCommentsApiV1AgentBoardsBoardIdTasksTaskIdCommentsGetParams, + ) => { return [ `/api/v1/agent/boards/${boardId}/tasks/${taskId}/comments`, + ...(params ? [params] : []), ] as const; }; @@ -1330,6 +1391,7 @@ export const getListTaskCommentsApiV1AgentBoardsBoardIdTasksTaskIdCommentsGetQue >( boardId: string, taskId: string, + params?: ListTaskCommentsApiV1AgentBoardsBoardIdTasksTaskIdCommentsGetParams, options?: { query?: Partial< UseQueryOptions< @@ -1352,6 +1414,7 @@ export const getListTaskCommentsApiV1AgentBoardsBoardIdTasksTaskIdCommentsGetQue getListTaskCommentsApiV1AgentBoardsBoardIdTasksTaskIdCommentsGetQueryKey( boardId, taskId, + params, ); const queryFn: QueryFunction< @@ -1364,6 +1427,7 @@ export const getListTaskCommentsApiV1AgentBoardsBoardIdTasksTaskIdCommentsGetQue listTaskCommentsApiV1AgentBoardsBoardIdTasksTaskIdCommentsGet( boardId, taskId, + params, { signal, ...requestOptions }, ); @@ -1404,6 +1468,9 @@ export function useListTaskCommentsApiV1AgentBoardsBoardIdTasksTaskIdCommentsGet >( boardId: string, taskId: string, + params: + | undefined + | ListTaskCommentsApiV1AgentBoardsBoardIdTasksTaskIdCommentsGetParams, options: { query: Partial< UseQueryOptions< @@ -1448,6 +1515,7 @@ export function useListTaskCommentsApiV1AgentBoardsBoardIdTasksTaskIdCommentsGet >( boardId: string, taskId: string, + params?: ListTaskCommentsApiV1AgentBoardsBoardIdTasksTaskIdCommentsGetParams, options?: { query?: Partial< UseQueryOptions< @@ -1492,6 +1560,7 @@ export function useListTaskCommentsApiV1AgentBoardsBoardIdTasksTaskIdCommentsGet >( boardId: string, taskId: string, + params?: ListTaskCommentsApiV1AgentBoardsBoardIdTasksTaskIdCommentsGetParams, options?: { query?: Partial< UseQueryOptions< @@ -1524,6 +1593,7 @@ export function useListTaskCommentsApiV1AgentBoardsBoardIdTasksTaskIdCommentsGet >( boardId: string, taskId: string, + params?: ListTaskCommentsApiV1AgentBoardsBoardIdTasksTaskIdCommentsGetParams, options?: { query?: Partial< UseQueryOptions< @@ -1546,6 +1616,7 @@ export function useListTaskCommentsApiV1AgentBoardsBoardIdTasksTaskIdCommentsGet getListTaskCommentsApiV1AgentBoardsBoardIdTasksTaskIdCommentsGetQueryOptions( boardId, taskId, + params, options, ); @@ -1720,7 +1791,7 @@ export const useCreateTaskCommentApiV1AgentBoardsBoardIdTasksTaskIdCommentsPost * @summary List Board Memory */ export type listBoardMemoryApiV1AgentBoardsBoardIdMemoryGetResponse200 = { - data: BoardMemoryRead[]; + data: LimitOffsetPageTypeVarCustomizedBoardMemoryRead; status: 200; }; @@ -2121,7 +2192,7 @@ export const useCreateBoardMemoryApiV1AgentBoardsBoardIdMemoryPost = < * @summary List Approvals */ export type listApprovalsApiV1AgentBoardsBoardIdApprovalsGetResponse200 = { - data: ApprovalRead[]; + data: LimitOffsetPageTypeVarCustomizedApprovalRead; status: 200; }; diff --git a/frontend/src/api/generated/agents/agents.ts b/frontend/src/api/generated/agents/agents.ts index 0f45ec8c..e1ace2e7 100644 --- a/frontend/src/api/generated/agents/agents.ts +++ b/frontend/src/api/generated/agents/agents.ts @@ -27,6 +27,8 @@ import type { AgentRead, AgentUpdate, HTTPValidationError, + LimitOffsetPageTypeVarCustomizedAgentRead, + ListAgentsApiV1AgentsGetParams, OkResponse, StreamAgentsApiV1AgentsStreamGetParams, UpdateAgentApiV1AgentsAgentIdPatchParams, @@ -40,26 +42,52 @@ type SecondParameter unknown> = Parameters[1]; * @summary List Agents */ export type listAgentsApiV1AgentsGetResponse200 = { - data: AgentRead[]; + data: LimitOffsetPageTypeVarCustomizedAgentRead; status: 200; }; +export type listAgentsApiV1AgentsGetResponse422 = { + data: HTTPValidationError; + status: 422; +}; + export type listAgentsApiV1AgentsGetResponseSuccess = listAgentsApiV1AgentsGetResponse200 & { headers: Headers; }; -export type listAgentsApiV1AgentsGetResponse = - listAgentsApiV1AgentsGetResponseSuccess; +export type listAgentsApiV1AgentsGetResponseError = + listAgentsApiV1AgentsGetResponse422 & { + headers: Headers; + }; -export const getListAgentsApiV1AgentsGetUrl = () => { - return `/api/v1/agents`; +export type listAgentsApiV1AgentsGetResponse = + | listAgentsApiV1AgentsGetResponseSuccess + | listAgentsApiV1AgentsGetResponseError; + +export const getListAgentsApiV1AgentsGetUrl = ( + params?: ListAgentsApiV1AgentsGetParams, +) => { + const normalizedParams = new URLSearchParams(); + + Object.entries(params || {}).forEach(([key, value]) => { + if (value !== undefined) { + normalizedParams.append(key, value === null ? "null" : value.toString()); + } + }); + + const stringifiedParams = normalizedParams.toString(); + + return stringifiedParams.length > 0 + ? `/api/v1/agents?${stringifiedParams}` + : `/api/v1/agents`; }; export const listAgentsApiV1AgentsGet = async ( + params?: ListAgentsApiV1AgentsGetParams, options?: RequestInit, ): Promise => { return customFetch( - getListAgentsApiV1AgentsGetUrl(), + getListAgentsApiV1AgentsGetUrl(params), { ...options, method: "GET", @@ -67,31 +95,37 @@ export const listAgentsApiV1AgentsGet = async ( ); }; -export const getListAgentsApiV1AgentsGetQueryKey = () => { - return [`/api/v1/agents`] as const; +export const getListAgentsApiV1AgentsGetQueryKey = ( + params?: ListAgentsApiV1AgentsGetParams, +) => { + return [`/api/v1/agents`, ...(params ? [params] : [])] as const; }; export const getListAgentsApiV1AgentsGetQueryOptions = < TData = Awaited>, - TError = unknown, ->(options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; -}) => { + TError = HTTPValidationError, +>( + params?: ListAgentsApiV1AgentsGetParams, + options?: { + query?: Partial< + UseQueryOptions< + Awaited>, + TError, + TData + > + >; + request?: SecondParameter; + }, +) => { const { query: queryOptions, request: requestOptions } = options ?? {}; const queryKey = - queryOptions?.queryKey ?? getListAgentsApiV1AgentsGetQueryKey(); + queryOptions?.queryKey ?? getListAgentsApiV1AgentsGetQueryKey(params); const queryFn: QueryFunction< Awaited> - > = ({ signal }) => listAgentsApiV1AgentsGet({ signal, ...requestOptions }); + > = ({ signal }) => + listAgentsApiV1AgentsGet(params, { signal, ...requestOptions }); return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< Awaited>, @@ -103,12 +137,13 @@ export const getListAgentsApiV1AgentsGetQueryOptions = < export type ListAgentsApiV1AgentsGetQueryResult = NonNullable< Awaited> >; -export type ListAgentsApiV1AgentsGetQueryError = unknown; +export type ListAgentsApiV1AgentsGetQueryError = HTTPValidationError; export function useListAgentsApiV1AgentsGet< TData = Awaited>, - TError = unknown, + TError = HTTPValidationError, >( + params: undefined | ListAgentsApiV1AgentsGetParams, options: { query: Partial< UseQueryOptions< @@ -133,8 +168,9 @@ export function useListAgentsApiV1AgentsGet< }; export function useListAgentsApiV1AgentsGet< TData = Awaited>, - TError = unknown, + TError = HTTPValidationError, >( + params?: ListAgentsApiV1AgentsGetParams, options?: { query?: Partial< UseQueryOptions< @@ -159,8 +195,9 @@ export function useListAgentsApiV1AgentsGet< }; export function useListAgentsApiV1AgentsGet< TData = Awaited>, - TError = unknown, + TError = HTTPValidationError, >( + params?: ListAgentsApiV1AgentsGetParams, options?: { query?: Partial< UseQueryOptions< @@ -181,8 +218,9 @@ export function useListAgentsApiV1AgentsGet< export function useListAgentsApiV1AgentsGet< TData = Awaited>, - TError = unknown, + TError = HTTPValidationError, >( + params?: ListAgentsApiV1AgentsGetParams, options?: { query?: Partial< UseQueryOptions< @@ -197,7 +235,7 @@ export function useListAgentsApiV1AgentsGet< ): UseQueryResult & { queryKey: DataTag; } { - const queryOptions = getListAgentsApiV1AgentsGetQueryOptions(options); + const queryOptions = getListAgentsApiV1AgentsGetQueryOptions(params, options); const query = useQuery(queryOptions, queryClient) as UseQueryResult< TData, diff --git a/frontend/src/api/generated/approvals/approvals.ts b/frontend/src/api/generated/approvals/approvals.ts index e84364fe..1af53078 100644 --- a/frontend/src/api/generated/approvals/approvals.ts +++ b/frontend/src/api/generated/approvals/approvals.ts @@ -25,6 +25,7 @@ import type { ApprovalRead, ApprovalUpdate, HTTPValidationError, + LimitOffsetPageTypeVarCustomizedApprovalRead, ListApprovalsApiV1BoardsBoardIdApprovalsGetParams, StreamApprovalsApiV1BoardsBoardIdApprovalsStreamGetParams, } from ".././model"; @@ -37,7 +38,7 @@ type SecondParameter unknown> = Parameters[1]; * @summary List Approvals */ export type listApprovalsApiV1BoardsBoardIdApprovalsGetResponse200 = { - data: ApprovalRead[]; + data: LimitOffsetPageTypeVarCustomizedApprovalRead; status: 200; }; diff --git a/frontend/src/api/generated/board-memory/board-memory.ts b/frontend/src/api/generated/board-memory/board-memory.ts index f2bd2589..286fea26 100644 --- a/frontend/src/api/generated/board-memory/board-memory.ts +++ b/frontend/src/api/generated/board-memory/board-memory.ts @@ -24,6 +24,7 @@ import type { BoardMemoryCreate, BoardMemoryRead, HTTPValidationError, + LimitOffsetPageTypeVarCustomizedBoardMemoryRead, ListBoardMemoryApiV1BoardsBoardIdMemoryGetParams, StreamBoardMemoryApiV1BoardsBoardIdMemoryStreamGetParams, } from ".././model"; @@ -36,7 +37,7 @@ type SecondParameter unknown> = Parameters[1]; * @summary List Board Memory */ export type listBoardMemoryApiV1BoardsBoardIdMemoryGetResponse200 = { - data: BoardMemoryRead[]; + data: LimitOffsetPageTypeVarCustomizedBoardMemoryRead; status: 200; }; diff --git a/frontend/src/api/generated/boards/boards.ts b/frontend/src/api/generated/boards/boards.ts index 4f1def92..05e6f96f 100644 --- a/frontend/src/api/generated/boards/boards.ts +++ b/frontend/src/api/generated/boards/boards.ts @@ -23,8 +23,11 @@ import type { import type { BoardCreate, BoardRead, + BoardSnapshot, BoardUpdate, HTTPValidationError, + LimitOffsetPageTypeVarCustomizedBoardRead, + ListBoardsApiV1BoardsGetParams, OkResponse, } from ".././model"; @@ -36,7 +39,7 @@ type SecondParameter unknown> = Parameters[1]; * @summary List Boards */ export type listBoardsApiV1BoardsGetResponse200 = { - data: BoardRead[]; + data: LimitOffsetPageTypeVarCustomizedBoardRead; status: 200; }; @@ -58,15 +61,30 @@ export type listBoardsApiV1BoardsGetResponse = | listBoardsApiV1BoardsGetResponseSuccess | listBoardsApiV1BoardsGetResponseError; -export const getListBoardsApiV1BoardsGetUrl = () => { - return `/api/v1/boards`; +export const getListBoardsApiV1BoardsGetUrl = ( + params?: ListBoardsApiV1BoardsGetParams, +) => { + const normalizedParams = new URLSearchParams(); + + Object.entries(params || {}).forEach(([key, value]) => { + if (value !== undefined) { + normalizedParams.append(key, value === null ? "null" : value.toString()); + } + }); + + const stringifiedParams = normalizedParams.toString(); + + return stringifiedParams.length > 0 + ? `/api/v1/boards?${stringifiedParams}` + : `/api/v1/boards`; }; export const listBoardsApiV1BoardsGet = async ( + params?: ListBoardsApiV1BoardsGetParams, options?: RequestInit, ): Promise => { return customFetch( - getListBoardsApiV1BoardsGetUrl(), + getListBoardsApiV1BoardsGetUrl(params), { ...options, method: "GET", @@ -74,31 +92,37 @@ export const listBoardsApiV1BoardsGet = async ( ); }; -export const getListBoardsApiV1BoardsGetQueryKey = () => { - return [`/api/v1/boards`] as const; +export const getListBoardsApiV1BoardsGetQueryKey = ( + params?: ListBoardsApiV1BoardsGetParams, +) => { + return [`/api/v1/boards`, ...(params ? [params] : [])] as const; }; export const getListBoardsApiV1BoardsGetQueryOptions = < TData = Awaited>, TError = HTTPValidationError, ->(options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; -}) => { +>( + params?: ListBoardsApiV1BoardsGetParams, + options?: { + query?: Partial< + UseQueryOptions< + Awaited>, + TError, + TData + > + >; + request?: SecondParameter; + }, +) => { const { query: queryOptions, request: requestOptions } = options ?? {}; const queryKey = - queryOptions?.queryKey ?? getListBoardsApiV1BoardsGetQueryKey(); + queryOptions?.queryKey ?? getListBoardsApiV1BoardsGetQueryKey(params); const queryFn: QueryFunction< Awaited> - > = ({ signal }) => listBoardsApiV1BoardsGet({ signal, ...requestOptions }); + > = ({ signal }) => + listBoardsApiV1BoardsGet(params, { signal, ...requestOptions }); return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< Awaited>, @@ -116,6 +140,7 @@ export function useListBoardsApiV1BoardsGet< TData = Awaited>, TError = HTTPValidationError, >( + params: undefined | ListBoardsApiV1BoardsGetParams, options: { query: Partial< UseQueryOptions< @@ -142,6 +167,7 @@ export function useListBoardsApiV1BoardsGet< TData = Awaited>, TError = HTTPValidationError, >( + params?: ListBoardsApiV1BoardsGetParams, options?: { query?: Partial< UseQueryOptions< @@ -168,6 +194,7 @@ export function useListBoardsApiV1BoardsGet< TData = Awaited>, TError = HTTPValidationError, >( + params?: ListBoardsApiV1BoardsGetParams, options?: { query?: Partial< UseQueryOptions< @@ -190,6 +217,7 @@ export function useListBoardsApiV1BoardsGet< TData = Awaited>, TError = HTTPValidationError, >( + params?: ListBoardsApiV1BoardsGetParams, options?: { query?: Partial< UseQueryOptions< @@ -204,7 +232,7 @@ export function useListBoardsApiV1BoardsGet< ): UseQueryResult & { queryKey: DataTag; } { - const queryOptions = getListBoardsApiV1BoardsGetQueryOptions(options); + const queryOptions = getListBoardsApiV1BoardsGetQueryOptions(params, options); const query = useQuery(queryOptions, queryClient) as UseQueryResult< TData, @@ -765,3 +793,240 @@ export const useDeleteBoardApiV1BoardsBoardIdDelete = < queryClient, ); }; +/** + * @summary Get Board Snapshot + */ +export type getBoardSnapshotApiV1BoardsBoardIdSnapshotGetResponse200 = { + data: BoardSnapshot; + status: 200; +}; + +export type getBoardSnapshotApiV1BoardsBoardIdSnapshotGetResponse422 = { + data: HTTPValidationError; + status: 422; +}; + +export type getBoardSnapshotApiV1BoardsBoardIdSnapshotGetResponseSuccess = + getBoardSnapshotApiV1BoardsBoardIdSnapshotGetResponse200 & { + headers: Headers; + }; +export type getBoardSnapshotApiV1BoardsBoardIdSnapshotGetResponseError = + getBoardSnapshotApiV1BoardsBoardIdSnapshotGetResponse422 & { + headers: Headers; + }; + +export type getBoardSnapshotApiV1BoardsBoardIdSnapshotGetResponse = + | getBoardSnapshotApiV1BoardsBoardIdSnapshotGetResponseSuccess + | getBoardSnapshotApiV1BoardsBoardIdSnapshotGetResponseError; + +export const getGetBoardSnapshotApiV1BoardsBoardIdSnapshotGetUrl = ( + boardId: string, +) => { + return `/api/v1/boards/${boardId}/snapshot`; +}; + +export const getBoardSnapshotApiV1BoardsBoardIdSnapshotGet = async ( + boardId: string, + options?: RequestInit, +): Promise => { + return customFetch( + getGetBoardSnapshotApiV1BoardsBoardIdSnapshotGetUrl(boardId), + { + ...options, + method: "GET", + }, + ); +}; + +export const getGetBoardSnapshotApiV1BoardsBoardIdSnapshotGetQueryKey = ( + boardId: string, +) => { + return [`/api/v1/boards/${boardId}/snapshot`] as const; +}; + +export const getGetBoardSnapshotApiV1BoardsBoardIdSnapshotGetQueryOptions = < + TData = Awaited< + ReturnType + >, + TError = HTTPValidationError, +>( + boardId: string, + options?: { + query?: Partial< + UseQueryOptions< + Awaited< + ReturnType + >, + TError, + TData + > + >; + request?: SecondParameter; + }, +) => { + const { query: queryOptions, request: requestOptions } = options ?? {}; + + const queryKey = + queryOptions?.queryKey ?? + getGetBoardSnapshotApiV1BoardsBoardIdSnapshotGetQueryKey(boardId); + + const queryFn: QueryFunction< + Awaited> + > = ({ signal }) => + getBoardSnapshotApiV1BoardsBoardIdSnapshotGet(boardId, { + signal, + ...requestOptions, + }); + + return { + queryKey, + queryFn, + enabled: !!boardId, + ...queryOptions, + } as UseQueryOptions< + Awaited>, + TError, + TData + > & { queryKey: DataTag }; +}; + +export type GetBoardSnapshotApiV1BoardsBoardIdSnapshotGetQueryResult = + NonNullable< + Awaited> + >; +export type GetBoardSnapshotApiV1BoardsBoardIdSnapshotGetQueryError = + HTTPValidationError; + +export function useGetBoardSnapshotApiV1BoardsBoardIdSnapshotGet< + TData = Awaited< + ReturnType + >, + TError = HTTPValidationError, +>( + boardId: string, + options: { + query: Partial< + UseQueryOptions< + Awaited< + ReturnType + >, + TError, + TData + > + > & + Pick< + DefinedInitialDataOptions< + Awaited< + ReturnType + >, + TError, + Awaited< + ReturnType + > + >, + "initialData" + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): DefinedUseQueryResult & { + queryKey: DataTag; +}; +export function useGetBoardSnapshotApiV1BoardsBoardIdSnapshotGet< + TData = Awaited< + ReturnType + >, + TError = HTTPValidationError, +>( + boardId: string, + options?: { + query?: Partial< + UseQueryOptions< + Awaited< + ReturnType + >, + TError, + TData + > + > & + Pick< + UndefinedInitialDataOptions< + Awaited< + ReturnType + >, + TError, + Awaited< + ReturnType + > + >, + "initialData" + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { + queryKey: DataTag; +}; +export function useGetBoardSnapshotApiV1BoardsBoardIdSnapshotGet< + TData = Awaited< + ReturnType + >, + TError = HTTPValidationError, +>( + boardId: string, + options?: { + query?: Partial< + UseQueryOptions< + Awaited< + ReturnType + >, + TError, + TData + > + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { + queryKey: DataTag; +}; +/** + * @summary Get Board Snapshot + */ + +export function useGetBoardSnapshotApiV1BoardsBoardIdSnapshotGet< + TData = Awaited< + ReturnType + >, + TError = HTTPValidationError, +>( + boardId: string, + options?: { + query?: Partial< + UseQueryOptions< + Awaited< + ReturnType + >, + TError, + TData + > + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { + queryKey: DataTag; +} { + const queryOptions = + getGetBoardSnapshotApiV1BoardsBoardIdSnapshotGetQueryOptions( + boardId, + options, + ); + + const query = useQuery(queryOptions, queryClient) as UseQueryResult< + TData, + TError + > & { queryKey: DataTag }; + + return { ...query, queryKey: queryOptions.queryKey }; +} diff --git a/frontend/src/api/generated/gateways/gateways.ts b/frontend/src/api/generated/gateways/gateways.ts index 70064c31..1def48f4 100644 --- a/frontend/src/api/generated/gateways/gateways.ts +++ b/frontend/src/api/generated/gateways/gateways.ts @@ -34,7 +34,9 @@ import type { GetGatewaySessionApiV1GatewaysSessionsSessionIdGetParams, GetSessionHistoryApiV1GatewaysSessionsSessionIdHistoryGetParams, HTTPValidationError, + LimitOffsetPageTypeVarCustomizedGatewayRead, ListGatewaySessionsApiV1GatewaysSessionsGetParams, + ListGatewaysApiV1GatewaysGetParams, OkResponse, SendGatewaySessionMessageApiV1GatewaysSessionsSessionIdMessagePostParams, } from ".././model"; @@ -1451,26 +1453,52 @@ export function useGatewayCommandsApiV1GatewaysCommandsGet< * @summary List Gateways */ export type listGatewaysApiV1GatewaysGetResponse200 = { - data: GatewayRead[]; + data: LimitOffsetPageTypeVarCustomizedGatewayRead; status: 200; }; +export type listGatewaysApiV1GatewaysGetResponse422 = { + data: HTTPValidationError; + status: 422; +}; + export type listGatewaysApiV1GatewaysGetResponseSuccess = listGatewaysApiV1GatewaysGetResponse200 & { headers: Headers; }; -export type listGatewaysApiV1GatewaysGetResponse = - listGatewaysApiV1GatewaysGetResponseSuccess; +export type listGatewaysApiV1GatewaysGetResponseError = + listGatewaysApiV1GatewaysGetResponse422 & { + headers: Headers; + }; -export const getListGatewaysApiV1GatewaysGetUrl = () => { - return `/api/v1/gateways`; +export type listGatewaysApiV1GatewaysGetResponse = + | listGatewaysApiV1GatewaysGetResponseSuccess + | listGatewaysApiV1GatewaysGetResponseError; + +export const getListGatewaysApiV1GatewaysGetUrl = ( + params?: ListGatewaysApiV1GatewaysGetParams, +) => { + const normalizedParams = new URLSearchParams(); + + Object.entries(params || {}).forEach(([key, value]) => { + if (value !== undefined) { + normalizedParams.append(key, value === null ? "null" : value.toString()); + } + }); + + const stringifiedParams = normalizedParams.toString(); + + return stringifiedParams.length > 0 + ? `/api/v1/gateways?${stringifiedParams}` + : `/api/v1/gateways`; }; export const listGatewaysApiV1GatewaysGet = async ( + params?: ListGatewaysApiV1GatewaysGetParams, options?: RequestInit, ): Promise => { return customFetch( - getListGatewaysApiV1GatewaysGetUrl(), + getListGatewaysApiV1GatewaysGetUrl(params), { ...options, method: "GET", @@ -1478,32 +1506,37 @@ export const listGatewaysApiV1GatewaysGet = async ( ); }; -export const getListGatewaysApiV1GatewaysGetQueryKey = () => { - return [`/api/v1/gateways`] as const; +export const getListGatewaysApiV1GatewaysGetQueryKey = ( + params?: ListGatewaysApiV1GatewaysGetParams, +) => { + return [`/api/v1/gateways`, ...(params ? [params] : [])] as const; }; export const getListGatewaysApiV1GatewaysGetQueryOptions = < TData = Awaited>, - TError = unknown, ->(options?: { - query?: Partial< - UseQueryOptions< - Awaited>, - TError, - TData - > - >; - request?: SecondParameter; -}) => { + TError = HTTPValidationError, +>( + params?: ListGatewaysApiV1GatewaysGetParams, + options?: { + query?: Partial< + UseQueryOptions< + Awaited>, + TError, + TData + > + >; + request?: SecondParameter; + }, +) => { const { query: queryOptions, request: requestOptions } = options ?? {}; const queryKey = - queryOptions?.queryKey ?? getListGatewaysApiV1GatewaysGetQueryKey(); + queryOptions?.queryKey ?? getListGatewaysApiV1GatewaysGetQueryKey(params); const queryFn: QueryFunction< Awaited> > = ({ signal }) => - listGatewaysApiV1GatewaysGet({ signal, ...requestOptions }); + listGatewaysApiV1GatewaysGet(params, { signal, ...requestOptions }); return { queryKey, queryFn, ...queryOptions } as UseQueryOptions< Awaited>, @@ -1515,12 +1548,13 @@ export const getListGatewaysApiV1GatewaysGetQueryOptions = < export type ListGatewaysApiV1GatewaysGetQueryResult = NonNullable< Awaited> >; -export type ListGatewaysApiV1GatewaysGetQueryError = unknown; +export type ListGatewaysApiV1GatewaysGetQueryError = HTTPValidationError; export function useListGatewaysApiV1GatewaysGet< TData = Awaited>, - TError = unknown, + TError = HTTPValidationError, >( + params: undefined | ListGatewaysApiV1GatewaysGetParams, options: { query: Partial< UseQueryOptions< @@ -1545,8 +1579,9 @@ export function useListGatewaysApiV1GatewaysGet< }; export function useListGatewaysApiV1GatewaysGet< TData = Awaited>, - TError = unknown, + TError = HTTPValidationError, >( + params?: ListGatewaysApiV1GatewaysGetParams, options?: { query?: Partial< UseQueryOptions< @@ -1571,8 +1606,9 @@ export function useListGatewaysApiV1GatewaysGet< }; export function useListGatewaysApiV1GatewaysGet< TData = Awaited>, - TError = unknown, + TError = HTTPValidationError, >( + params?: ListGatewaysApiV1GatewaysGetParams, options?: { query?: Partial< UseQueryOptions< @@ -1593,8 +1629,9 @@ export function useListGatewaysApiV1GatewaysGet< export function useListGatewaysApiV1GatewaysGet< TData = Awaited>, - TError = unknown, + TError = HTTPValidationError, >( + params?: ListGatewaysApiV1GatewaysGetParams, options?: { query?: Partial< UseQueryOptions< @@ -1609,7 +1646,10 @@ export function useListGatewaysApiV1GatewaysGet< ): UseQueryResult & { queryKey: DataTag; } { - const queryOptions = getListGatewaysApiV1GatewaysGetQueryOptions(options); + const queryOptions = getListGatewaysApiV1GatewaysGetQueryOptions( + params, + options, + ); const query = useQuery(queryOptions, queryClient) as UseQueryResult< TData, diff --git a/frontend/src/api/generated/model/approvalCreate.ts b/frontend/src/api/generated/model/approvalCreate.ts index 099b2a67..3172f0a2 100644 --- a/frontend/src/api/generated/model/approvalCreate.ts +++ b/frontend/src/api/generated/model/approvalCreate.ts @@ -10,6 +10,7 @@ import type { ApprovalCreateStatus } from "./approvalCreateStatus"; export interface ApprovalCreate { action_type: string; + task_id?: string | null; payload?: ApprovalCreatePayload; confidence: number; rubric_scores?: ApprovalCreateRubricScores; diff --git a/frontend/src/api/generated/model/approvalRead.ts b/frontend/src/api/generated/model/approvalRead.ts index d00ffd95..8049d94e 100644 --- a/frontend/src/api/generated/model/approvalRead.ts +++ b/frontend/src/api/generated/model/approvalRead.ts @@ -10,6 +10,7 @@ import type { ApprovalReadStatus } from "./approvalReadStatus"; export interface ApprovalRead { action_type: string; + task_id?: string | null; payload?: ApprovalReadPayload; confidence: number; rubric_scores?: ApprovalReadRubricScores; diff --git a/frontend/src/api/generated/model/boardMemoryRead.ts b/frontend/src/api/generated/model/boardMemoryRead.ts index 4e3c7db0..ea198c3d 100644 --- a/frontend/src/api/generated/model/boardMemoryRead.ts +++ b/frontend/src/api/generated/model/boardMemoryRead.ts @@ -12,5 +12,6 @@ export interface BoardMemoryRead { source?: string | null; id: string; board_id: string; + is_chat?: boolean; created_at: string; } diff --git a/frontend/src/api/generated/model/boardSnapshot.ts b/frontend/src/api/generated/model/boardSnapshot.ts new file mode 100644 index 00000000..ac1b39cb --- /dev/null +++ b/frontend/src/api/generated/model/boardSnapshot.ts @@ -0,0 +1,20 @@ +/** + * Generated by orval v8.2.0 🍺 + * Do not edit manually. + * Mission Control API + * OpenAPI spec version: 0.1.0 + */ +import type { AgentRead } from "./agentRead"; +import type { ApprovalRead } from "./approvalRead"; +import type { BoardMemoryRead } from "./boardMemoryRead"; +import type { BoardRead } from "./boardRead"; +import type { TaskCardRead } from "./taskCardRead"; + +export interface BoardSnapshot { + board: BoardRead; + tasks: TaskCardRead[]; + agents: AgentRead[]; + approvals: ApprovalRead[]; + chat_messages: BoardMemoryRead[]; + pending_approvals_count?: number; +} diff --git a/frontend/src/api/generated/model/index.ts b/frontend/src/api/generated/model/index.ts index ccdf4117..ae1c2edf 100644 --- a/frontend/src/api/generated/model/index.ts +++ b/frontend/src/api/generated/model/index.ts @@ -46,6 +46,7 @@ export * from "./boardOnboardingReadMessages"; export * from "./boardOnboardingStart"; export * from "./boardRead"; export * from "./boardReadSuccessMetrics"; +export * from "./boardSnapshot"; export * from "./boardUpdate"; export * from "./boardUpdateSuccessMetrics"; export * from "./confirmDeleteAgentApiV1AgentsAgentIdDeleteConfirmPost200"; @@ -90,15 +91,29 @@ export * from "./getSessionHistoryApiV1GatewaysSessionsSessionIdHistoryGetParams export * from "./healthHealthGet200"; export * from "./healthzHealthzGet200"; export * from "./hTTPValidationError"; +export * from "./limitOffsetPageTypeVarCustomizedActivityEventRead"; +export * from "./limitOffsetPageTypeVarCustomizedAgentRead"; +export * from "./limitOffsetPageTypeVarCustomizedApprovalRead"; +export * from "./limitOffsetPageTypeVarCustomizedBoardMemoryRead"; +export * from "./limitOffsetPageTypeVarCustomizedBoardRead"; +export * from "./limitOffsetPageTypeVarCustomizedGatewayRead"; +export * from "./limitOffsetPageTypeVarCustomizedTaskCommentRead"; +export * from "./limitOffsetPageTypeVarCustomizedTaskRead"; export * from "./listActivityApiV1ActivityGetParams"; export * from "./listAgentsApiV1AgentAgentsGetParams"; +export * from "./listAgentsApiV1AgentsGetParams"; export * from "./listApprovalsApiV1AgentBoardsBoardIdApprovalsGetParams"; export * from "./listApprovalsApiV1BoardsBoardIdApprovalsGetParams"; export * from "./listBoardMemoryApiV1AgentBoardsBoardIdMemoryGetParams"; export * from "./listBoardMemoryApiV1BoardsBoardIdMemoryGetParams"; +export * from "./listBoardsApiV1AgentBoardsGetParams"; +export * from "./listBoardsApiV1BoardsGetParams"; +export * from "./listGatewaysApiV1GatewaysGetParams"; export * from "./listGatewaySessionsApiV1GatewaysSessionsGetParams"; export * from "./listSessionsApiV1GatewaySessionsGet200"; export * from "./listSessionsApiV1GatewaySessionsGetParams"; +export * from "./listTaskCommentsApiV1AgentBoardsBoardIdTasksTaskIdCommentsGetParams"; +export * from "./listTaskCommentsApiV1BoardsBoardIdTasksTaskIdCommentsGetParams"; export * from "./listTasksApiV1AgentBoardsBoardIdTasksGetParams"; export * from "./listTasksApiV1BoardsBoardIdTasksGetParams"; export * from "./okResponse"; @@ -111,6 +126,8 @@ export * from "./streamAgentsApiV1AgentsStreamGetParams"; export * from "./streamApprovalsApiV1BoardsBoardIdApprovalsStreamGetParams"; export * from "./streamBoardMemoryApiV1BoardsBoardIdMemoryStreamGetParams"; export * from "./streamTasksApiV1BoardsBoardIdTasksStreamGetParams"; +export * from "./taskCardRead"; +export * from "./taskCardReadStatus"; export * from "./taskCommentCreate"; export * from "./taskCommentRead"; export * from "./taskCreate"; diff --git a/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedActivityEventRead.ts b/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedActivityEventRead.ts new file mode 100644 index 00000000..e7eb3a00 --- /dev/null +++ b/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedActivityEventRead.ts @@ -0,0 +1,17 @@ +/** + * Generated by orval v8.2.0 🍺 + * Do not edit manually. + * Mission Control API + * OpenAPI spec version: 0.1.0 + */ +import type { ActivityEventRead } from "./activityEventRead"; + +export interface LimitOffsetPageTypeVarCustomizedActivityEventRead { + items: ActivityEventRead[]; + /** @minimum 0 */ + total: number; + /** @minimum 1 */ + limit: number; + /** @minimum 0 */ + offset: number; +} diff --git a/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedAgentRead.ts b/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedAgentRead.ts new file mode 100644 index 00000000..28ae2480 --- /dev/null +++ b/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedAgentRead.ts @@ -0,0 +1,17 @@ +/** + * Generated by orval v8.2.0 🍺 + * Do not edit manually. + * Mission Control API + * OpenAPI spec version: 0.1.0 + */ +import type { AgentRead } from "./agentRead"; + +export interface LimitOffsetPageTypeVarCustomizedAgentRead { + items: AgentRead[]; + /** @minimum 0 */ + total: number; + /** @minimum 1 */ + limit: number; + /** @minimum 0 */ + offset: number; +} diff --git a/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedApprovalRead.ts b/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedApprovalRead.ts new file mode 100644 index 00000000..dbfacd4b --- /dev/null +++ b/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedApprovalRead.ts @@ -0,0 +1,17 @@ +/** + * Generated by orval v8.2.0 🍺 + * Do not edit manually. + * Mission Control API + * OpenAPI spec version: 0.1.0 + */ +import type { ApprovalRead } from "./approvalRead"; + +export interface LimitOffsetPageTypeVarCustomizedApprovalRead { + items: ApprovalRead[]; + /** @minimum 0 */ + total: number; + /** @minimum 1 */ + limit: number; + /** @minimum 0 */ + offset: number; +} diff --git a/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedBoardMemoryRead.ts b/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedBoardMemoryRead.ts new file mode 100644 index 00000000..a045271f --- /dev/null +++ b/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedBoardMemoryRead.ts @@ -0,0 +1,17 @@ +/** + * Generated by orval v8.2.0 🍺 + * Do not edit manually. + * Mission Control API + * OpenAPI spec version: 0.1.0 + */ +import type { BoardMemoryRead } from "./boardMemoryRead"; + +export interface LimitOffsetPageTypeVarCustomizedBoardMemoryRead { + items: BoardMemoryRead[]; + /** @minimum 0 */ + total: number; + /** @minimum 1 */ + limit: number; + /** @minimum 0 */ + offset: number; +} diff --git a/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedBoardRead.ts b/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedBoardRead.ts new file mode 100644 index 00000000..bd9c340d --- /dev/null +++ b/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedBoardRead.ts @@ -0,0 +1,17 @@ +/** + * Generated by orval v8.2.0 🍺 + * Do not edit manually. + * Mission Control API + * OpenAPI spec version: 0.1.0 + */ +import type { BoardRead } from "./boardRead"; + +export interface LimitOffsetPageTypeVarCustomizedBoardRead { + items: BoardRead[]; + /** @minimum 0 */ + total: number; + /** @minimum 1 */ + limit: number; + /** @minimum 0 */ + offset: number; +} diff --git a/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedGatewayRead.ts b/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedGatewayRead.ts new file mode 100644 index 00000000..137773df --- /dev/null +++ b/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedGatewayRead.ts @@ -0,0 +1,17 @@ +/** + * Generated by orval v8.2.0 🍺 + * Do not edit manually. + * Mission Control API + * OpenAPI spec version: 0.1.0 + */ +import type { GatewayRead } from "./gatewayRead"; + +export interface LimitOffsetPageTypeVarCustomizedGatewayRead { + items: GatewayRead[]; + /** @minimum 0 */ + total: number; + /** @minimum 1 */ + limit: number; + /** @minimum 0 */ + offset: number; +} diff --git a/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedTaskCommentRead.ts b/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedTaskCommentRead.ts new file mode 100644 index 00000000..e3ec06dc --- /dev/null +++ b/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedTaskCommentRead.ts @@ -0,0 +1,17 @@ +/** + * Generated by orval v8.2.0 🍺 + * Do not edit manually. + * Mission Control API + * OpenAPI spec version: 0.1.0 + */ +import type { TaskCommentRead } from "./taskCommentRead"; + +export interface LimitOffsetPageTypeVarCustomizedTaskCommentRead { + items: TaskCommentRead[]; + /** @minimum 0 */ + total: number; + /** @minimum 1 */ + limit: number; + /** @minimum 0 */ + offset: number; +} diff --git a/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedTaskRead.ts b/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedTaskRead.ts new file mode 100644 index 00000000..cd1205dd --- /dev/null +++ b/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedTaskRead.ts @@ -0,0 +1,17 @@ +/** + * Generated by orval v8.2.0 🍺 + * Do not edit manually. + * Mission Control API + * OpenAPI spec version: 0.1.0 + */ +import type { TaskRead } from "./taskRead"; + +export interface LimitOffsetPageTypeVarCustomizedTaskRead { + items: TaskRead[]; + /** @minimum 0 */ + total: number; + /** @minimum 1 */ + limit: number; + /** @minimum 0 */ + offset: number; +} diff --git a/frontend/src/api/generated/model/listAgentsApiV1AgentAgentsGetParams.ts b/frontend/src/api/generated/model/listAgentsApiV1AgentAgentsGetParams.ts index e22792fb..83b88510 100644 --- a/frontend/src/api/generated/model/listAgentsApiV1AgentAgentsGetParams.ts +++ b/frontend/src/api/generated/model/listAgentsApiV1AgentAgentsGetParams.ts @@ -7,5 +7,13 @@ export type ListAgentsApiV1AgentAgentsGetParams = { board_id?: string | null; - limit?: number | null; + /** + * @minimum 1 + * @maximum 200 + */ + limit?: number; + /** + * @minimum 0 + */ + offset?: number; }; diff --git a/frontend/src/api/generated/model/listAgentsApiV1AgentsGetParams.ts b/frontend/src/api/generated/model/listAgentsApiV1AgentsGetParams.ts new file mode 100644 index 00000000..1b17501f --- /dev/null +++ b/frontend/src/api/generated/model/listAgentsApiV1AgentsGetParams.ts @@ -0,0 +1,20 @@ +/** + * Generated by orval v8.2.0 🍺 + * Do not edit manually. + * Mission Control API + * OpenAPI spec version: 0.1.0 + */ + +export type ListAgentsApiV1AgentsGetParams = { + board_id?: string | null; + gateway_id?: string | null; + /** + * @minimum 1 + * @maximum 200 + */ + limit?: number; + /** + * @minimum 0 + */ + offset?: number; +}; diff --git a/frontend/src/api/generated/model/listApprovalsApiV1AgentBoardsBoardIdApprovalsGetParams.ts b/frontend/src/api/generated/model/listApprovalsApiV1AgentBoardsBoardIdApprovalsGetParams.ts index 13281422..927bcb00 100644 --- a/frontend/src/api/generated/model/listApprovalsApiV1AgentBoardsBoardIdApprovalsGetParams.ts +++ b/frontend/src/api/generated/model/listApprovalsApiV1AgentBoardsBoardIdApprovalsGetParams.ts @@ -7,4 +7,13 @@ export type ListApprovalsApiV1AgentBoardsBoardIdApprovalsGetParams = { status?: "pending" | "approved" | "rejected" | null; + /** + * @minimum 1 + * @maximum 200 + */ + limit?: number; + /** + * @minimum 0 + */ + offset?: number; }; diff --git a/frontend/src/api/generated/model/listApprovalsApiV1BoardsBoardIdApprovalsGetParams.ts b/frontend/src/api/generated/model/listApprovalsApiV1BoardsBoardIdApprovalsGetParams.ts index ffc822d9..af71f27b 100644 --- a/frontend/src/api/generated/model/listApprovalsApiV1BoardsBoardIdApprovalsGetParams.ts +++ b/frontend/src/api/generated/model/listApprovalsApiV1BoardsBoardIdApprovalsGetParams.ts @@ -7,4 +7,13 @@ export type ListApprovalsApiV1BoardsBoardIdApprovalsGetParams = { status?: "pending" | "approved" | "rejected" | null; + /** + * @minimum 1 + * @maximum 200 + */ + limit?: number; + /** + * @minimum 0 + */ + offset?: number; }; diff --git a/frontend/src/api/generated/model/listBoardMemoryApiV1BoardsBoardIdMemoryGetParams.ts b/frontend/src/api/generated/model/listBoardMemoryApiV1BoardsBoardIdMemoryGetParams.ts index 2c679c2a..bd3aa75a 100644 --- a/frontend/src/api/generated/model/listBoardMemoryApiV1BoardsBoardIdMemoryGetParams.ts +++ b/frontend/src/api/generated/model/listBoardMemoryApiV1BoardsBoardIdMemoryGetParams.ts @@ -6,6 +6,7 @@ */ export type ListBoardMemoryApiV1BoardsBoardIdMemoryGetParams = { + is_chat?: boolean | null; /** * @minimum 1 * @maximum 200 diff --git a/frontend/src/api/generated/model/listBoardsApiV1AgentBoardsGetParams.ts b/frontend/src/api/generated/model/listBoardsApiV1AgentBoardsGetParams.ts new file mode 100644 index 00000000..3fb85986 --- /dev/null +++ b/frontend/src/api/generated/model/listBoardsApiV1AgentBoardsGetParams.ts @@ -0,0 +1,18 @@ +/** + * Generated by orval v8.2.0 🍺 + * Do not edit manually. + * Mission Control API + * OpenAPI spec version: 0.1.0 + */ + +export type ListBoardsApiV1AgentBoardsGetParams = { + /** + * @minimum 1 + * @maximum 200 + */ + limit?: number; + /** + * @minimum 0 + */ + offset?: number; +}; diff --git a/frontend/src/api/generated/model/listBoardsApiV1BoardsGetParams.ts b/frontend/src/api/generated/model/listBoardsApiV1BoardsGetParams.ts new file mode 100644 index 00000000..44cad29e --- /dev/null +++ b/frontend/src/api/generated/model/listBoardsApiV1BoardsGetParams.ts @@ -0,0 +1,19 @@ +/** + * Generated by orval v8.2.0 🍺 + * Do not edit manually. + * Mission Control API + * OpenAPI spec version: 0.1.0 + */ + +export type ListBoardsApiV1BoardsGetParams = { + gateway_id?: string | null; + /** + * @minimum 1 + * @maximum 200 + */ + limit?: number; + /** + * @minimum 0 + */ + offset?: number; +}; diff --git a/frontend/src/api/generated/model/listGatewaysApiV1GatewaysGetParams.ts b/frontend/src/api/generated/model/listGatewaysApiV1GatewaysGetParams.ts new file mode 100644 index 00000000..d5d5b92d --- /dev/null +++ b/frontend/src/api/generated/model/listGatewaysApiV1GatewaysGetParams.ts @@ -0,0 +1,18 @@ +/** + * Generated by orval v8.2.0 🍺 + * Do not edit manually. + * Mission Control API + * OpenAPI spec version: 0.1.0 + */ + +export type ListGatewaysApiV1GatewaysGetParams = { + /** + * @minimum 1 + * @maximum 200 + */ + limit?: number; + /** + * @minimum 0 + */ + offset?: number; +}; diff --git a/frontend/src/api/generated/model/listTaskCommentsApiV1AgentBoardsBoardIdTasksTaskIdCommentsGetParams.ts b/frontend/src/api/generated/model/listTaskCommentsApiV1AgentBoardsBoardIdTasksTaskIdCommentsGetParams.ts new file mode 100644 index 00000000..91e1f752 --- /dev/null +++ b/frontend/src/api/generated/model/listTaskCommentsApiV1AgentBoardsBoardIdTasksTaskIdCommentsGetParams.ts @@ -0,0 +1,19 @@ +/** + * Generated by orval v8.2.0 🍺 + * Do not edit manually. + * Mission Control API + * OpenAPI spec version: 0.1.0 + */ + +export type ListTaskCommentsApiV1AgentBoardsBoardIdTasksTaskIdCommentsGetParams = + { + /** + * @minimum 1 + * @maximum 200 + */ + limit?: number; + /** + * @minimum 0 + */ + offset?: number; + }; diff --git a/frontend/src/api/generated/model/listTaskCommentsApiV1BoardsBoardIdTasksTaskIdCommentsGetParams.ts b/frontend/src/api/generated/model/listTaskCommentsApiV1BoardsBoardIdTasksTaskIdCommentsGetParams.ts new file mode 100644 index 00000000..acadea95 --- /dev/null +++ b/frontend/src/api/generated/model/listTaskCommentsApiV1BoardsBoardIdTasksTaskIdCommentsGetParams.ts @@ -0,0 +1,18 @@ +/** + * Generated by orval v8.2.0 🍺 + * Do not edit manually. + * Mission Control API + * OpenAPI spec version: 0.1.0 + */ + +export type ListTaskCommentsApiV1BoardsBoardIdTasksTaskIdCommentsGetParams = { + /** + * @minimum 1 + * @maximum 200 + */ + limit?: number; + /** + * @minimum 0 + */ + offset?: number; +}; diff --git a/frontend/src/api/generated/model/listTasksApiV1AgentBoardsBoardIdTasksGetParams.ts b/frontend/src/api/generated/model/listTasksApiV1AgentBoardsBoardIdTasksGetParams.ts index 7f9f34fb..3ee0378f 100644 --- a/frontend/src/api/generated/model/listTasksApiV1AgentBoardsBoardIdTasksGetParams.ts +++ b/frontend/src/api/generated/model/listTasksApiV1AgentBoardsBoardIdTasksGetParams.ts @@ -9,5 +9,13 @@ export type ListTasksApiV1AgentBoardsBoardIdTasksGetParams = { status?: string | null; assigned_agent_id?: string | null; unassigned?: boolean | null; - limit?: number | null; + /** + * @minimum 1 + * @maximum 200 + */ + limit?: number; + /** + * @minimum 0 + */ + offset?: number; }; diff --git a/frontend/src/api/generated/model/listTasksApiV1BoardsBoardIdTasksGetParams.ts b/frontend/src/api/generated/model/listTasksApiV1BoardsBoardIdTasksGetParams.ts index b4952bbf..6c5b7499 100644 --- a/frontend/src/api/generated/model/listTasksApiV1BoardsBoardIdTasksGetParams.ts +++ b/frontend/src/api/generated/model/listTasksApiV1BoardsBoardIdTasksGetParams.ts @@ -9,5 +9,13 @@ export type ListTasksApiV1BoardsBoardIdTasksGetParams = { status?: string | null; assigned_agent_id?: string | null; unassigned?: boolean | null; - limit?: number | null; + /** + * @minimum 1 + * @maximum 200 + */ + limit?: number; + /** + * @minimum 0 + */ + offset?: number; }; diff --git a/frontend/src/api/generated/model/streamBoardMemoryApiV1BoardsBoardIdMemoryStreamGetParams.ts b/frontend/src/api/generated/model/streamBoardMemoryApiV1BoardsBoardIdMemoryStreamGetParams.ts index 4ef48164..a4304ba9 100644 --- a/frontend/src/api/generated/model/streamBoardMemoryApiV1BoardsBoardIdMemoryStreamGetParams.ts +++ b/frontend/src/api/generated/model/streamBoardMemoryApiV1BoardsBoardIdMemoryStreamGetParams.ts @@ -7,4 +7,5 @@ export type StreamBoardMemoryApiV1BoardsBoardIdMemoryStreamGetParams = { since?: string | null; + is_chat?: boolean | null; }; diff --git a/frontend/src/api/generated/model/taskCardRead.ts b/frontend/src/api/generated/model/taskCardRead.ts new file mode 100644 index 00000000..785926ef --- /dev/null +++ b/frontend/src/api/generated/model/taskCardRead.ts @@ -0,0 +1,25 @@ +/** + * Generated by orval v8.2.0 🍺 + * Do not edit manually. + * Mission Control API + * OpenAPI spec version: 0.1.0 + */ +import type { TaskCardReadStatus } from "./taskCardReadStatus"; + +export interface TaskCardRead { + title: string; + description?: string | null; + status?: TaskCardReadStatus; + priority?: string; + due_at?: string | null; + assigned_agent_id?: string | null; + id: string; + board_id: string | null; + created_by_user_id: string | null; + in_progress_at: string | null; + created_at: string; + updated_at: string; + assignee?: string | null; + approvals_count?: number; + approvals_pending_count?: number; +} diff --git a/frontend/src/api/generated/model/taskCardReadStatus.ts b/frontend/src/api/generated/model/taskCardReadStatus.ts new file mode 100644 index 00000000..7fc0bcd6 --- /dev/null +++ b/frontend/src/api/generated/model/taskCardReadStatus.ts @@ -0,0 +1,16 @@ +/** + * Generated by orval v8.2.0 🍺 + * Do not edit manually. + * Mission Control API + * OpenAPI spec version: 0.1.0 + */ + +export type TaskCardReadStatus = + (typeof TaskCardReadStatus)[keyof typeof TaskCardReadStatus]; + +export const TaskCardReadStatus = { + inbox: "inbox", + in_progress: "in_progress", + review: "review", + done: "done", +} as const; diff --git a/frontend/src/api/generated/tasks/tasks.ts b/frontend/src/api/generated/tasks/tasks.ts index 75dbcc39..2dadbf54 100644 --- a/frontend/src/api/generated/tasks/tasks.ts +++ b/frontend/src/api/generated/tasks/tasks.ts @@ -22,6 +22,9 @@ import type { import type { HTTPValidationError, + LimitOffsetPageTypeVarCustomizedTaskCommentRead, + LimitOffsetPageTypeVarCustomizedTaskRead, + ListTaskCommentsApiV1BoardsBoardIdTasksTaskIdCommentsGetParams, ListTasksApiV1BoardsBoardIdTasksGetParams, OkResponse, StreamTasksApiV1BoardsBoardIdTasksStreamGetParams, @@ -292,7 +295,7 @@ export function useStreamTasksApiV1BoardsBoardIdTasksStreamGet< * @summary List Tasks */ export type listTasksApiV1BoardsBoardIdTasksGetResponse200 = { - data: TaskRead[]; + data: LimitOffsetPageTypeVarCustomizedTaskRead; status: 200; }; @@ -900,7 +903,7 @@ export const useDeleteTaskApiV1BoardsBoardIdTasksTaskIdDelete = < */ export type listTaskCommentsApiV1BoardsBoardIdTasksTaskIdCommentsGetResponse200 = { - data: TaskCommentRead[]; + data: LimitOffsetPageTypeVarCustomizedTaskCommentRead; status: 200; }; @@ -926,19 +929,34 @@ export type listTaskCommentsApiV1BoardsBoardIdTasksTaskIdCommentsGetResponse = export const getListTaskCommentsApiV1BoardsBoardIdTasksTaskIdCommentsGetUrl = ( boardId: string, taskId: string, + params?: ListTaskCommentsApiV1BoardsBoardIdTasksTaskIdCommentsGetParams, ) => { - return `/api/v1/boards/${boardId}/tasks/${taskId}/comments`; + const normalizedParams = new URLSearchParams(); + + Object.entries(params || {}).forEach(([key, value]) => { + if (value !== undefined) { + normalizedParams.append(key, value === null ? "null" : value.toString()); + } + }); + + const stringifiedParams = normalizedParams.toString(); + + return stringifiedParams.length > 0 + ? `/api/v1/boards/${boardId}/tasks/${taskId}/comments?${stringifiedParams}` + : `/api/v1/boards/${boardId}/tasks/${taskId}/comments`; }; export const listTaskCommentsApiV1BoardsBoardIdTasksTaskIdCommentsGet = async ( boardId: string, taskId: string, + params?: ListTaskCommentsApiV1BoardsBoardIdTasksTaskIdCommentsGetParams, options?: RequestInit, ): Promise => { return customFetch( getListTaskCommentsApiV1BoardsBoardIdTasksTaskIdCommentsGetUrl( boardId, taskId, + params, ), { ...options, @@ -948,8 +966,15 @@ export const listTaskCommentsApiV1BoardsBoardIdTasksTaskIdCommentsGet = async ( }; export const getListTaskCommentsApiV1BoardsBoardIdTasksTaskIdCommentsGetQueryKey = - (boardId: string, taskId: string) => { - return [`/api/v1/boards/${boardId}/tasks/${taskId}/comments`] as const; + ( + boardId: string, + taskId: string, + params?: ListTaskCommentsApiV1BoardsBoardIdTasksTaskIdCommentsGetParams, + ) => { + return [ + `/api/v1/boards/${boardId}/tasks/${taskId}/comments`, + ...(params ? [params] : []), + ] as const; }; export const getListTaskCommentsApiV1BoardsBoardIdTasksTaskIdCommentsGetQueryOptions = @@ -963,6 +988,7 @@ export const getListTaskCommentsApiV1BoardsBoardIdTasksTaskIdCommentsGetQueryOpt >( boardId: string, taskId: string, + params?: ListTaskCommentsApiV1BoardsBoardIdTasksTaskIdCommentsGetParams, options?: { query?: Partial< UseQueryOptions< @@ -985,6 +1011,7 @@ export const getListTaskCommentsApiV1BoardsBoardIdTasksTaskIdCommentsGetQueryOpt getListTaskCommentsApiV1BoardsBoardIdTasksTaskIdCommentsGetQueryKey( boardId, taskId, + params, ); const queryFn: QueryFunction< @@ -997,6 +1024,7 @@ export const getListTaskCommentsApiV1BoardsBoardIdTasksTaskIdCommentsGetQueryOpt listTaskCommentsApiV1BoardsBoardIdTasksTaskIdCommentsGet( boardId, taskId, + params, { signal, ...requestOptions }, ); @@ -1035,6 +1063,9 @@ export function useListTaskCommentsApiV1BoardsBoardIdTasksTaskIdCommentsGet< >( boardId: string, taskId: string, + params: + | undefined + | ListTaskCommentsApiV1BoardsBoardIdTasksTaskIdCommentsGetParams, options: { query: Partial< UseQueryOptions< @@ -1077,6 +1108,7 @@ export function useListTaskCommentsApiV1BoardsBoardIdTasksTaskIdCommentsGet< >( boardId: string, taskId: string, + params?: ListTaskCommentsApiV1BoardsBoardIdTasksTaskIdCommentsGetParams, options?: { query?: Partial< UseQueryOptions< @@ -1119,6 +1151,7 @@ export function useListTaskCommentsApiV1BoardsBoardIdTasksTaskIdCommentsGet< >( boardId: string, taskId: string, + params?: ListTaskCommentsApiV1BoardsBoardIdTasksTaskIdCommentsGetParams, options?: { query?: Partial< UseQueryOptions< @@ -1149,6 +1182,7 @@ export function useListTaskCommentsApiV1BoardsBoardIdTasksTaskIdCommentsGet< >( boardId: string, taskId: string, + params?: ListTaskCommentsApiV1BoardsBoardIdTasksTaskIdCommentsGetParams, options?: { query?: Partial< UseQueryOptions< @@ -1171,6 +1205,7 @@ export function useListTaskCommentsApiV1BoardsBoardIdTasksTaskIdCommentsGet< getListTaskCommentsApiV1BoardsBoardIdTasksTaskIdCommentsGetQueryOptions( boardId, taskId, + params, options, ); diff --git a/frontend/src/app/agents/[agentId]/edit/page.tsx b/frontend/src/app/agents/[agentId]/edit/page.tsx index 83f7be96..377a775c 100644 --- a/frontend/src/app/agents/[agentId]/edit/page.tsx +++ b/frontend/src/app/agents/[agentId]/edit/page.tsx @@ -116,7 +116,7 @@ export default function EditAgentPage() { const boardsQuery = useListBoardsApiV1BoardsGet< listBoardsApiV1BoardsGetResponse, ApiError - >({ + >(undefined, { query: { enabled: Boolean(isSignedIn), refetchOnMount: "always", @@ -148,7 +148,8 @@ export default function EditAgentPage() { }, }); - const boards = boardsQuery.data?.status === 200 ? boardsQuery.data.data : []; + const boards = + boardsQuery.data?.status === 200 ? boardsQuery.data.data.items ?? [] : []; const loadedAgent: AgentRead | null = agentQuery.data?.status === 200 ? agentQuery.data.data : null; diff --git a/frontend/src/app/agents/[agentId]/page.tsx b/frontend/src/app/agents/[agentId]/page.tsx index 1cdf1989..29706e31 100644 --- a/frontend/src/app/agents/[agentId]/page.tsx +++ b/frontend/src/app/agents/[agentId]/page.tsx @@ -106,7 +106,7 @@ export default function AgentDetailPage() { const boardsQuery = useListBoardsApiV1BoardsGet< listBoardsApiV1BoardsGetResponse, ApiError - >({ + >(undefined, { query: { enabled: Boolean(isSignedIn), refetchInterval: 60_000, @@ -118,9 +118,11 @@ export default function AgentDetailPage() { const agent: AgentRead | null = agentQuery.data?.status === 200 ? agentQuery.data.data : null; const events: ActivityEventRead[] = - activityQuery.data?.status === 200 ? activityQuery.data.data : []; + activityQuery.data?.status === 200 + ? activityQuery.data.data.items ?? [] + : []; const boards: BoardRead[] = - boardsQuery.data?.status === 200 ? boardsQuery.data.data : []; + boardsQuery.data?.status === 200 ? boardsQuery.data.data.items ?? [] : []; const agentEvents = useMemo(() => { if (!agent) return []; diff --git a/frontend/src/app/agents/new/page.tsx b/frontend/src/app/agents/new/page.tsx index e8b1489d..92fec06f 100644 --- a/frontend/src/app/agents/new/page.tsx +++ b/frontend/src/app/agents/new/page.tsx @@ -91,7 +91,7 @@ export default function NewAgentPage() { const boardsQuery = useListBoardsApiV1BoardsGet< listBoardsApiV1BoardsGetResponse, ApiError - >({ + >(undefined, { query: { enabled: Boolean(isSignedIn), refetchOnMount: "always", @@ -111,7 +111,8 @@ export default function NewAgentPage() { }, }); - const boards = boardsQuery.data?.status === 200 ? boardsQuery.data.data : []; + const boards = + boardsQuery.data?.status === 200 ? boardsQuery.data.data.items ?? [] : []; const displayBoardId = boardId || boards[0]?.id || ""; const isLoading = boardsQuery.isLoading || createAgentMutation.isPending; const errorMessage = error ?? boardsQuery.error?.message ?? null; diff --git a/frontend/src/app/agents/page.tsx b/frontend/src/app/agents/page.tsx index f4694928..ea5cb450 100644 --- a/frontend/src/app/agents/page.tsx +++ b/frontend/src/app/agents/page.tsx @@ -98,7 +98,7 @@ export default function AgentsPage() { const boardsQuery = useListBoardsApiV1BoardsGet< listBoardsApiV1BoardsGetResponse, ApiError - >({ + >(undefined, { query: { enabled: Boolean(isSignedIn), refetchInterval: 30_000, @@ -109,7 +109,7 @@ export default function AgentsPage() { const agentsQuery = useListAgentsApiV1AgentsGet< listAgentsApiV1AgentsGetResponse, ApiError - >({ + >(undefined, { query: { enabled: Boolean(isSignedIn), refetchInterval: 15_000, @@ -118,10 +118,15 @@ export default function AgentsPage() { }); const boards = useMemo( - () => (boardsQuery.data?.status === 200 ? boardsQuery.data.data : []), + () => + boardsQuery.data?.status === 200 ? boardsQuery.data.data.items ?? [] : [], [boardsQuery.data] ); - const agents = useMemo(() => agentsQuery.data?.data ?? [], [agentsQuery.data]); + const agents = useMemo( + () => + agentsQuery.data?.status === 200 ? agentsQuery.data.data.items ?? [] : [], + [agentsQuery.data] + ); const deleteMutation = useDeleteAgentApiV1AgentsAgentIdDelete< ApiError, @@ -133,10 +138,18 @@ export default function AgentsPage() { await queryClient.cancelQueries({ queryKey: agentsKey }); const previous = queryClient.getQueryData(agentsKey); - if (previous) { + if (previous && previous.status === 200) { + const nextItems = previous.data.items.filter( + (agent) => agent.id !== agentId + ); + const removedCount = previous.data.items.length - nextItems.length; queryClient.setQueryData(agentsKey, { ...previous, - data: previous.data.filter((agent) => agent.id !== agentId), + data: { + ...previous.data, + items: nextItems, + total: Math.max(0, previous.data.total - removedCount), + }, }); } return { previous }; diff --git a/frontend/src/app/boards/[boardId]/edit/page.tsx b/frontend/src/app/boards/[boardId]/edit/page.tsx index 9528b4c0..c77ff7c2 100644 --- a/frontend/src/app/boards/[boardId]/edit/page.tsx +++ b/frontend/src/app/boards/[boardId]/edit/page.tsx @@ -16,8 +16,6 @@ import { useListGatewaysApiV1GatewaysGet, } from "@/api/generated/gateways/gateways"; import type { BoardRead, BoardUpdate } from "@/api/generated/model"; -import { BoardApprovalsPanel } from "@/components/BoardApprovalsPanel"; -import { BoardGoalPanel } from "@/components/BoardGoalPanel"; import { BoardOnboardingChat } from "@/components/BoardOnboardingChat"; import { DashboardSidebar } from "@/components/organisms/DashboardSidebar"; import { DashboardShell } from "@/components/templates/DashboardShell"; @@ -72,7 +70,7 @@ export default function EditBoardPage() { const gatewaysQuery = useListGatewaysApiV1GatewaysGet< listGatewaysApiV1GatewaysGetResponse, ApiError - >({ + >(undefined, { query: { enabled: Boolean(isSignedIn), refetchOnMount: "always", @@ -105,7 +103,9 @@ export default function EditBoardPage() { }); const gateways = - gatewaysQuery.data?.status === 200 ? gatewaysQuery.data.data : []; + gatewaysQuery.data?.status === 200 + ? gatewaysQuery.data.data.items ?? [] + : []; const loadedBoard: BoardRead | null = boardQuery.data?.status === 200 ? boardQuery.data.data : null; const baseBoard = board ?? loadedBoard; @@ -224,135 +224,149 @@ export default function EditBoardPage() {
-
-
- setIsOnboardingOpen(true)} - /> -
-
-
- - setName(event.target.value)} - placeholder="Board name" - disabled={isLoading || !baseBoard} - /> +
+ + {resolvedBoardType !== "general" && + baseBoard && + !(baseBoard.goal_confirmed ?? false) ? ( +
+
+

+ Goal needs confirmation +

+

+ Start onboarding to draft an objective and success + metrics. +

-
- - -
-
- -
-
- - -
-
- - setTargetDate(event.target.value)} - disabled={isLoading} - /> -
-
- -
- -