feat: add validation for minimum length on various fields and update type definitions

This commit is contained in:
Abhimanyu Saharan
2026-02-06 16:12:04 +05:30
parent ca614328ac
commit d86fe0a7a6
157 changed files with 12340 additions and 2977 deletions

View File

@@ -2,7 +2,8 @@ from __future__ import annotations
from fastapi import APIRouter, Depends, Query from fastapi import APIRouter, Depends, Query
from sqlalchemy import desc from sqlalchemy import desc
from sqlmodel import Session, col, select from sqlmodel import col, select
from sqlmodel.ext.asyncio.session import AsyncSession
from app.api.deps import ActorContext, require_admin_or_agent from app.api.deps import ActorContext, require_admin_or_agent
from app.db.session import get_session from app.db.session import get_session
@@ -13,14 +14,14 @@ router = APIRouter(prefix="/activity", tags=["activity"])
@router.get("", response_model=list[ActivityEventRead]) @router.get("", response_model=list[ActivityEventRead])
def list_activity( async def list_activity(
limit: int = Query(50, ge=1, le=200), limit: int = Query(50, ge=1, le=200),
offset: int = Query(0, ge=0), offset: int = Query(0, ge=0),
session: Session = Depends(get_session), session: AsyncSession = Depends(get_session),
actor: ActorContext = Depends(require_admin_or_agent), actor: ActorContext = Depends(require_admin_or_agent),
) -> list[ActivityEvent]: ) -> list[ActivityEvent]:
statement = select(ActivityEvent) statement = select(ActivityEvent)
if actor.actor_type == "agent" and actor.agent: if actor.actor_type == "agent" and actor.agent:
statement = statement.where(ActivityEvent.agent_id == actor.agent.id) statement = statement.where(ActivityEvent.agent_id == actor.agent.id)
statement = statement.order_by(desc(col(ActivityEvent.created_at))).offset(offset).limit(limit) statement = statement.order_by(desc(col(ActivityEvent.created_at))).offset(offset).limit(limit)
return list(session.exec(statement)) return list(await session.exec(statement))

View File

@@ -1,10 +1,10 @@
from __future__ import annotations from __future__ import annotations
import asyncio
from uuid import UUID from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Query, status from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlmodel import Session, select from sqlmodel import select
from sqlmodel.ext.asyncio.session import AsyncSession
from app.api import agents as agents_api from app.api import agents as agents_api
from app.api import approvals as approvals_api from app.api import approvals as approvals_api
@@ -16,15 +16,20 @@ from app.core.agent_auth import AgentAuthContext, get_agent_auth_context
from app.db.session import get_session from app.db.session import get_session
from app.integrations.openclaw_gateway import GatewayConfig as GatewayClientConfig from app.integrations.openclaw_gateway import GatewayConfig as GatewayClientConfig
from app.integrations.openclaw_gateway import OpenClawGatewayError, ensure_session, send_message 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.agents import Agent
from app.models.approvals import Approval
from app.models.board_memory import BoardMemory
from app.models.board_onboarding import BoardOnboardingSession
from app.models.boards import Board from app.models.boards import Board
from app.models.gateways import Gateway from app.models.gateways import Gateway
from app.models.tasks import Task from app.models.tasks import Task
from app.schemas.agents import AgentCreate, AgentHeartbeat, AgentHeartbeatCreate, AgentNudge, AgentRead from app.schemas.agents import AgentCreate, AgentHeartbeat, AgentHeartbeatCreate, AgentNudge, AgentRead
from app.schemas.approvals import ApprovalCreate, ApprovalRead from app.schemas.approvals import ApprovalCreate, ApprovalRead, ApprovalStatus
from app.schemas.board_memory import BoardMemoryCreate, BoardMemoryRead from app.schemas.board_memory import BoardMemoryCreate, BoardMemoryRead
from app.schemas.board_onboarding import BoardOnboardingRead from app.schemas.board_onboarding import BoardOnboardingAgentUpdate, BoardOnboardingRead
from app.schemas.boards import BoardRead from app.schemas.boards import BoardRead
from app.schemas.common import OkResponse
from app.schemas.tasks import TaskCommentCreate, TaskCommentRead, TaskCreate, TaskRead, TaskUpdate from app.schemas.tasks import TaskCommentCreate, TaskCommentRead, TaskCreate, TaskRead, TaskUpdate
from app.services.activity_log import record_activity from app.services.activity_log import record_activity
@@ -40,24 +45,24 @@ def _guard_board_access(agent_ctx: AgentAuthContext, board: Board) -> None:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
def _gateway_config(session: Session, board: Board) -> GatewayClientConfig: async def _gateway_config(session: AsyncSession, board: Board) -> GatewayClientConfig:
if not board.gateway_id: if not board.gateway_id:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY) raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
gateway = session.get(Gateway, board.gateway_id) gateway = await session.get(Gateway, board.gateway_id)
if gateway is None or not gateway.url: if gateway is None or not gateway.url:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY) raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
return GatewayClientConfig(url=gateway.url, token=gateway.token) return GatewayClientConfig(url=gateway.url, token=gateway.token)
@router.get("/boards", response_model=list[BoardRead]) @router.get("/boards", response_model=list[BoardRead])
def list_boards( async def list_boards(
session: Session = Depends(get_session), session: AsyncSession = Depends(get_session),
agent_ctx: AgentAuthContext = Depends(get_agent_auth_context), agent_ctx: AgentAuthContext = Depends(get_agent_auth_context),
) -> list[Board]: ) -> list[Board]:
if agent_ctx.agent.board_id: if agent_ctx.agent.board_id:
board = session.get(Board, agent_ctx.agent.board_id) board = await session.get(Board, agent_ctx.agent.board_id)
return [board] if board else [] return [board] if board else []
return list(session.exec(select(Board))) return list(await session.exec(select(Board)))
@router.get("/boards/{board_id}", response_model=BoardRead) @router.get("/boards/{board_id}", response_model=BoardRead)
@@ -70,10 +75,10 @@ def get_board(
@router.get("/agents", response_model=list[AgentRead]) @router.get("/agents", response_model=list[AgentRead])
def list_agents( async def list_agents(
board_id: UUID | None = Query(default=None), board_id: UUID | None = Query(default=None),
limit: int | None = Query(default=None, ge=1, le=200), limit: int | None = Query(default=None, ge=1, le=200),
session: Session = Depends(get_session), session: AsyncSession = Depends(get_session),
agent_ctx: AgentAuthContext = Depends(get_agent_auth_context), agent_ctx: AgentAuthContext = Depends(get_agent_auth_context),
) -> list[AgentRead]: ) -> list[AgentRead]:
statement = select(Agent) statement = select(Agent)
@@ -85,8 +90,8 @@ def list_agents(
statement = statement.where(Agent.board_id == board_id) statement = statement.where(Agent.board_id == board_id)
if limit is not None: if limit is not None:
statement = statement.limit(limit) statement = statement.limit(limit)
agents = list(session.exec(statement)) agents = list(await session.exec(statement))
main_session_keys = agents_api._get_gateway_main_session_keys(session) main_session_keys = await agents_api._get_gateway_main_session_keys(session)
return [ return [
agents_api._to_agent_read(agents_api._with_computed_status(agent), main_session_keys) agents_api._to_agent_read(agents_api._with_computed_status(agent), main_session_keys)
for agent in agents for agent in agents
@@ -94,17 +99,17 @@ def list_agents(
@router.get("/boards/{board_id}/tasks", response_model=list[TaskRead]) @router.get("/boards/{board_id}/tasks", response_model=list[TaskRead])
def list_tasks( async def list_tasks(
status_filter: str | None = Query(default=None, alias="status"), status_filter: str | None = Query(default=None, alias="status"),
assigned_agent_id: UUID | None = None, assigned_agent_id: UUID | None = None,
unassigned: bool | None = None, unassigned: bool | None = None,
limit: int | None = Query(default=None, ge=1, le=200), limit: int | None = Query(default=None, ge=1, le=200),
board: Board = Depends(get_board_or_404), board: Board = Depends(get_board_or_404),
session: Session = Depends(get_session), session: AsyncSession = Depends(get_session),
agent_ctx: AgentAuthContext = Depends(get_agent_auth_context), agent_ctx: AgentAuthContext = Depends(get_agent_auth_context),
) -> list[TaskRead]: ) -> list[Task]:
_guard_board_access(agent_ctx, board) _guard_board_access(agent_ctx, board)
return tasks_api.list_tasks( return await tasks_api.list_tasks(
status_filter=status_filter, status_filter=status_filter,
assigned_agent_id=assigned_agent_id, assigned_agent_id=assigned_agent_id,
unassigned=unassigned, unassigned=unassigned,
@@ -116,22 +121,21 @@ def list_tasks(
@router.post("/boards/{board_id}/tasks", response_model=TaskRead) @router.post("/boards/{board_id}/tasks", response_model=TaskRead)
def create_task( async def create_task(
payload: TaskCreate, payload: TaskCreate,
board: Board = Depends(get_board_or_404), board: Board = Depends(get_board_or_404),
session: Session = Depends(get_session), session: AsyncSession = Depends(get_session),
agent_ctx: AgentAuthContext = Depends(get_agent_auth_context), agent_ctx: AgentAuthContext = Depends(get_agent_auth_context),
) -> TaskRead: ) -> Task:
_guard_board_access(agent_ctx, board) _guard_board_access(agent_ctx, board)
if not agent_ctx.agent.is_board_lead: if not agent_ctx.agent.is_board_lead:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
tasks_api.validate_task_status(payload.status)
task = Task.model_validate(payload) task = Task.model_validate(payload)
task.board_id = board.id task.board_id = board.id
task.auto_created = True task.auto_created = True
task.auto_reason = f"lead_agent:{agent_ctx.agent.id}" task.auto_reason = f"lead_agent:{agent_ctx.agent.id}"
if task.assigned_agent_id: if task.assigned_agent_id:
agent = session.get(Agent, task.assigned_agent_id) agent = await session.get(Agent, task.assigned_agent_id)
if agent is None: if agent is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
if agent.is_board_lead: if agent.is_board_lead:
@@ -142,8 +146,8 @@ def create_task(
if agent.board_id and agent.board_id != board.id: if agent.board_id and agent.board_id != board.id:
raise HTTPException(status_code=status.HTTP_409_CONFLICT) raise HTTPException(status_code=status.HTTP_409_CONFLICT)
session.add(task) session.add(task)
session.commit() await session.commit()
session.refresh(task) await session.refresh(task)
record_activity( record_activity(
session, session,
event_type="task.created", event_type="task.created",
@@ -151,11 +155,11 @@ def create_task(
message=f"Task created by lead: {task.title}.", message=f"Task created by lead: {task.title}.",
agent_id=agent_ctx.agent.id, agent_id=agent_ctx.agent.id,
) )
session.commit() await session.commit()
if task.assigned_agent_id: if task.assigned_agent_id:
assigned_agent = session.get(Agent, task.assigned_agent_id) assigned_agent = await session.get(Agent, task.assigned_agent_id)
if assigned_agent: if assigned_agent:
tasks_api._notify_agent_on_task_assign( await tasks_api._notify_agent_on_task_assign(
session=session, session=session,
board=board, board=board,
task=task, task=task,
@@ -165,15 +169,15 @@ def create_task(
@router.patch("/boards/{board_id}/tasks/{task_id}", response_model=TaskRead) @router.patch("/boards/{board_id}/tasks/{task_id}", response_model=TaskRead)
def update_task( async def update_task(
payload: TaskUpdate, payload: TaskUpdate,
task=Depends(get_task_or_404), task: Task = Depends(get_task_or_404),
session: Session = Depends(get_session), session: AsyncSession = Depends(get_session),
agent_ctx: AgentAuthContext = Depends(get_agent_auth_context), agent_ctx: AgentAuthContext = Depends(get_agent_auth_context),
) -> TaskRead: ) -> Task:
if agent_ctx.agent.board_id and task.board_id and agent_ctx.agent.board_id != task.board_id: 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) raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
return tasks_api.update_task( return await tasks_api.update_task(
payload=payload, payload=payload,
task=task, task=task,
session=session, session=session,
@@ -182,14 +186,14 @@ 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=list[TaskCommentRead])
def list_task_comments( async def list_task_comments(
task=Depends(get_task_or_404), task: Task = Depends(get_task_or_404),
session: Session = Depends(get_session), session: AsyncSession = Depends(get_session),
agent_ctx: AgentAuthContext = Depends(get_agent_auth_context), agent_ctx: AgentAuthContext = Depends(get_agent_auth_context),
) -> list[TaskCommentRead]: ) -> list[ActivityEvent]:
if agent_ctx.agent.board_id and task.board_id and agent_ctx.agent.board_id != task.board_id: 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) raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
return tasks_api.list_task_comments( return await tasks_api.list_task_comments(
task=task, task=task,
session=session, session=session,
actor=_actor(agent_ctx), actor=_actor(agent_ctx),
@@ -197,15 +201,15 @@ def list_task_comments(
@router.post("/boards/{board_id}/tasks/{task_id}/comments", response_model=TaskCommentRead) @router.post("/boards/{board_id}/tasks/{task_id}/comments", response_model=TaskCommentRead)
def create_task_comment( async def create_task_comment(
payload: TaskCommentCreate, payload: TaskCommentCreate,
task=Depends(get_task_or_404), task: Task = Depends(get_task_or_404),
session: Session = Depends(get_session), session: AsyncSession = Depends(get_session),
agent_ctx: AgentAuthContext = Depends(get_agent_auth_context), agent_ctx: AgentAuthContext = Depends(get_agent_auth_context),
) -> TaskCommentRead: ) -> ActivityEvent:
if agent_ctx.agent.board_id and task.board_id and agent_ctx.agent.board_id != task.board_id: 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) raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
return tasks_api.create_task_comment( return await tasks_api.create_task_comment(
payload=payload, payload=payload,
task=task, task=task,
session=session, session=session,
@@ -214,15 +218,15 @@ def create_task_comment(
@router.get("/boards/{board_id}/memory", response_model=list[BoardMemoryRead]) @router.get("/boards/{board_id}/memory", response_model=list[BoardMemoryRead])
def list_board_memory( async def list_board_memory(
limit: int = Query(default=50, ge=1, le=200), limit: int = Query(default=50, ge=1, le=200),
offset: int = Query(default=0, ge=0), offset: int = Query(default=0, ge=0),
board=Depends(get_board_or_404), board: Board = Depends(get_board_or_404),
session: Session = Depends(get_session), session: AsyncSession = Depends(get_session),
agent_ctx: AgentAuthContext = Depends(get_agent_auth_context), agent_ctx: AgentAuthContext = Depends(get_agent_auth_context),
) -> list[BoardMemoryRead]: ) -> list[BoardMemory]:
_guard_board_access(agent_ctx, board) _guard_board_access(agent_ctx, board)
return board_memory_api.list_board_memory( return await board_memory_api.list_board_memory(
limit=limit, limit=limit,
offset=offset, offset=offset,
board=board, board=board,
@@ -232,14 +236,14 @@ def list_board_memory(
@router.post("/boards/{board_id}/memory", response_model=BoardMemoryRead) @router.post("/boards/{board_id}/memory", response_model=BoardMemoryRead)
def create_board_memory( async def create_board_memory(
payload: BoardMemoryCreate, payload: BoardMemoryCreate,
board=Depends(get_board_or_404), board: Board = Depends(get_board_or_404),
session: Session = Depends(get_session), session: AsyncSession = Depends(get_session),
agent_ctx: AgentAuthContext = Depends(get_agent_auth_context), agent_ctx: AgentAuthContext = Depends(get_agent_auth_context),
) -> BoardMemoryRead: ) -> BoardMemory:
_guard_board_access(agent_ctx, board) _guard_board_access(agent_ctx, board)
return board_memory_api.create_board_memory( return await board_memory_api.create_board_memory(
payload=payload, payload=payload,
board=board, board=board,
session=session, session=session,
@@ -248,14 +252,14 @@ def create_board_memory(
@router.get("/boards/{board_id}/approvals", response_model=list[ApprovalRead]) @router.get("/boards/{board_id}/approvals", response_model=list[ApprovalRead])
def list_approvals( async def list_approvals(
status_filter: str | None = Query(default=None, alias="status"), status_filter: ApprovalStatus | None = Query(default=None, alias="status"),
board=Depends(get_board_or_404), board: Board = Depends(get_board_or_404),
session: Session = Depends(get_session), session: AsyncSession = Depends(get_session),
agent_ctx: AgentAuthContext = Depends(get_agent_auth_context), agent_ctx: AgentAuthContext = Depends(get_agent_auth_context),
) -> list[ApprovalRead]: ) -> list[Approval]:
_guard_board_access(agent_ctx, board) _guard_board_access(agent_ctx, board)
return approvals_api.list_approvals( return await approvals_api.list_approvals(
status_filter=status_filter, status_filter=status_filter,
board=board, board=board,
session=session, session=session,
@@ -264,14 +268,14 @@ def list_approvals(
@router.post("/boards/{board_id}/approvals", response_model=ApprovalRead) @router.post("/boards/{board_id}/approvals", response_model=ApprovalRead)
def create_approval( async def create_approval(
payload: ApprovalCreate, payload: ApprovalCreate,
board=Depends(get_board_or_404), board: Board = Depends(get_board_or_404),
session: Session = Depends(get_session), session: AsyncSession = Depends(get_session),
agent_ctx: AgentAuthContext = Depends(get_agent_auth_context), agent_ctx: AgentAuthContext = Depends(get_agent_auth_context),
) -> ApprovalRead: ) -> Approval:
_guard_board_access(agent_ctx, board) _guard_board_access(agent_ctx, board)
return approvals_api.create_approval( return await approvals_api.create_approval(
payload=payload, payload=payload,
board=board, board=board,
session=session, session=session,
@@ -280,14 +284,14 @@ def create_approval(
@router.post("/boards/{board_id}/onboarding", response_model=BoardOnboardingRead) @router.post("/boards/{board_id}/onboarding", response_model=BoardOnboardingRead)
def update_onboarding( async def update_onboarding(
payload: dict[str, object], payload: BoardOnboardingAgentUpdate,
board: Board = Depends(get_board_or_404), board: Board = Depends(get_board_or_404),
session: Session = Depends(get_session), session: AsyncSession = Depends(get_session),
agent_ctx: AgentAuthContext = Depends(get_agent_auth_context), agent_ctx: AgentAuthContext = Depends(get_agent_auth_context),
) -> BoardOnboardingRead: ) -> BoardOnboardingSession:
_guard_board_access(agent_ctx, board) _guard_board_access(agent_ctx, board)
return onboarding_api.agent_onboarding_update( return await onboarding_api.agent_onboarding_update(
payload=payload, payload=payload,
board=board, board=board,
session=session, session=session,
@@ -298,7 +302,7 @@ def update_onboarding(
@router.post("/agents", response_model=AgentRead) @router.post("/agents", response_model=AgentRead)
async def create_agent( async def create_agent(
payload: AgentCreate, payload: AgentCreate,
session: Session = Depends(get_session), session: AsyncSession = Depends(get_session),
agent_ctx: AgentAuthContext = Depends(get_agent_auth_context), agent_ctx: AgentAuthContext = Depends(get_agent_auth_context),
) -> AgentRead: ) -> AgentRead:
if not agent_ctx.agent.is_board_lead: if not agent_ctx.agent.is_board_lead:
@@ -313,18 +317,18 @@ async def create_agent(
) )
@router.post("/boards/{board_id}/agents/{agent_id}/nudge") @router.post("/boards/{board_id}/agents/{agent_id}/nudge", response_model=OkResponse)
def nudge_agent( async def nudge_agent(
payload: AgentNudge, payload: AgentNudge,
agent_id: str, agent_id: str,
board: Board = Depends(get_board_or_404), board: Board = Depends(get_board_or_404),
session: Session = Depends(get_session), session: AsyncSession = Depends(get_session),
agent_ctx: AgentAuthContext = Depends(get_agent_auth_context), agent_ctx: AgentAuthContext = Depends(get_agent_auth_context),
) -> dict[str, bool]: ) -> OkResponse:
_guard_board_access(agent_ctx, board) _guard_board_access(agent_ctx, board)
if not agent_ctx.agent.is_board_lead: if not agent_ctx.agent.is_board_lead:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
target = session.get(Agent, agent_id) target = await session.get(Agent, agent_id)
if target is None or (target.board_id and target.board_id != board.id): if target is None or (target.board_id and target.board_id != board.id):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
if not target.openclaw_session_id: if not target.openclaw_session_id:
@@ -332,15 +336,9 @@ def nudge_agent(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Target agent has no session key", detail="Target agent has no session key",
) )
message = payload.message.strip() message = payload.message
if not message: config = await _gateway_config(session, board)
raise HTTPException( try:
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="message is required",
)
config = _gateway_config(session, board)
async def _send() -> None:
await ensure_session(target.openclaw_session_id, config=config, label=target.name) await ensure_session(target.openclaw_session_id, config=config, label=target.name)
await send_message( await send_message(
message, message,
@@ -348,9 +346,6 @@ def nudge_agent(
config=config, config=config,
deliver=True, deliver=True,
) )
try:
asyncio.run(_send())
except OpenClawGatewayError as exc: except OpenClawGatewayError as exc:
record_activity( record_activity(
session, session,
@@ -358,7 +353,7 @@ def nudge_agent(
message=f"Nudge failed for {target.name}: {exc}", message=f"Nudge failed for {target.name}: {exc}",
agent_id=agent_ctx.agent.id, agent_id=agent_ctx.agent.id,
) )
session.commit() await session.commit()
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(exc)) from exc raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(exc)) from exc
record_activity( record_activity(
session, session,
@@ -366,18 +361,18 @@ def nudge_agent(
message=f"Nudge sent to {target.name}.", message=f"Nudge sent to {target.name}.",
agent_id=agent_ctx.agent.id, agent_id=agent_ctx.agent.id,
) )
session.commit() await session.commit()
return {"ok": True} return OkResponse()
@router.post("/heartbeat", response_model=AgentRead) @router.post("/heartbeat", response_model=AgentRead)
async def agent_heartbeat( async def agent_heartbeat(
payload: AgentHeartbeatCreate, payload: AgentHeartbeatCreate,
session: Session = Depends(get_session), session: AsyncSession = Depends(get_session),
agent_ctx: AgentAuthContext = Depends(get_agent_auth_context), agent_ctx: AgentAuthContext = Depends(get_agent_auth_context),
) -> AgentRead: ) -> AgentRead:
# Heartbeats must apply to the authenticated agent; agent names are not unique. # Heartbeats must apply to the authenticated agent; agent names are not unique.
return agents_api.heartbeat_agent( # type: ignore[attr-defined] return await agents_api.heartbeat_agent(
agent_id=str(agent_ctx.agent.id), agent_id=str(agent_ctx.agent.id),
payload=AgentHeartbeat(status=payload.status), payload=AgentHeartbeat(status=payload.status),
session=session, session=session,

View File

@@ -3,19 +3,21 @@ from __future__ import annotations
import asyncio import asyncio
import json import json
import re import re
from collections.abc import AsyncIterator
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from uuid import UUID, uuid4 from uuid import UUID, uuid4
from fastapi import APIRouter, Depends, HTTPException, Query, Request, status from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
from sqlalchemy import asc, or_, update from sqlalchemy import asc, or_, update
from sqlmodel import Session, col, select from sqlmodel import col, select
from sqlmodel.ext.asyncio.session import AsyncSession
from sse_starlette.sse import EventSourceResponse from sse_starlette.sse import EventSourceResponse
from starlette.concurrency import run_in_threadpool
from app.api.deps import ActorContext, require_admin_auth, require_admin_or_agent from app.api.deps import ActorContext, require_admin_auth, require_admin_or_agent
from app.core.agent_tokens import generate_agent_token, hash_agent_token from app.core.agent_tokens import generate_agent_token, hash_agent_token
from app.core.auth import AuthContext from app.core.auth import AuthContext
from app.db.session import engine, get_session from app.core.time import utcnow
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 GatewayConfig as GatewayClientConfig
from app.integrations.openclaw_gateway import OpenClawGatewayError, ensure_session, send_message from app.integrations.openclaw_gateway import OpenClawGatewayError, ensure_session, send_message
from app.models.activity_events import ActivityEvent from app.models.activity_events import ActivityEvent
@@ -23,6 +25,7 @@ from app.models.agents import Agent
from app.models.boards import Board from app.models.boards import Board
from app.models.gateways import Gateway from app.models.gateways import Gateway
from app.models.tasks import Task from app.models.tasks import Task
from app.schemas.common import OkResponse
from app.schemas.agents import ( from app.schemas.agents import (
AgentCreate, AgentCreate,
AgentHeartbeat, AgentHeartbeat,
@@ -60,27 +63,6 @@ def _parse_since(value: str | None) -> datetime | None:
return parsed return parsed
def _normalize_identity_profile(
profile: dict[str, object] | None,
) -> dict[str, str] | None:
if not profile:
return None
normalized: dict[str, str] = {}
for key, raw in profile.items():
if raw is None:
continue
if isinstance(raw, list):
parts = [str(item).strip() for item in raw if str(item).strip()]
if not parts:
continue
normalized[key] = ", ".join(parts)
continue
value = str(raw).strip()
if value:
normalized[key] = value
return normalized or None
def _slugify(value: str) -> str: def _slugify(value: str) -> str:
slug = re.sub(r"[^a-z0-9]+", "-", value.lower()).strip("-") slug = re.sub(r"[^a-z0-9]+", "-", value.lower()).strip("-")
return slug or uuid4().hex return slug or uuid4().hex
@@ -100,25 +82,25 @@ def _workspace_path(agent_name: str, workspace_root: str | None) -> str:
return f"{root}/workspace-{_slugify(agent_name)}" return f"{root}/workspace-{_slugify(agent_name)}"
def _require_board(session: Session, board_id: UUID | str | None) -> Board: async def _require_board(session: AsyncSession, board_id: UUID | str | None) -> Board:
if not board_id: if not board_id:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="board_id is required", detail="board_id is required",
) )
board = session.get(Board, board_id) board = await session.get(Board, board_id)
if board is None: if board is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Board not found") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Board not found")
return board return board
def _require_gateway(session: Session, board: Board) -> tuple[Gateway, GatewayClientConfig]: async def _require_gateway(session: AsyncSession, board: Board) -> tuple[Gateway, GatewayClientConfig]:
if not board.gateway_id: if not board.gateway_id:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Board gateway_id is required", detail="Board gateway_id is required",
) )
gateway = session.get(Gateway, board.gateway_id) gateway = await session.get(Gateway, board.gateway_id)
if gateway is None: if gateway is None:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
@@ -151,8 +133,8 @@ def _gateway_client_config(gateway: Gateway) -> GatewayClientConfig:
return GatewayClientConfig(url=gateway.url, token=gateway.token) return GatewayClientConfig(url=gateway.url, token=gateway.token)
def _get_gateway_main_session_keys(session: Session) -> set[str]: async def _get_gateway_main_session_keys(session: AsyncSession) -> set[str]:
keys = session.exec(select(Gateway.main_session_key)).all() keys = (await session.exec(select(Gateway.main_session_key))).all()
return {key for key in keys if key} return {key for key in keys if key}
@@ -165,10 +147,12 @@ def _to_agent_read(agent: Agent, main_session_keys: set[str]) -> AgentRead:
return model.model_copy(update={"is_gateway_main": _is_gateway_main(agent, main_session_keys)}) return model.model_copy(update={"is_gateway_main": _is_gateway_main(agent, main_session_keys)})
def _find_gateway_for_main_session(session: Session, session_key: str | None) -> Gateway | None: async def _find_gateway_for_main_session(
session: AsyncSession, session_key: str | None
) -> Gateway | None:
if not session_key: if not session_key:
return None return None
return session.exec(select(Gateway).where(Gateway.main_session_key == session_key)).first() return (await session.exec(select(Gateway).where(Gateway.main_session_key == session_key))).first()
async def _ensure_gateway_session( async def _ensure_gateway_session(
@@ -184,7 +168,7 @@ async def _ensure_gateway_session(
def _with_computed_status(agent: Agent) -> Agent: def _with_computed_status(agent: Agent) -> Agent:
now = datetime.utcnow() now = utcnow()
if agent.status in {"deleting", "updating"}: if agent.status in {"deleting", "updating"}:
return agent return agent
if agent.last_seen_at is None: if agent.last_seen_at is None:
@@ -198,24 +182,24 @@ def _serialize_agent(agent: Agent, main_session_keys: set[str]) -> dict[str, obj
return _to_agent_read(_with_computed_status(agent), main_session_keys).model_dump(mode="json") return _to_agent_read(_with_computed_status(agent), main_session_keys).model_dump(mode="json")
def _fetch_agent_events( async def _fetch_agent_events(
session: AsyncSession,
board_id: UUID | None, board_id: UUID | None,
since: datetime, since: datetime,
) -> list[Agent]: ) -> list[Agent]:
with Session(engine) as session: statement = select(Agent)
statement = select(Agent) if board_id:
if board_id: statement = statement.where(col(Agent.board_id) == board_id)
statement = statement.where(col(Agent.board_id) == board_id) statement = statement.where(
statement = statement.where( or_(
or_( col(Agent.updated_at) >= since,
col(Agent.updated_at) >= since, col(Agent.last_seen_at) >= since,
col(Agent.last_seen_at) >= since, )
) ).order_by(asc(col(Agent.updated_at)))
).order_by(asc(col(Agent.updated_at))) return list(await session.exec(statement))
return list(session.exec(statement))
def _record_heartbeat(session: Session, agent: Agent) -> None: def _record_heartbeat(session: AsyncSession, agent: Agent) -> None:
record_activity( record_activity(
session, session,
event_type="agent.heartbeat", event_type="agent.heartbeat",
@@ -224,7 +208,7 @@ def _record_heartbeat(session: Session, agent: Agent) -> None:
) )
def _record_instruction_failure(session: Session, agent: Agent, error: str, action: str) -> None: def _record_instruction_failure(session: AsyncSession, agent: Agent, error: str, action: str) -> None:
action_label = action.replace("_", " ").capitalize() action_label = action.replace("_", " ").capitalize()
record_activity( record_activity(
session, session,
@@ -248,12 +232,12 @@ async def _send_wakeup_message(
@router.get("", response_model=list[AgentRead]) @router.get("", response_model=list[AgentRead])
def list_agents( async def list_agents(
session: Session = Depends(get_session), session: AsyncSession = Depends(get_session),
auth: AuthContext = Depends(require_admin_auth), auth: AuthContext = Depends(require_admin_auth),
) -> list[Agent]: ) -> list[AgentRead]:
agents = list(session.exec(select(Agent))) agents = list(await session.exec(select(Agent)))
main_session_keys = _get_gateway_main_session_keys(session) 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] return [_to_agent_read(_with_computed_status(agent), main_session_keys) for agent in agents]
@@ -264,24 +248,23 @@ async def stream_agents(
since: str | None = Query(default=None), since: str | None = Query(default=None),
auth: AuthContext = Depends(require_admin_auth), auth: AuthContext = Depends(require_admin_auth),
) -> EventSourceResponse: ) -> EventSourceResponse:
since_dt = _parse_since(since) or datetime.utcnow() since_dt = _parse_since(since) or utcnow()
last_seen = since_dt last_seen = since_dt
async def event_generator(): async def event_generator() -> AsyncIterator[dict[str, str]]:
nonlocal last_seen nonlocal last_seen
while True: while True:
if await request.is_disconnected(): if await request.is_disconnected():
break break
agents = await run_in_threadpool(_fetch_agent_events, board_id, last_seen) async with async_session_maker() as session:
if agents: agents = await _fetch_agent_events(session, board_id, last_seen)
with Session(engine) as session: main_session_keys = await _get_gateway_main_session_keys(session) if agents else set()
main_session_keys = _get_gateway_main_session_keys(session) for agent in agents:
for agent in agents: updated_at = agent.updated_at or agent.last_seen_at or utcnow()
updated_at = agent.updated_at or agent.last_seen_at or datetime.utcnow() if updated_at > last_seen:
if updated_at > last_seen: last_seen = updated_at
last_seen = updated_at payload = {"agent": _serialize_agent(agent, main_session_keys)}
payload = {"agent": _serialize_agent(agent, main_session_keys)} yield {"event": "agent", "data": json.dumps(payload)}
yield {"event": "agent", "data": json.dumps(payload)}
await asyncio.sleep(2) await asyncio.sleep(2)
return EventSourceResponse(event_generator(), ping=15) return EventSourceResponse(event_generator(), ping=15)
@@ -290,9 +273,9 @@ async def stream_agents(
@router.post("", response_model=AgentRead) @router.post("", response_model=AgentRead)
async def create_agent( async def create_agent(
payload: AgentCreate, payload: AgentCreate,
session: Session = Depends(get_session), session: AsyncSession = Depends(get_session),
actor: ActorContext = Depends(require_admin_or_agent), actor: ActorContext = Depends(require_admin_or_agent),
) -> Agent: ) -> AgentRead:
if actor.actor_type == "agent": if actor.actor_type == "agent":
if not actor.agent or not actor.agent.is_board_lead: if not actor.agent or not actor.agent.is_board_lead:
raise HTTPException( raise HTTPException(
@@ -311,39 +294,36 @@ async def create_agent(
) )
payload = AgentCreate(**{**payload.model_dump(), "board_id": actor.agent.board_id}) payload = AgentCreate(**{**payload.model_dump(), "board_id": actor.agent.board_id})
board = _require_board(session, payload.board_id) board = await _require_board(session, payload.board_id)
gateway, client_config = _require_gateway(session, board) gateway, client_config = await _require_gateway(session, board)
data = payload.model_dump() data = payload.model_dump()
requested_name = (data.get("name") or "").strip() requested_name = (data.get("name") or "").strip()
if requested_name: if requested_name:
existing = session.exec( existing = (
select(Agent) await session.exec(
.where(Agent.board_id == board.id) select(Agent)
.where(col(Agent.name).ilike(requested_name)) .where(Agent.board_id == board.id)
.where(col(Agent.name).ilike(requested_name))
)
).first() ).first()
if existing: if existing:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_409_CONFLICT, status_code=status.HTTP_409_CONFLICT,
detail="An agent with this name already exists on this board.", detail="An agent with this name already exists on this board.",
) )
if data.get("identity_template") == "":
data["identity_template"] = None
if data.get("soul_template") == "":
data["soul_template"] = None
data["identity_profile"] = _normalize_identity_profile(data.get("identity_profile"))
agent = Agent.model_validate(data) agent = Agent.model_validate(data)
agent.status = "provisioning" agent.status = "provisioning"
raw_token = generate_agent_token() raw_token = generate_agent_token()
agent.agent_token_hash = hash_agent_token(raw_token) agent.agent_token_hash = hash_agent_token(raw_token)
if agent.heartbeat_config is None: if agent.heartbeat_config is None:
agent.heartbeat_config = DEFAULT_HEARTBEAT_CONFIG.copy() agent.heartbeat_config = DEFAULT_HEARTBEAT_CONFIG.copy()
agent.provision_requested_at = datetime.utcnow() agent.provision_requested_at = utcnow()
agent.provision_action = "provision" agent.provision_action = "provision"
session_key, session_error = await _ensure_gateway_session(agent.name, client_config) session_key, session_error = await _ensure_gateway_session(agent.name, client_config)
agent.openclaw_session_id = session_key agent.openclaw_session_id = session_key
session.add(agent) session.add(agent)
session.commit() await session.commit()
session.refresh(agent) await session.refresh(agent)
if session_error: if session_error:
record_activity( record_activity(
session, session,
@@ -358,7 +338,7 @@ async def create_agent(
message=f"Session created for {agent.name}.", message=f"Session created for {agent.name}.",
agent_id=agent.id, agent_id=agent.id,
) )
session.commit() await session.commit()
try: try:
await provision_agent( await provision_agent(
agent, agent,
@@ -372,9 +352,9 @@ async def create_agent(
agent.provision_confirm_token_hash = None agent.provision_confirm_token_hash = None
agent.provision_requested_at = None agent.provision_requested_at = None
agent.provision_action = None agent.provision_action = None
agent.updated_at = datetime.utcnow() agent.updated_at = utcnow()
session.add(agent) session.add(agent)
session.commit() await session.commit()
record_activity( record_activity(
session, session,
event_type="agent.provision", event_type="agent.provision",
@@ -387,26 +367,27 @@ async def create_agent(
message=f"Wakeup message sent to {agent.name}.", message=f"Wakeup message sent to {agent.name}.",
agent_id=agent.id, agent_id=agent.id,
) )
session.commit() await session.commit()
except OpenClawGatewayError as exc: except OpenClawGatewayError as exc:
_record_instruction_failure(session, agent, str(exc), "provision") _record_instruction_failure(session, agent, str(exc), "provision")
session.commit() await session.commit()
except Exception as exc: # pragma: no cover - unexpected provisioning errors except Exception as exc: # pragma: no cover - unexpected provisioning errors
_record_instruction_failure(session, agent, str(exc), "provision") _record_instruction_failure(session, agent, str(exc), "provision")
session.commit() await session.commit()
return agent main_session_keys = await _get_gateway_main_session_keys(session)
return _to_agent_read(_with_computed_status(agent), main_session_keys)
@router.get("/{agent_id}", response_model=AgentRead) @router.get("/{agent_id}", response_model=AgentRead)
def get_agent( async def get_agent(
agent_id: str, agent_id: str,
session: Session = Depends(get_session), session: AsyncSession = Depends(get_session),
auth: AuthContext = Depends(require_admin_auth), auth: AuthContext = Depends(require_admin_auth),
) -> Agent: ) -> AgentRead:
agent = session.get(Agent, agent_id) agent = await session.get(Agent, agent_id)
if agent is None: if agent is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
main_session_keys = _get_gateway_main_session_keys(session) main_session_keys = await _get_gateway_main_session_keys(session)
return _to_agent_read(_with_computed_status(agent), main_session_keys) return _to_agent_read(_with_computed_status(agent), main_session_keys)
@@ -415,10 +396,10 @@ async def update_agent(
agent_id: str, agent_id: str,
payload: AgentUpdate, payload: AgentUpdate,
force: bool = False, force: bool = False,
session: Session = Depends(get_session), session: AsyncSession = Depends(get_session),
auth: AuthContext = Depends(require_admin_auth), auth: AuthContext = Depends(require_admin_auth),
) -> Agent: ) -> AgentRead:
agent = session.get(Agent, agent_id) agent = await session.get(Agent, agent_id)
if agent is None: if agent is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
updates = payload.model_dump(exclude_unset=True) updates = payload.model_dump(exclude_unset=True)
@@ -428,21 +409,15 @@ async def update_agent(
status_code=status.HTTP_403_FORBIDDEN, status_code=status.HTTP_403_FORBIDDEN,
detail="status is controlled by agent heartbeat", detail="status is controlled by agent heartbeat",
) )
if updates.get("identity_template") == "":
updates["identity_template"] = None
if updates.get("soul_template") == "":
updates["soul_template"] = None
if "identity_profile" in updates:
updates["identity_profile"] = _normalize_identity_profile(updates.get("identity_profile"))
if not updates and not force and make_main is None: if not updates and not force and make_main is None:
main_session_keys = _get_gateway_main_session_keys(session) main_session_keys = await _get_gateway_main_session_keys(session)
return _to_agent_read(_with_computed_status(agent), main_session_keys) return _to_agent_read(_with_computed_status(agent), main_session_keys)
main_gateway = _find_gateway_for_main_session(session, agent.openclaw_session_id) main_gateway = await _find_gateway_for_main_session(session, agent.openclaw_session_id)
gateway_for_main: Gateway | None = None gateway_for_main: Gateway | None = None
if make_main is True: if make_main is True:
board_source = updates.get("board_id") or agent.board_id board_source = updates.get("board_id") or agent.board_id
board_for_main = _require_board(session, board_source) board_for_main = await _require_board(session, board_source)
gateway_for_main, _ = _require_gateway(session, board_for_main) gateway_for_main, _ = await _require_gateway(session, board_for_main)
updates["board_id"] = None updates["board_id"] = None
agent.is_board_lead = False agent.is_board_lead = False
agent.openclaw_session_id = gateway_for_main.main_session_key agent.openclaw_session_id = gateway_for_main.main_session_key
@@ -450,18 +425,18 @@ async def update_agent(
elif make_main is False: elif make_main is False:
agent.openclaw_session_id = None agent.openclaw_session_id = None
if make_main is not True and "board_id" in updates: if make_main is not True and "board_id" in updates:
_require_board(session, updates["board_id"]) await _require_board(session, updates["board_id"])
for key, value in updates.items(): for key, value in updates.items():
setattr(agent, key, value) setattr(agent, key, value)
if make_main is None and main_gateway is not None: if make_main is None and main_gateway is not None:
agent.board_id = None agent.board_id = None
agent.is_board_lead = False agent.is_board_lead = False
agent.updated_at = datetime.utcnow() agent.updated_at = utcnow()
if agent.heartbeat_config is None: if agent.heartbeat_config is None:
agent.heartbeat_config = DEFAULT_HEARTBEAT_CONFIG.copy() agent.heartbeat_config = DEFAULT_HEARTBEAT_CONFIG.copy()
session.add(agent) session.add(agent)
session.commit() await session.commit()
session.refresh(agent) await session.refresh(agent)
is_main_agent = False is_main_agent = False
board: Board | None = None board: Board | None = None
gateway: Gateway | None = None gateway: Gateway | None = None
@@ -490,8 +465,8 @@ async def update_agent(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="board_id is required for non-main agents", detail="board_id is required for non-main agents",
) )
board = _require_board(session, agent.board_id) board = await _require_board(session, agent.board_id)
gateway, client_config = _require_gateway(session, board) gateway, client_config = await _require_gateway(session, board)
session_key = agent.openclaw_session_id or _build_session_key(agent.name) session_key = agent.openclaw_session_id or _build_session_key(agent.name)
try: try:
if client_config is None: if client_config is None:
@@ -503,19 +478,19 @@ async def update_agent(
if not agent.openclaw_session_id: if not agent.openclaw_session_id:
agent.openclaw_session_id = session_key agent.openclaw_session_id = session_key
session.add(agent) session.add(agent)
session.commit() await session.commit()
session.refresh(agent) await session.refresh(agent)
except OpenClawGatewayError as exc: except OpenClawGatewayError as exc:
_record_instruction_failure(session, agent, str(exc), "update") _record_instruction_failure(session, agent, str(exc), "update")
session.commit() await session.commit()
raw_token = generate_agent_token() raw_token = generate_agent_token()
agent.agent_token_hash = hash_agent_token(raw_token) agent.agent_token_hash = hash_agent_token(raw_token)
agent.provision_requested_at = datetime.utcnow() agent.provision_requested_at = utcnow()
agent.provision_action = "update" agent.provision_action = "update"
agent.status = "updating" agent.status = "updating"
session.add(agent) session.add(agent)
session.commit() await session.commit()
session.refresh(agent) await session.refresh(agent)
try: try:
if gateway is None: if gateway is None:
raise HTTPException( raise HTTPException(
@@ -533,6 +508,11 @@ async def update_agent(
reset_session=True, reset_session=True,
) )
else: else:
if board is None:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="board is required for non-main agent provisioning",
)
await provision_agent( await provision_agent(
agent, agent,
board, board,
@@ -548,9 +528,9 @@ async def update_agent(
agent.provision_requested_at = None agent.provision_requested_at = None
agent.provision_action = None agent.provision_action = None
agent.status = "online" agent.status = "online"
agent.updated_at = datetime.utcnow() agent.updated_at = utcnow()
session.add(agent) session.add(agent)
session.commit() await session.commit()
record_activity( record_activity(
session, session,
event_type="agent.update.direct", event_type="agent.update.direct",
@@ -563,33 +543,33 @@ async def update_agent(
message=f"Wakeup message sent to {agent.name}.", message=f"Wakeup message sent to {agent.name}.",
agent_id=agent.id, agent_id=agent.id,
) )
session.commit() await session.commit()
except OpenClawGatewayError as exc: except OpenClawGatewayError as exc:
_record_instruction_failure(session, agent, str(exc), "update") _record_instruction_failure(session, agent, str(exc), "update")
session.commit() await session.commit()
raise HTTPException( raise HTTPException(
status_code=status.HTTP_502_BAD_GATEWAY, status_code=status.HTTP_502_BAD_GATEWAY,
detail=f"Gateway update failed: {exc}", detail=f"Gateway update failed: {exc}",
) from exc ) from exc
except Exception as exc: # pragma: no cover - unexpected provisioning errors except Exception as exc: # pragma: no cover - unexpected provisioning errors
_record_instruction_failure(session, agent, str(exc), "update") _record_instruction_failure(session, agent, str(exc), "update")
session.commit() await session.commit()
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Unexpected error updating agent provisioning.", detail="Unexpected error updating agent provisioning.",
) from exc ) from exc
main_session_keys = _get_gateway_main_session_keys(session) main_session_keys = await _get_gateway_main_session_keys(session)
return _to_agent_read(_with_computed_status(agent), main_session_keys) return _to_agent_read(_with_computed_status(agent), main_session_keys)
@router.post("/{agent_id}/heartbeat", response_model=AgentRead) @router.post("/{agent_id}/heartbeat", response_model=AgentRead)
def heartbeat_agent( async def heartbeat_agent(
agent_id: str, agent_id: str,
payload: AgentHeartbeat, payload: AgentHeartbeat,
session: Session = Depends(get_session), session: AsyncSession = Depends(get_session),
actor: ActorContext = Depends(require_admin_or_agent), actor: ActorContext = Depends(require_admin_or_agent),
) -> AgentRead: ) -> AgentRead:
agent = session.get(Agent, agent_id) agent = await session.get(Agent, agent_id)
if agent is None: if agent is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
if actor.actor_type == "agent" and actor.agent and actor.agent.id != agent.id: if actor.actor_type == "agent" and actor.agent and actor.agent.id != agent.id:
@@ -598,25 +578,25 @@ def heartbeat_agent(
agent.status = payload.status agent.status = payload.status
elif agent.status == "provisioning": elif agent.status == "provisioning":
agent.status = "online" agent.status = "online"
agent.last_seen_at = datetime.utcnow() agent.last_seen_at = utcnow()
agent.updated_at = datetime.utcnow() agent.updated_at = utcnow()
_record_heartbeat(session, agent) _record_heartbeat(session, agent)
session.add(agent) session.add(agent)
session.commit() await session.commit()
session.refresh(agent) await session.refresh(agent)
main_session_keys = _get_gateway_main_session_keys(session) main_session_keys = await _get_gateway_main_session_keys(session)
return _to_agent_read(_with_computed_status(agent), main_session_keys) return _to_agent_read(_with_computed_status(agent), main_session_keys)
@router.post("/heartbeat", response_model=AgentRead) @router.post("/heartbeat", response_model=AgentRead)
async def heartbeat_or_create_agent( async def heartbeat_or_create_agent(
payload: AgentHeartbeatCreate, payload: AgentHeartbeatCreate,
session: Session = Depends(get_session), session: AsyncSession = Depends(get_session),
actor: ActorContext = Depends(require_admin_or_agent), actor: ActorContext = Depends(require_admin_or_agent),
) -> AgentRead: ) -> AgentRead:
# Agent tokens must heartbeat their authenticated agent record. Names are not unique. # Agent tokens must heartbeat their authenticated agent record. Names are not unique.
if actor.actor_type == "agent" and actor.agent: if actor.actor_type == "agent" and actor.agent:
return heartbeat_agent( return await heartbeat_agent(
agent_id=str(actor.agent.id), agent_id=str(actor.agent.id),
payload=AgentHeartbeat(status=payload.status), payload=AgentHeartbeat(status=payload.status),
session=session, session=session,
@@ -626,12 +606,12 @@ async def heartbeat_or_create_agent(
statement = select(Agent).where(Agent.name == payload.name) statement = select(Agent).where(Agent.name == payload.name)
if payload.board_id is not None: if payload.board_id is not None:
statement = statement.where(Agent.board_id == payload.board_id) statement = statement.where(Agent.board_id == payload.board_id)
agent = session.exec(statement).first() agent = (await session.exec(statement)).first()
if agent is None: if agent is None:
if actor.actor_type == "agent": if actor.actor_type == "agent":
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
board = _require_board(session, payload.board_id) board = await _require_board(session, payload.board_id)
gateway, client_config = _require_gateway(session, board) gateway, client_config = await _require_gateway(session, board)
agent = Agent( agent = Agent(
name=payload.name, name=payload.name,
status="provisioning", status="provisioning",
@@ -640,13 +620,13 @@ async def heartbeat_or_create_agent(
) )
raw_token = generate_agent_token() raw_token = generate_agent_token()
agent.agent_token_hash = hash_agent_token(raw_token) agent.agent_token_hash = hash_agent_token(raw_token)
agent.provision_requested_at = datetime.utcnow() agent.provision_requested_at = utcnow()
agent.provision_action = "provision" agent.provision_action = "provision"
session_key, session_error = await _ensure_gateway_session(agent.name, client_config) session_key, session_error = await _ensure_gateway_session(agent.name, client_config)
agent.openclaw_session_id = session_key agent.openclaw_session_id = session_key
session.add(agent) session.add(agent)
session.commit() await session.commit()
session.refresh(agent) await session.refresh(agent)
if session_error: if session_error:
record_activity( record_activity(
session, session,
@@ -661,16 +641,16 @@ async def heartbeat_or_create_agent(
message=f"Session created for {agent.name}.", message=f"Session created for {agent.name}.",
agent_id=agent.id, agent_id=agent.id,
) )
session.commit() await session.commit()
try: try:
await provision_agent(agent, board, gateway, raw_token, actor.user, action="provision") await provision_agent(agent, board, gateway, raw_token, actor.user, action="provision")
await _send_wakeup_message(agent, client_config, verb="provisioned") await _send_wakeup_message(agent, client_config, verb="provisioned")
agent.provision_confirm_token_hash = None agent.provision_confirm_token_hash = None
agent.provision_requested_at = None agent.provision_requested_at = None
agent.provision_action = None agent.provision_action = None
agent.updated_at = datetime.utcnow() agent.updated_at = utcnow()
session.add(agent) session.add(agent)
session.commit() await session.commit()
record_activity( record_activity(
session, session,
event_type="agent.provision", event_type="agent.provision",
@@ -683,13 +663,13 @@ async def heartbeat_or_create_agent(
message=f"Wakeup message sent to {agent.name}.", message=f"Wakeup message sent to {agent.name}.",
agent_id=agent.id, agent_id=agent.id,
) )
session.commit() await session.commit()
except OpenClawGatewayError as exc: except OpenClawGatewayError as exc:
_record_instruction_failure(session, agent, str(exc), "provision") _record_instruction_failure(session, agent, str(exc), "provision")
session.commit() await session.commit()
except Exception as exc: # pragma: no cover - unexpected provisioning errors except Exception as exc: # pragma: no cover - unexpected provisioning errors
_record_instruction_failure(session, agent, str(exc), "provision") _record_instruction_failure(session, agent, str(exc), "provision")
session.commit() await session.commit()
elif actor.actor_type == "agent" and actor.agent and actor.agent.id != agent.id: elif actor.actor_type == "agent" and actor.agent and actor.agent.id != agent.id:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
elif agent.agent_token_hash is None and actor.actor_type == "user": elif agent.agent_token_hash is None and actor.actor_type == "user":
@@ -697,22 +677,22 @@ async def heartbeat_or_create_agent(
agent.agent_token_hash = hash_agent_token(raw_token) agent.agent_token_hash = hash_agent_token(raw_token)
if agent.heartbeat_config is None: if agent.heartbeat_config is None:
agent.heartbeat_config = DEFAULT_HEARTBEAT_CONFIG.copy() agent.heartbeat_config = DEFAULT_HEARTBEAT_CONFIG.copy()
agent.provision_requested_at = datetime.utcnow() agent.provision_requested_at = utcnow()
agent.provision_action = "provision" agent.provision_action = "provision"
session.add(agent) session.add(agent)
session.commit() await session.commit()
session.refresh(agent) await session.refresh(agent)
try: try:
board = _require_board(session, str(agent.board_id) if agent.board_id else None) board = await _require_board(session, str(agent.board_id) if agent.board_id else None)
gateway, client_config = _require_gateway(session, board) gateway, client_config = await _require_gateway(session, board)
await provision_agent(agent, board, gateway, raw_token, actor.user, action="provision") await provision_agent(agent, board, gateway, raw_token, actor.user, action="provision")
await _send_wakeup_message(agent, client_config, verb="provisioned") await _send_wakeup_message(agent, client_config, verb="provisioned")
agent.provision_confirm_token_hash = None agent.provision_confirm_token_hash = None
agent.provision_requested_at = None agent.provision_requested_at = None
agent.provision_action = None agent.provision_action = None
agent.updated_at = datetime.utcnow() agent.updated_at = utcnow()
session.add(agent) session.add(agent)
session.commit() await session.commit()
record_activity( record_activity(
session, session,
event_type="agent.provision", event_type="agent.provision",
@@ -725,16 +705,16 @@ async def heartbeat_or_create_agent(
message=f"Wakeup message sent to {agent.name}.", message=f"Wakeup message sent to {agent.name}.",
agent_id=agent.id, agent_id=agent.id,
) )
session.commit() await session.commit()
except OpenClawGatewayError as exc: except OpenClawGatewayError as exc:
_record_instruction_failure(session, agent, str(exc), "provision") _record_instruction_failure(session, agent, str(exc), "provision")
session.commit() await session.commit()
except Exception as exc: # pragma: no cover - unexpected provisioning errors except Exception as exc: # pragma: no cover - unexpected provisioning errors
_record_instruction_failure(session, agent, str(exc), "provision") _record_instruction_failure(session, agent, str(exc), "provision")
session.commit() await session.commit()
elif not agent.openclaw_session_id: elif not agent.openclaw_session_id:
board = _require_board(session, str(agent.board_id) if agent.board_id else None) board = await _require_board(session, str(agent.board_id) if agent.board_id else None)
gateway, client_config = _require_gateway(session, board) gateway, client_config = await _require_gateway(session, board)
session_key, session_error = await _ensure_gateway_session(agent.name, client_config) session_key, session_error = await _ensure_gateway_session(agent.name, client_config)
agent.openclaw_session_id = session_key agent.openclaw_session_id = session_key
if session_error: if session_error:
@@ -751,47 +731,45 @@ async def heartbeat_or_create_agent(
message=f"Session created for {agent.name}.", message=f"Session created for {agent.name}.",
agent_id=agent.id, agent_id=agent.id,
) )
session.commit() await session.commit()
if payload.status: if payload.status:
agent.status = payload.status agent.status = payload.status
elif agent.status == "provisioning": elif agent.status == "provisioning":
agent.status = "online" agent.status = "online"
agent.last_seen_at = datetime.utcnow() agent.last_seen_at = utcnow()
agent.updated_at = datetime.utcnow() agent.updated_at = utcnow()
_record_heartbeat(session, agent) _record_heartbeat(session, agent)
session.add(agent) session.add(agent)
session.commit() await session.commit()
session.refresh(agent) await session.refresh(agent)
main_session_keys = _get_gateway_main_session_keys(session) main_session_keys = await _get_gateway_main_session_keys(session)
return _to_agent_read(_with_computed_status(agent), main_session_keys) return _to_agent_read(_with_computed_status(agent), main_session_keys)
@router.delete("/{agent_id}") @router.delete("/{agent_id}", response_model=OkResponse)
def delete_agent( async def delete_agent(
agent_id: str, agent_id: str,
session: Session = Depends(get_session), session: AsyncSession = Depends(get_session),
auth: AuthContext = Depends(require_admin_auth), auth: AuthContext = Depends(require_admin_auth),
) -> dict[str, bool]: ) -> OkResponse:
agent = session.get(Agent, agent_id) agent = await session.get(Agent, agent_id)
if agent is None: if agent is None:
return {"ok": True} return OkResponse()
board = _require_board(session, str(agent.board_id) if agent.board_id else None) board = await _require_board(session, str(agent.board_id) if agent.board_id else None)
gateway, client_config = _require_gateway(session, board) gateway, client_config = await _require_gateway(session, board)
try: try:
import asyncio workspace_path = await cleanup_agent(agent, gateway)
workspace_path = asyncio.run(cleanup_agent(agent, gateway))
except OpenClawGatewayError as exc: except OpenClawGatewayError as exc:
_record_instruction_failure(session, agent, str(exc), "delete") _record_instruction_failure(session, agent, str(exc), "delete")
session.commit() await session.commit()
raise HTTPException( raise HTTPException(
status_code=status.HTTP_502_BAD_GATEWAY, status_code=status.HTTP_502_BAD_GATEWAY,
detail=f"Gateway cleanup failed: {exc}", detail=f"Gateway cleanup failed: {exc}",
) from exc ) from exc
except Exception as exc: # pragma: no cover - unexpected cleanup errors except Exception as exc: # pragma: no cover - unexpected cleanup errors
_record_instruction_failure(session, agent, str(exc), "delete") _record_instruction_failure(session, agent, str(exc), "delete")
session.commit() await session.commit()
raise HTTPException( raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Workspace cleanup failed: {exc}", detail=f"Workspace cleanup failed: {exc}",
@@ -804,7 +782,7 @@ def delete_agent(
agent_id=None, agent_id=None,
) )
now = datetime.now() now = datetime.now()
session.execute( await session.execute(
update(Task) update(Task)
.where(col(Task.assigned_agent_id) == agent.id) .where(col(Task.assigned_agent_id) == agent.id)
.where(col(Task.status) == "in_progress") .where(col(Task.status) == "in_progress")
@@ -815,7 +793,7 @@ def delete_agent(
updated_at=now, updated_at=now,
) )
) )
session.execute( await session.execute(
update(Task) update(Task)
.where(col(Task.assigned_agent_id) == agent.id) .where(col(Task.assigned_agent_id) == agent.id)
.where(col(Task.status) != "in_progress") .where(col(Task.status) != "in_progress")
@@ -824,11 +802,11 @@ def delete_agent(
updated_at=now, updated_at=now,
) )
) )
session.execute( await session.execute(
update(ActivityEvent).where(col(ActivityEvent.agent_id) == agent.id).values(agent_id=None) update(ActivityEvent).where(col(ActivityEvent.agent_id) == agent.id).values(agent_id=None)
) )
session.delete(agent) await session.delete(agent)
session.commit() await session.commit()
# Always ask the main agent to confirm workspace cleanup. # Always ask the main agent to confirm workspace cleanup.
try: try:
@@ -843,20 +821,14 @@ def delete_agent(
"1) Remove the workspace directory.\n" "1) Remove the workspace directory.\n"
"2) Reply NO_REPLY.\n" "2) Reply NO_REPLY.\n"
) )
await ensure_session(main_session, config=client_config, label="Main Agent")
async def _request_cleanup() -> None: await send_message(
await ensure_session(main_session, config=client_config, label="Main Agent") cleanup_message,
await send_message( session_key=main_session,
cleanup_message, config=client_config,
session_key=main_session, deliver=False,
config=client_config, )
deliver=False,
)
import asyncio
asyncio.run(_request_cleanup())
except Exception: except Exception:
# Cleanup request is best-effort; deletion already completed. # Cleanup request is best-effort; deletion already completed.
pass pass
return {"ok": True} return OkResponse()

View File

@@ -2,24 +2,26 @@ from __future__ import annotations
import asyncio import asyncio
import json import json
from collections.abc import AsyncIterator
from datetime import datetime, timezone from datetime import datetime, timezone
from uuid import UUID from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Query, Request, status from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
from sqlalchemy import asc, or_ from sqlalchemy import asc, or_
from sqlmodel import Session, col, select from sqlmodel import col, select
from sqlmodel.ext.asyncio.session import AsyncSession
from sse_starlette.sse import EventSourceResponse from sse_starlette.sse import EventSourceResponse
from starlette.concurrency import run_in_threadpool
from app.api.deps import ActorContext, get_board_or_404, require_admin_auth, require_admin_or_agent from app.api.deps import ActorContext, get_board_or_404, require_admin_auth, require_admin_or_agent
from app.db.session import engine, get_session from app.core.auth import AuthContext
from app.core.time import utcnow
from app.db.session import async_session_maker, get_session
from app.models.approvals import Approval from app.models.approvals import Approval
from app.schemas.approvals import ApprovalCreate, ApprovalRead, ApprovalUpdate from app.models.boards import Board
from app.schemas.approvals import ApprovalCreate, ApprovalRead, ApprovalStatus, ApprovalUpdate
router = APIRouter(prefix="/boards/{board_id}/approvals", tags=["approvals"]) router = APIRouter(prefix="/boards/{board_id}/approvals", tags=["approvals"])
ALLOWED_STATUSES = {"pending", "approved", "rejected"}
def _parse_since(value: str | None) -> datetime | None: def _parse_since(value: str | None) -> datetime | None:
if not value: if not value:
@@ -45,30 +47,30 @@ def _serialize_approval(approval: Approval) -> dict[str, object]:
return ApprovalRead.model_validate(approval, from_attributes=True).model_dump(mode="json") return ApprovalRead.model_validate(approval, from_attributes=True).model_dump(mode="json")
def _fetch_approval_events( async def _fetch_approval_events(
session: AsyncSession,
board_id: UUID, board_id: UUID,
since: datetime, since: datetime,
) -> list[Approval]: ) -> list[Approval]:
with Session(engine) as session: statement = (
statement = ( select(Approval)
select(Approval) .where(col(Approval.board_id) == board_id)
.where(col(Approval.board_id) == board_id) .where(
.where( or_(
or_( col(Approval.created_at) >= since,
col(Approval.created_at) >= since, col(Approval.resolved_at) >= since,
col(Approval.resolved_at) >= since,
)
) )
.order_by(asc(col(Approval.created_at)))
) )
return list(session.exec(statement)) .order_by(asc(col(Approval.created_at)))
)
return list(await session.exec(statement))
@router.get("", response_model=list[ApprovalRead]) @router.get("", response_model=list[ApprovalRead])
def list_approvals( async def list_approvals(
status_filter: str | None = Query(default=None, alias="status"), status_filter: ApprovalStatus | None = Query(default=None, alias="status"),
board=Depends(get_board_or_404), board: Board = Depends(get_board_or_404),
session: Session = Depends(get_session), session: AsyncSession = Depends(get_session),
actor: ActorContext = Depends(require_admin_or_agent), actor: ActorContext = Depends(require_admin_or_agent),
) -> list[Approval]: ) -> list[Approval]:
if actor.actor_type == "agent" and actor.agent: if actor.actor_type == "agent" and actor.agent:
@@ -76,32 +78,31 @@ def list_approvals(
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
statement = select(Approval).where(col(Approval.board_id) == board.id) statement = select(Approval).where(col(Approval.board_id) == board.id)
if status_filter: if status_filter:
if status_filter not in ALLOWED_STATUSES:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
statement = statement.where(col(Approval.status) == status_filter) statement = statement.where(col(Approval.status) == status_filter)
statement = statement.order_by(col(Approval.created_at).desc()) statement = statement.order_by(col(Approval.created_at).desc())
return list(session.exec(statement)) return list(await session.exec(statement))
@router.get("/stream") @router.get("/stream")
async def stream_approvals( async def stream_approvals(
request: Request, request: Request,
board=Depends(get_board_or_404), board: Board = Depends(get_board_or_404),
actor: ActorContext = Depends(require_admin_or_agent), actor: ActorContext = Depends(require_admin_or_agent),
since: str | None = Query(default=None), since: str | None = Query(default=None),
) -> EventSourceResponse: ) -> EventSourceResponse:
if actor.actor_type == "agent" and actor.agent: if actor.actor_type == "agent" and actor.agent:
if actor.agent.board_id and actor.agent.board_id != board.id: if actor.agent.board_id and actor.agent.board_id != board.id:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
since_dt = _parse_since(since) or datetime.utcnow() since_dt = _parse_since(since) or utcnow()
last_seen = since_dt last_seen = since_dt
async def event_generator(): async def event_generator() -> AsyncIterator[dict[str, str]]:
nonlocal last_seen nonlocal last_seen
while True: while True:
if await request.is_disconnected(): if await request.is_disconnected():
break break
approvals = await run_in_threadpool(_fetch_approval_events, board.id, last_seen) async with async_session_maker() as session:
approvals = await _fetch_approval_events(session, board.id, last_seen)
for approval in approvals: for approval in approvals:
updated_at = _approval_updated_at(approval) updated_at = _approval_updated_at(approval)
if updated_at > last_seen: if updated_at > last_seen:
@@ -114,10 +115,10 @@ async def stream_approvals(
@router.post("", response_model=ApprovalRead) @router.post("", response_model=ApprovalRead)
def create_approval( async def create_approval(
payload: ApprovalCreate, payload: ApprovalCreate,
board=Depends(get_board_or_404), board: Board = Depends(get_board_or_404),
session: Session = Depends(get_session), session: AsyncSession = Depends(get_session),
actor: ActorContext = Depends(require_admin_or_agent), actor: ActorContext = Depends(require_admin_or_agent),
) -> Approval: ) -> Approval:
if actor.actor_type == "agent" and actor.agent: if actor.actor_type == "agent" and actor.agent:
@@ -133,30 +134,28 @@ def create_approval(
status=payload.status, status=payload.status,
) )
session.add(approval) session.add(approval)
session.commit() await session.commit()
session.refresh(approval) await session.refresh(approval)
return approval return approval
@router.patch("/{approval_id}", response_model=ApprovalRead) @router.patch("/{approval_id}", response_model=ApprovalRead)
def update_approval( async def update_approval(
approval_id: str, approval_id: str,
payload: ApprovalUpdate, payload: ApprovalUpdate,
board=Depends(get_board_or_404), board: Board = Depends(get_board_or_404),
session: Session = Depends(get_session), session: AsyncSession = Depends(get_session),
auth=Depends(require_admin_auth), auth: AuthContext = Depends(require_admin_auth),
) -> Approval: ) -> Approval:
approval = session.get(Approval, approval_id) approval = await session.get(Approval, approval_id)
if approval is None or approval.board_id != board.id: if approval is None or approval.board_id != board.id:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
updates = payload.model_dump(exclude_unset=True) updates = payload.model_dump(exclude_unset=True)
if "status" in updates: if "status" in updates:
if updates["status"] not in ALLOWED_STATUSES:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
approval.status = updates["status"] approval.status = updates["status"]
if approval.status != "pending": if approval.status != "pending":
approval.resolved_at = datetime.utcnow() approval.resolved_at = utcnow()
session.add(approval) session.add(approval)
session.commit() await session.commit()
session.refresh(approval) await session.refresh(approval)
return approval return approval

View File

@@ -3,20 +3,24 @@ from __future__ import annotations
import asyncio import asyncio
import json import json
import re import re
from collections.abc import AsyncIterator
from datetime import datetime, timezone from datetime import datetime, timezone
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Query, Request from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
from sqlmodel import Session, col, select from sqlmodel import col, select
from sqlmodel.ext.asyncio.session import AsyncSession
from sse_starlette.sse import EventSourceResponse from sse_starlette.sse import EventSourceResponse
from starlette.concurrency import run_in_threadpool
from app.api.deps import ActorContext, get_board_or_404, require_admin_or_agent from app.api.deps import ActorContext, get_board_or_404, require_admin_or_agent
from app.core.config import settings from app.core.config import settings
from app.db.session import engine, get_session from app.core.time import utcnow
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 GatewayConfig as GatewayClientConfig
from app.integrations.openclaw_gateway import OpenClawGatewayError, ensure_session, send_message from app.integrations.openclaw_gateway import OpenClawGatewayError, ensure_session, send_message
from app.models.agents import Agent from app.models.agents import Agent
from app.models.board_memory import BoardMemory from app.models.board_memory import BoardMemory
from app.models.boards import Board
from app.models.gateways import Gateway from app.models.gateways import Gateway
from app.schemas.board_memory import BoardMemoryCreate, BoardMemoryRead from app.schemas.board_memory import BoardMemoryCreate, BoardMemoryRead
@@ -62,10 +66,10 @@ def _matches_mention(agent: Agent, mentions: set[str]) -> bool:
return first in mentions return first in mentions
def _gateway_config(session: Session, board) -> GatewayClientConfig | None: async def _gateway_config(session: AsyncSession, board: Board) -> GatewayClientConfig | None:
if not board.gateway_id: if board.gateway_id is None:
return None return None
gateway = session.get(Gateway, board.gateway_id) gateway = await session.get(Gateway, board.gateway_id)
if gateway is None or not gateway.url: if gateway is None or not gateway.url:
return None return None
return GatewayClientConfig(url=gateway.url, token=gateway.token) return GatewayClientConfig(url=gateway.url, token=gateway.token)
@@ -82,36 +86,36 @@ async def _send_agent_message(
await send_message(message, session_key=session_key, config=config, deliver=False) await send_message(message, session_key=session_key, config=config, deliver=False)
def _fetch_memory_events( async def _fetch_memory_events(
board_id, session: AsyncSession,
board_id: UUID,
since: datetime, since: datetime,
) -> list[BoardMemory]: ) -> list[BoardMemory]:
with Session(engine) as session: statement = (
statement = ( select(BoardMemory)
select(BoardMemory) .where(col(BoardMemory.board_id) == board_id)
.where(col(BoardMemory.board_id) == board_id) .where(col(BoardMemory.created_at) >= since)
.where(col(BoardMemory.created_at) >= since) .order_by(col(BoardMemory.created_at))
.order_by(col(BoardMemory.created_at)) )
) return list(await session.exec(statement))
return list(session.exec(statement))
def _notify_chat_targets( async def _notify_chat_targets(
*, *,
session: Session, session: AsyncSession,
board, board: Board,
memory: BoardMemory, memory: BoardMemory,
actor: ActorContext, actor: ActorContext,
) -> None: ) -> None:
if not memory.content: if not memory.content:
return return
config = _gateway_config(session, board) config = await _gateway_config(session, board)
if config is None: if config is None:
return return
mentions = _extract_mentions(memory.content) mentions = _extract_mentions(memory.content)
statement = select(Agent).where(col(Agent.board_id) == board.id) statement = select(Agent).where(col(Agent.board_id) == board.id)
targets: dict[str, Agent] = {} targets: dict[str, Agent] = {}
for agent in session.exec(statement): for agent in await session.exec(statement):
if agent.is_board_lead: if agent.is_board_lead:
targets[str(agent.id)] = agent targets[str(agent.id)] = agent
continue continue
@@ -145,24 +149,22 @@ def _notify_chat_targets(
'Body: {"content":"...","tags":["chat"]}' 'Body: {"content":"...","tags":["chat"]}'
) )
try: try:
asyncio.run( await _send_agent_message(
_send_agent_message( session_key=agent.openclaw_session_id,
session_key=agent.openclaw_session_id, config=config,
config=config, agent_name=agent.name,
agent_name=agent.name, message=message,
message=message,
)
) )
except OpenClawGatewayError: except OpenClawGatewayError:
continue continue
@router.get("", response_model=list[BoardMemoryRead]) @router.get("", response_model=list[BoardMemoryRead])
def list_board_memory( async def list_board_memory(
limit: int = Query(default=50, ge=1, le=200), limit: int = Query(default=50, ge=1, le=200),
offset: int = Query(default=0, ge=0), offset: int = Query(default=0, ge=0),
board=Depends(get_board_or_404), board: Board = Depends(get_board_or_404),
session: Session = Depends(get_session), session: AsyncSession = Depends(get_session),
actor: ActorContext = Depends(require_admin_or_agent), actor: ActorContext = Depends(require_admin_or_agent),
) -> list[BoardMemory]: ) -> list[BoardMemory]:
if actor.actor_type == "agent" and actor.agent: if actor.actor_type == "agent" and actor.agent:
@@ -175,28 +177,29 @@ def list_board_memory(
.offset(offset) .offset(offset)
.limit(limit) .limit(limit)
) )
return list(session.exec(statement)) return list(await session.exec(statement))
@router.get("/stream") @router.get("/stream")
async def stream_board_memory( async def stream_board_memory(
request: Request, request: Request,
board=Depends(get_board_or_404), board: Board = Depends(get_board_or_404),
actor: ActorContext = Depends(require_admin_or_agent), actor: ActorContext = Depends(require_admin_or_agent),
since: str | None = Query(default=None), since: str | None = Query(default=None),
) -> EventSourceResponse: ) -> EventSourceResponse:
if actor.actor_type == "agent" and actor.agent: if actor.actor_type == "agent" and actor.agent:
if actor.agent.board_id and actor.agent.board_id != board.id: if actor.agent.board_id and actor.agent.board_id != board.id:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
since_dt = _parse_since(since) or datetime.utcnow() since_dt = _parse_since(since) or utcnow()
last_seen = since_dt last_seen = since_dt
async def event_generator(): async def event_generator() -> AsyncIterator[dict[str, str]]:
nonlocal last_seen nonlocal last_seen
while True: while True:
if await request.is_disconnected(): if await request.is_disconnected():
break break
memories = await run_in_threadpool(_fetch_memory_events, board.id, last_seen) async with async_session_maker() as session:
memories = await _fetch_memory_events(session, board.id, last_seen)
for memory in memories: for memory in memories:
if memory.created_at > last_seen: if memory.created_at > last_seen:
last_seen = memory.created_at last_seen = memory.created_at
@@ -208,10 +211,10 @@ async def stream_board_memory(
@router.post("", response_model=BoardMemoryRead) @router.post("", response_model=BoardMemoryRead)
def create_board_memory( async def create_board_memory(
payload: BoardMemoryCreate, payload: BoardMemoryCreate,
board=Depends(get_board_or_404), board: Board = Depends(get_board_or_404),
session: Session = Depends(get_session), session: AsyncSession = Depends(get_session),
actor: ActorContext = Depends(require_admin_or_agent), actor: ActorContext = Depends(require_admin_or_agent),
) -> BoardMemory: ) -> BoardMemory:
if actor.actor_type == "agent" and actor.agent: if actor.actor_type == "agent" and actor.agent:
@@ -231,8 +234,8 @@ def create_board_memory(
source=source, source=source,
) )
session.add(memory) session.add(memory)
session.commit() await session.commit()
session.refresh(memory) await session.refresh(memory)
if is_chat: if is_chat:
_notify_chat_targets(session=session, board=board, memory=memory, actor=actor) await _notify_chat_targets(session=session, board=board, memory=memory, actor=actor)
return memory return memory

View File

@@ -1,18 +1,19 @@
from __future__ import annotations from __future__ import annotations
import json
import logging import logging
import re import re
from datetime import datetime from datetime import datetime
from uuid import uuid4 from uuid import uuid4
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from sqlmodel import Session, select from sqlmodel import col, select
from sqlmodel.ext.asyncio.session import AsyncSession
from app.api.deps import ActorContext, get_board_or_404, require_admin_auth, require_admin_or_agent from app.api.deps import ActorContext, get_board_or_404, require_admin_auth, require_admin_or_agent
from app.core.agent_tokens import generate_agent_token, hash_agent_token from app.core.agent_tokens import generate_agent_token, hash_agent_token
from app.core.auth import AuthContext from app.core.auth import AuthContext
from app.core.config import settings from app.core.config import settings
from app.core.time import utcnow
from app.db.session import get_session from app.db.session import get_session
from app.integrations.openclaw_gateway import GatewayConfig as GatewayClientConfig from app.integrations.openclaw_gateway import GatewayConfig as GatewayClientConfig
from app.integrations.openclaw_gateway import OpenClawGatewayError, ensure_session, send_message from app.integrations.openclaw_gateway import OpenClawGatewayError, ensure_session, send_message
@@ -22,6 +23,8 @@ from app.models.boards import Board
from app.models.gateways import Gateway from app.models.gateways import Gateway
from app.schemas.board_onboarding import ( from app.schemas.board_onboarding import (
BoardOnboardingAnswer, BoardOnboardingAnswer,
BoardOnboardingAgentComplete,
BoardOnboardingAgentUpdate,
BoardOnboardingConfirm, BoardOnboardingConfirm,
BoardOnboardingRead, BoardOnboardingRead,
BoardOnboardingStart, BoardOnboardingStart,
@@ -33,10 +36,12 @@ router = APIRouter(prefix="/boards/{board_id}/onboarding", tags=["board-onboardi
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def _gateway_config(session: Session, board: Board) -> tuple[Gateway, GatewayClientConfig]: async def _gateway_config(
session: AsyncSession, board: Board
) -> tuple[Gateway, GatewayClientConfig]:
if not board.gateway_id: if not board.gateway_id:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY) raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
gateway = session.get(Gateway, board.gateway_id) gateway = await session.get(Gateway, board.gateway_id)
if gateway is None or not gateway.url or not gateway.main_session_key: if gateway is None or not gateway.url or not gateway.main_session_key:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY) raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
return gateway, GatewayClientConfig(url=gateway.url, token=gateway.token) return gateway, GatewayClientConfig(url=gateway.url, token=gateway.token)
@@ -56,21 +61,25 @@ def _lead_session_key(board: Board) -> str:
async def _ensure_lead_agent( async def _ensure_lead_agent(
session: Session, session: AsyncSession,
board: Board, board: Board,
gateway: Gateway, gateway: Gateway,
config: GatewayClientConfig, config: GatewayClientConfig,
auth: AuthContext, auth: AuthContext,
) -> Agent: ) -> Agent:
existing = session.exec( existing = (
select(Agent).where(Agent.board_id == board.id).where(Agent.is_board_lead.is_(True)) await session.exec(
select(Agent)
.where(Agent.board_id == board.id)
.where(col(Agent.is_board_lead).is_(True))
)
).first() ).first()
if existing: if existing:
if existing.name != _lead_agent_name(board): if existing.name != _lead_agent_name(board):
existing.name = _lead_agent_name(board) existing.name = _lead_agent_name(board)
session.add(existing) session.add(existing)
session.commit() await session.commit()
session.refresh(existing) await session.refresh(existing)
return existing return existing
agent = Agent( agent = Agent(
@@ -87,12 +96,12 @@ async def _ensure_lead_agent(
) )
raw_token = generate_agent_token() raw_token = generate_agent_token()
agent.agent_token_hash = hash_agent_token(raw_token) agent.agent_token_hash = hash_agent_token(raw_token)
agent.provision_requested_at = datetime.utcnow() agent.provision_requested_at = utcnow()
agent.provision_action = "provision" agent.provision_action = "provision"
agent.openclaw_session_id = _lead_session_key(board) agent.openclaw_session_id = _lead_session_key(board)
session.add(agent) session.add(agent)
session.commit() await session.commit()
session.refresh(agent) await session.refresh(agent)
try: try:
await provision_agent(agent, board, gateway, raw_token, auth.user, action="provision") await provision_agent(agent, board, gateway, raw_token, auth.user, action="provision")
@@ -114,15 +123,17 @@ async def _ensure_lead_agent(
@router.get("", response_model=BoardOnboardingRead) @router.get("", response_model=BoardOnboardingRead)
def get_onboarding( async def get_onboarding(
board: Board = Depends(get_board_or_404), board: Board = Depends(get_board_or_404),
session: Session = Depends(get_session), session: AsyncSession = Depends(get_session),
auth: AuthContext = Depends(require_admin_auth), auth: AuthContext = Depends(require_admin_auth),
) -> BoardOnboardingSession: ) -> BoardOnboardingSession:
onboarding = session.exec( onboarding = (
select(BoardOnboardingSession) await session.exec(
.where(BoardOnboardingSession.board_id == board.id) select(BoardOnboardingSession)
.order_by(BoardOnboardingSession.created_at.desc()) .where(BoardOnboardingSession.board_id == board.id)
.order_by(col(BoardOnboardingSession.created_at).desc())
)
).first() ).first()
if onboarding is None: if onboarding is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
@@ -133,18 +144,20 @@ def get_onboarding(
async def start_onboarding( async def start_onboarding(
payload: BoardOnboardingStart, payload: BoardOnboardingStart,
board: Board = Depends(get_board_or_404), board: Board = Depends(get_board_or_404),
session: Session = Depends(get_session), session: AsyncSession = Depends(get_session),
auth: AuthContext = Depends(require_admin_auth), auth: AuthContext = Depends(require_admin_auth),
) -> BoardOnboardingSession: ) -> BoardOnboardingSession:
onboarding = session.exec( onboarding = (
select(BoardOnboardingSession) await session.exec(
.where(BoardOnboardingSession.board_id == board.id) select(BoardOnboardingSession)
.where(BoardOnboardingSession.status == "active") .where(BoardOnboardingSession.board_id == board.id)
.where(BoardOnboardingSession.status == "active")
)
).first() ).first()
if onboarding: if onboarding:
return onboarding return onboarding
gateway, config = _gateway_config(session, board) gateway, config = await _gateway_config(session, board)
session_key = gateway.main_session_key session_key = gateway.main_session_key
base_url = settings.base_url or "http://localhost:8000" base_url = settings.base_url or "http://localhost:8000"
prompt = ( prompt = (
@@ -185,11 +198,11 @@ async def start_onboarding(
board_id=board.id, board_id=board.id,
session_key=session_key, session_key=session_key,
status="active", status="active",
messages=[{"role": "user", "content": prompt, "timestamp": datetime.utcnow().isoformat()}], messages=[{"role": "user", "content": prompt, "timestamp": utcnow().isoformat()}],
) )
session.add(onboarding) session.add(onboarding)
session.commit() await session.commit()
session.refresh(onboarding) await session.refresh(onboarding)
return onboarding return onboarding
@@ -197,25 +210,27 @@ async def start_onboarding(
async def answer_onboarding( async def answer_onboarding(
payload: BoardOnboardingAnswer, payload: BoardOnboardingAnswer,
board: Board = Depends(get_board_or_404), board: Board = Depends(get_board_or_404),
session: Session = Depends(get_session), session: AsyncSession = Depends(get_session),
auth: AuthContext = Depends(require_admin_auth), auth: AuthContext = Depends(require_admin_auth),
) -> BoardOnboardingSession: ) -> BoardOnboardingSession:
onboarding = session.exec( onboarding = (
select(BoardOnboardingSession) await session.exec(
.where(BoardOnboardingSession.board_id == board.id) select(BoardOnboardingSession)
.order_by(BoardOnboardingSession.created_at.desc()) .where(BoardOnboardingSession.board_id == board.id)
.order_by(col(BoardOnboardingSession.created_at).desc())
)
).first() ).first()
if onboarding is None: if onboarding is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
_, config = _gateway_config(session, board) _, config = await _gateway_config(session, board)
answer_text = payload.answer answer_text = payload.answer
if payload.other_text: if payload.other_text:
answer_text = f"{payload.answer}: {payload.other_text}" answer_text = f"{payload.answer}: {payload.other_text}"
messages = list(onboarding.messages or []) messages = list(onboarding.messages or [])
messages.append( messages.append(
{"role": "user", "content": answer_text, "timestamp": datetime.utcnow().isoformat()} {"role": "user", "content": answer_text, "timestamp": utcnow().isoformat()}
) )
try: try:
@@ -227,18 +242,18 @@ async def answer_onboarding(
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(exc)) from exc raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(exc)) from exc
onboarding.messages = messages onboarding.messages = messages
onboarding.updated_at = datetime.utcnow() onboarding.updated_at = utcnow()
session.add(onboarding) session.add(onboarding)
session.commit() await session.commit()
session.refresh(onboarding) await session.refresh(onboarding)
return onboarding return onboarding
@router.post("/agent", response_model=BoardOnboardingRead) @router.post("/agent", response_model=BoardOnboardingRead)
def agent_onboarding_update( async def agent_onboarding_update(
payload: dict[str, object], payload: BoardOnboardingAgentUpdate,
board: Board = Depends(get_board_or_404), board: Board = Depends(get_board_or_404),
session: Session = Depends(get_session), session: AsyncSession = Depends(get_session),
actor: ActorContext = Depends(require_admin_or_agent), actor: ActorContext = Depends(require_admin_or_agent),
) -> BoardOnboardingSession: ) -> BoardOnboardingSession:
if actor.actor_type != "agent" or actor.agent is None: if actor.actor_type != "agent" or actor.agent is None:
@@ -248,15 +263,17 @@ def agent_onboarding_update(
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
if board.gateway_id: if board.gateway_id:
gateway = session.get(Gateway, board.gateway_id) gateway = await session.get(Gateway, board.gateway_id)
if gateway and gateway.main_session_key and agent.openclaw_session_id: if gateway and gateway.main_session_key and agent.openclaw_session_id:
if agent.openclaw_session_id != gateway.main_session_key: if agent.openclaw_session_id != gateway.main_session_key:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
onboarding = session.exec( onboarding = (
select(BoardOnboardingSession) await session.exec(
.where(BoardOnboardingSession.board_id == board.id) select(BoardOnboardingSession)
.order_by(BoardOnboardingSession.created_at.desc()) .where(BoardOnboardingSession.board_id == board.id)
.order_by(col(BoardOnboardingSession.created_at).desc())
)
).first() ).first()
if onboarding is None: if onboarding is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
@@ -264,31 +281,27 @@ def agent_onboarding_update(
raise HTTPException(status_code=status.HTTP_409_CONFLICT) raise HTTPException(status_code=status.HTTP_409_CONFLICT)
messages = list(onboarding.messages or []) messages = list(onboarding.messages or [])
now = datetime.utcnow().isoformat() now = utcnow().isoformat()
payload_text = json.dumps(payload) payload_text = payload.model_dump_json(exclude_none=True)
payload_data = payload.model_dump(mode="json", exclude_none=True)
logger.info( logger.info(
"onboarding.agent.update board_id=%s agent_id=%s payload=%s", "onboarding.agent.update board_id=%s agent_id=%s payload=%s",
board.id, board.id,
agent.id, agent.id,
payload_text, payload_text,
) )
payload_status = payload.get("status") if isinstance(payload, BoardOnboardingAgentComplete):
if payload_status == "complete": onboarding.draft_goal = payload_data
onboarding.draft_goal = payload
onboarding.status = "completed" onboarding.status = "completed"
messages.append({"role": "assistant", "content": payload_text, "timestamp": now}) messages.append({"role": "assistant", "content": payload_text, "timestamp": now})
else: else:
question = payload.get("question")
options = payload.get("options")
if not isinstance(question, str) or not isinstance(options, list):
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
messages.append({"role": "assistant", "content": payload_text, "timestamp": now}) messages.append({"role": "assistant", "content": payload_text, "timestamp": now})
onboarding.messages = messages onboarding.messages = messages
onboarding.updated_at = datetime.utcnow() onboarding.updated_at = utcnow()
session.add(onboarding) session.add(onboarding)
session.commit() await session.commit()
session.refresh(onboarding) await session.refresh(onboarding)
logger.info( logger.info(
"onboarding.agent.update stored board_id=%s messages_count=%s status=%s", "onboarding.agent.update stored board_id=%s messages_count=%s status=%s",
board.id, board.id,
@@ -302,13 +315,15 @@ def agent_onboarding_update(
async def confirm_onboarding( async def confirm_onboarding(
payload: BoardOnboardingConfirm, payload: BoardOnboardingConfirm,
board: Board = Depends(get_board_or_404), board: Board = Depends(get_board_or_404),
session: Session = Depends(get_session), session: AsyncSession = Depends(get_session),
auth: AuthContext = Depends(require_admin_auth), auth: AuthContext = Depends(require_admin_auth),
) -> Board: ) -> Board:
onboarding = session.exec( onboarding = (
select(BoardOnboardingSession) await session.exec(
.where(BoardOnboardingSession.board_id == board.id) select(BoardOnboardingSession)
.order_by(BoardOnboardingSession.created_at.desc()) .where(BoardOnboardingSession.board_id == board.id)
.order_by(col(BoardOnboardingSession.created_at).desc())
)
).first() ).first()
if onboarding is None: if onboarding is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
@@ -321,12 +336,12 @@ async def confirm_onboarding(
board.goal_source = "lead_agent_onboarding" board.goal_source = "lead_agent_onboarding"
onboarding.status = "confirmed" onboarding.status = "confirmed"
onboarding.updated_at = datetime.utcnow() onboarding.updated_at = utcnow()
gateway, config = _gateway_config(session, board) gateway, config = await _gateway_config(session, board)
session.add(board) session.add(board)
session.add(onboarding) session.add(onboarding)
session.commit() await session.commit()
session.refresh(board) await session.refresh(board)
await _ensure_lead_agent(session, board, gateway, config, auth) await _ensure_lead_agent(session, board, gateway, config, auth)
return board return board

View File

@@ -1,15 +1,17 @@
from __future__ import annotations from __future__ import annotations
import asyncio
import re import re
from uuid import uuid4 from uuid import uuid4
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from sqlalchemy import delete from sqlalchemy import delete
from sqlmodel import Session, col, select from sqlmodel import col, select
from sqlmodel.ext.asyncio.session import AsyncSession
from app.api.deps import ActorContext, get_board_or_404, require_admin_auth, require_admin_or_agent 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.auth import AuthContext
from app.core.time import utcnow
from app.db import crud
from app.db.session import get_session from app.db.session import get_session
from app.integrations.openclaw_gateway import GatewayConfig as GatewayClientConfig from app.integrations.openclaw_gateway import GatewayConfig as GatewayClientConfig
from app.integrations.openclaw_gateway import ( from app.integrations.openclaw_gateway import (
@@ -27,6 +29,7 @@ from app.models.boards import Board
from app.models.gateways import Gateway from app.models.gateways import Gateway
from app.models.task_fingerprints import TaskFingerprint from app.models.task_fingerprints import TaskFingerprint
from app.models.tasks import Task from app.models.tasks import Task
from app.schemas.common import OkResponse
from app.schemas.boards import BoardCreate, BoardRead, BoardUpdate from app.schemas.boards import BoardCreate, BoardRead, BoardUpdate
router = APIRouter(prefix="/boards", tags=["boards"]) router = APIRouter(prefix="/boards", tags=["boards"])
@@ -43,12 +46,56 @@ def _build_session_key(agent_name: str) -> str:
return f"{AGENT_SESSION_PREFIX}:{_slugify(agent_name)}:main" return f"{AGENT_SESSION_PREFIX}:{_slugify(agent_name)}:main"
def _board_gateway( async def _require_gateway(session: AsyncSession, gateway_id: object) -> Gateway:
session: Session, board: Board gateway = await crud.get_by_id(session, Gateway, gateway_id)
if gateway is None:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="gateway_id is invalid",
)
return gateway
async def _require_gateway_for_create(
payload: BoardCreate,
session: AsyncSession = Depends(get_session),
) -> Gateway:
return await _require_gateway(session, payload.gateway_id)
async def _apply_board_update(
*,
payload: BoardUpdate,
session: AsyncSession,
board: Board,
) -> Board:
updates = payload.model_dump(exclude_unset=True)
if "gateway_id" in updates:
await _require_gateway(session, updates["gateway_id"])
for key, value in updates.items():
setattr(board, key, value)
if updates.get("board_type") == "goal":
# Validate only when explicitly switching to goal boards.
if not board.objective or not board.success_metrics:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Goal boards require objective and success_metrics",
)
if not board.gateway_id:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="gateway_id is required",
)
board.updated_at = utcnow()
return await crud.save(session, board)
async def _board_gateway(
session: AsyncSession, board: Board
) -> tuple[Gateway | None, GatewayClientConfig | None]: ) -> tuple[Gateway | None, GatewayClientConfig | None]:
if not board.gateway_id: if not board.gateway_id:
return None, None return None, None
config = session.get(Gateway, board.gateway_id) config = await session.get(Gateway, board.gateway_id)
if config is None: if config is None:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
@@ -103,36 +150,21 @@ async def _cleanup_agent_on_gateway(
@router.get("", response_model=list[BoardRead]) @router.get("", response_model=list[BoardRead])
def list_boards( async def list_boards(
session: Session = Depends(get_session), session: AsyncSession = Depends(get_session),
actor: ActorContext = Depends(require_admin_or_agent), actor: ActorContext = Depends(require_admin_or_agent),
) -> list[Board]: ) -> list[Board]:
return list(session.exec(select(Board))) return list(await session.exec(select(Board)))
@router.post("", response_model=BoardRead) @router.post("", response_model=BoardRead)
def create_board( async def create_board(
payload: BoardCreate, payload: BoardCreate,
session: Session = Depends(get_session), _gateway: Gateway = Depends(_require_gateway_for_create),
session: AsyncSession = Depends(get_session),
auth: AuthContext = Depends(require_admin_auth), auth: AuthContext = Depends(require_admin_auth),
) -> Board: ) -> Board:
data = payload.model_dump() return await crud.create(session, Board, **payload.model_dump())
if not data.get("gateway_id"):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="gateway_id is required",
)
config = session.get(Gateway, data["gateway_id"])
if config is None:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="gateway_id is invalid",
)
board = Board.model_validate(data)
session.add(board)
session.commit()
session.refresh(board)
return board
@router.get("/{board_id}", response_model=BoardRead) @router.get("/{board_id}", response_model=BoardRead)
@@ -144,60 +176,29 @@ def get_board(
@router.patch("/{board_id}", response_model=BoardRead) @router.patch("/{board_id}", response_model=BoardRead)
def update_board( async def update_board(
payload: BoardUpdate, payload: BoardUpdate,
session: Session = Depends(get_session), session: AsyncSession = Depends(get_session),
board: Board = Depends(get_board_or_404), board: Board = Depends(get_board_or_404),
auth: AuthContext = Depends(require_admin_auth), auth: AuthContext = Depends(require_admin_auth),
) -> Board: ) -> Board:
updates = payload.model_dump(exclude_unset=True) return await _apply_board_update(payload=payload, session=session, board=board)
if "gateway_id" in updates:
if not updates.get("gateway_id"):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="gateway_id is required",
)
config = session.get(Gateway, updates["gateway_id"])
if config is None:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="gateway_id is invalid",
)
for key, value in updates.items():
setattr(board, key, value)
if updates.get("board_type") == "goal":
objective = updates.get("objective") or board.objective
metrics = updates.get("success_metrics") or board.success_metrics
if not objective or not metrics:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Goal boards require objective and success_metrics",
)
if not board.gateway_id:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="gateway_id is required",
)
session.add(board)
session.commit()
session.refresh(board)
return board
@router.delete("/{board_id}") @router.delete("/{board_id}", response_model=OkResponse)
def delete_board( async def delete_board(
session: Session = Depends(get_session), session: AsyncSession = Depends(get_session),
board: Board = Depends(get_board_or_404), board: Board = Depends(get_board_or_404),
auth: AuthContext = Depends(require_admin_auth), auth: AuthContext = Depends(require_admin_auth),
) -> dict[str, bool]: ) -> OkResponse:
agents = list(session.exec(select(Agent).where(Agent.board_id == board.id))) agents = list(await session.exec(select(Agent).where(Agent.board_id == board.id)))
task_ids = list(session.exec(select(Task.id).where(Task.board_id == board.id))) task_ids = list(await session.exec(select(Task.id).where(Task.board_id == board.id)))
config, client_config = _board_gateway(session, board) config, client_config = await _board_gateway(session, board)
if config and client_config: if config and client_config:
try: try:
for agent in agents: for agent in agents:
asyncio.run(_cleanup_agent_on_gateway(agent, config, client_config)) await _cleanup_agent_on_gateway(agent, config, client_config)
except OpenClawGatewayError as exc: except OpenClawGatewayError as exc:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_502_BAD_GATEWAY, status_code=status.HTTP_502_BAD_GATEWAY,
@@ -205,18 +206,18 @@ def delete_board(
) from exc ) from exc
if task_ids: if task_ids:
session.execute(delete(ActivityEvent).where(col(ActivityEvent.task_id).in_(task_ids))) await session.execute(delete(ActivityEvent).where(col(ActivityEvent.task_id).in_(task_ids)))
session.execute(delete(TaskFingerprint).where(col(TaskFingerprint.board_id) == board.id)) await session.execute(delete(TaskFingerprint).where(col(TaskFingerprint.board_id) == board.id))
if agents: if agents:
agent_ids = [agent.id for agent in agents] agent_ids = [agent.id for agent in agents]
session.execute(delete(ActivityEvent).where(col(ActivityEvent.agent_id).in_(agent_ids))) await session.execute(delete(ActivityEvent).where(col(ActivityEvent.agent_id).in_(agent_ids)))
session.execute(delete(Agent).where(col(Agent.id).in_(agent_ids))) await session.execute(delete(Agent).where(col(Agent.id).in_(agent_ids)))
session.execute(delete(Approval).where(col(Approval.board_id) == board.id)) await session.execute(delete(Approval).where(col(Approval.board_id) == board.id))
session.execute(delete(BoardMemory).where(col(BoardMemory.board_id) == board.id)) await session.execute(delete(BoardMemory).where(col(BoardMemory.board_id) == board.id))
session.execute( await session.execute(
delete(BoardOnboardingSession).where(col(BoardOnboardingSession.board_id) == board.id) delete(BoardOnboardingSession).where(col(BoardOnboardingSession.board_id) == board.id)
) )
session.execute(delete(Task).where(col(Task.board_id) == board.id)) await session.execute(delete(Task).where(col(Task.board_id) == board.id))
session.delete(board) await session.delete(board)
session.commit() await session.commit()
return {"ok": True} return OkResponse()

View File

@@ -4,7 +4,7 @@ from dataclasses import dataclass
from typing import Literal from typing import Literal
from fastapi import Depends, HTTPException, status from fastapi import Depends, HTTPException, status
from sqlmodel import Session from sqlmodel.ext.asyncio.session import AsyncSession
from app.core.agent_auth import AgentAuthContext, get_agent_auth_context_optional from app.core.agent_auth import AgentAuthContext, get_agent_auth_context_optional
from app.core.auth import AuthContext, get_auth_context, get_auth_context_optional from app.core.auth import AuthContext, get_auth_context, get_auth_context_optional
@@ -40,22 +40,22 @@ def require_admin_or_agent(
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED) raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
def get_board_or_404( async def get_board_or_404(
board_id: str, board_id: str,
session: Session = Depends(get_session), session: AsyncSession = Depends(get_session),
) -> Board: ) -> Board:
board = session.get(Board, board_id) board = await session.get(Board, board_id)
if board is None: if board is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
return board return board
def get_task_or_404( async def get_task_or_404(
task_id: str, task_id: str,
board: Board = Depends(get_board_or_404), board: Board = Depends(get_board_or_404),
session: Session = Depends(get_session), session: AsyncSession = Depends(get_session),
) -> Task: ) -> Task:
task = session.get(Task, task_id) task = await session.get(Task, task_id)
if task is None or task.board_id != board.id: if task is None or task.board_id != board.id:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
return task return task

View File

@@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
from fastapi import APIRouter, Body, Depends, HTTPException, Query, status from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlmodel import Session from sqlmodel.ext.asyncio.session import AsyncSession
from app.core.auth import AuthContext, get_auth_context from app.core.auth import AuthContext, get_auth_context
from app.db.session import get_session from app.db.session import get_session
@@ -20,12 +20,22 @@ from app.integrations.openclaw_gateway_protocol import (
) )
from app.models.boards import Board from app.models.boards import Board
from app.models.gateways import Gateway from app.models.gateways import Gateway
from app.schemas.common import OkResponse
from app.schemas.gateway_api import (
GatewayCommandsResponse,
GatewayResolveQuery,
GatewaySessionHistoryResponse,
GatewaySessionMessageRequest,
GatewaySessionResponse,
GatewaySessionsResponse,
GatewaysStatusResponse,
)
router = APIRouter(prefix="/gateways", tags=["gateways"]) router = APIRouter(prefix="/gateways", tags=["gateways"])
def _resolve_gateway( async def _resolve_gateway(
session: Session, session: AsyncSession,
board_id: str | None, board_id: str | None,
gateway_url: str | None, gateway_url: str | None,
gateway_token: str | None, gateway_token: str | None,
@@ -42,7 +52,7 @@ def _resolve_gateway(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="board_id or gateway_url is required", detail="board_id or gateway_url is required",
) )
board = session.get(Board, board_id) board = await session.get(Board, board_id)
if board is None: if board is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Board not found") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Board not found")
if not board.gateway_id: if not board.gateway_id:
@@ -50,7 +60,7 @@ def _resolve_gateway(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Board gateway_id is required", detail="Board gateway_id is required",
) )
gateway = session.get(Gateway, board.gateway_id) gateway = await session.get(Gateway, board.gateway_id)
if gateway is None: if gateway is None:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
@@ -68,10 +78,10 @@ def _resolve_gateway(
) )
def _require_gateway( async def _require_gateway(
session: Session, board_id: str | None session: AsyncSession, board_id: str | None
) -> tuple[Board, GatewayClientConfig, str | None]: ) -> tuple[Board, GatewayClientConfig, str | None]:
board, config, main_session = _resolve_gateway(session, board_id, None, None, None) board, config, main_session = await _resolve_gateway(session, board_id, None, None, None)
if board is None: if board is None:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
@@ -80,21 +90,18 @@ def _require_gateway(
return board, config, main_session return board, config, main_session
@router.get("/status") @router.get("/status", response_model=GatewaysStatusResponse)
async def gateways_status( async def gateways_status(
board_id: str | None = Query(default=None), params: GatewayResolveQuery = Depends(),
gateway_url: str | None = Query(default=None), session: AsyncSession = Depends(get_session),
gateway_token: str | None = Query(default=None),
gateway_main_session_key: str | None = Query(default=None),
session: Session = Depends(get_session),
auth: AuthContext = Depends(get_auth_context), auth: AuthContext = Depends(get_auth_context),
) -> dict[str, object]: ) -> GatewaysStatusResponse:
board, config, main_session = _resolve_gateway( board, config, main_session = await _resolve_gateway(
session, session,
board_id, params.board_id,
gateway_url, params.gateway_url,
gateway_token, params.gateway_token,
gateway_main_session_key, params.gateway_main_session_key,
) )
try: try:
sessions = await openclaw_call("sessions.list", config=config) sessions = await openclaw_call("sessions.list", config=config)
@@ -111,30 +118,26 @@ async def gateways_status(
main_session_entry = ensured.get("entry") or ensured main_session_entry = ensured.get("entry") or ensured
except OpenClawGatewayError as exc: except OpenClawGatewayError as exc:
main_session_error = str(exc) main_session_error = str(exc)
return { return GatewaysStatusResponse(
"connected": True, connected=True,
"gateway_url": config.url, gateway_url=config.url,
"sessions_count": len(sessions_list), sessions_count=len(sessions_list),
"sessions": sessions_list, sessions=sessions_list,
"main_session_key": main_session, main_session_key=main_session,
"main_session": main_session_entry, main_session=main_session_entry,
"main_session_error": main_session_error, main_session_error=main_session_error,
} )
except OpenClawGatewayError as exc: except OpenClawGatewayError as exc:
return { return GatewaysStatusResponse(connected=False, gateway_url=config.url, error=str(exc))
"connected": False,
"gateway_url": config.url,
"error": str(exc),
}
@router.get("/sessions") @router.get("/sessions", response_model=GatewaySessionsResponse)
async def list_gateway_sessions( async def list_gateway_sessions(
board_id: str | None = Query(default=None), board_id: str | None = Query(default=None),
session: Session = Depends(get_session), session: AsyncSession = Depends(get_session),
auth: AuthContext = Depends(get_auth_context), auth: AuthContext = Depends(get_auth_context),
) -> dict[str, object]: ) -> GatewaySessionsResponse:
board, config, main_session = _resolve_gateway( board, config, main_session = await _resolve_gateway(
session, session,
board_id, board_id,
None, None,
@@ -159,21 +162,21 @@ async def list_gateway_sessions(
except OpenClawGatewayError: except OpenClawGatewayError:
main_session_entry = None main_session_entry = None
return { return GatewaySessionsResponse(
"sessions": sessions_list, sessions=sessions_list,
"main_session_key": main_session, main_session_key=main_session,
"main_session": main_session_entry, main_session=main_session_entry,
} )
@router.get("/sessions/{session_id}") @router.get("/sessions/{session_id}", response_model=GatewaySessionResponse)
async def get_gateway_session( async def get_gateway_session(
session_id: str, session_id: str,
board_id: str | None = Query(default=None), board_id: str | None = Query(default=None),
session: Session = Depends(get_session), session: AsyncSession = Depends(get_session),
auth: AuthContext = Depends(get_auth_context), auth: AuthContext = Depends(get_auth_context),
) -> dict[str, object]: ) -> GatewaySessionResponse:
board, config, main_session = _resolve_gateway( board, config, main_session = await _resolve_gateway(
session, session,
board_id, board_id,
None, None,
@@ -208,55 +211,50 @@ async def get_gateway_session(
session_entry = None session_entry = None
if session_entry is None: if session_entry is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Session not found") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Session not found")
return {"session": session_entry} return GatewaySessionResponse(session=session_entry)
@router.get("/sessions/{session_id}/history") @router.get("/sessions/{session_id}/history", response_model=GatewaySessionHistoryResponse)
async def get_session_history( async def get_session_history(
session_id: str, session_id: str,
board_id: str | None = Query(default=None), board_id: str | None = Query(default=None),
session: Session = Depends(get_session), session: AsyncSession = Depends(get_session),
auth: AuthContext = Depends(get_auth_context), auth: AuthContext = Depends(get_auth_context),
) -> dict[str, object]: ) -> GatewaySessionHistoryResponse:
_, config, _ = _require_gateway(session, board_id) _, config, _ = await _require_gateway(session, board_id)
try: try:
history = await get_chat_history(session_id, config=config) history = await get_chat_history(session_id, config=config)
except OpenClawGatewayError as exc: except OpenClawGatewayError as exc:
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(exc)) from exc raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(exc)) from exc
if isinstance(history, dict) and isinstance(history.get("messages"), list): if isinstance(history, dict) and isinstance(history.get("messages"), list):
return {"history": history["messages"]} return GatewaySessionHistoryResponse(history=history["messages"])
return {"history": list(history or [])} return GatewaySessionHistoryResponse(history=list(history or []))
@router.post("/sessions/{session_id}/message") @router.post("/sessions/{session_id}/message", response_model=OkResponse)
async def send_gateway_session_message( async def send_gateway_session_message(
session_id: str, session_id: str,
payload: dict = Body(...), payload: GatewaySessionMessageRequest,
board_id: str | None = Query(default=None), board_id: str | None = Query(default=None),
session: Session = Depends(get_session), session: AsyncSession = Depends(get_session),
auth: AuthContext = Depends(get_auth_context), auth: AuthContext = Depends(get_auth_context),
) -> dict[str, bool]: ) -> OkResponse:
content = payload.get("content") board, config, main_session = await _require_gateway(session, board_id)
if not content:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail="content is required"
)
board, config, main_session = _require_gateway(session, board_id)
try: try:
if main_session and session_id == main_session: if main_session and session_id == main_session:
await ensure_session(main_session, config=config, label="Main Agent") await ensure_session(main_session, config=config, label="Main Agent")
await send_message(content, session_key=session_id, config=config) await send_message(payload.content, session_key=session_id, config=config)
except OpenClawGatewayError as exc: except OpenClawGatewayError as exc:
raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(exc)) from exc raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(exc)) from exc
return {"ok": True} return OkResponse()
@router.get("/commands") @router.get("/commands", response_model=GatewayCommandsResponse)
async def gateway_commands( async def gateway_commands(
auth: AuthContext = Depends(get_auth_context), auth: AuthContext = Depends(get_auth_context),
) -> dict[str, object]: ) -> GatewayCommandsResponse:
return { return GatewayCommandsResponse(
"protocol_version": PROTOCOL_VERSION, protocol_version=PROTOCOL_VERSION,
"methods": GATEWAY_METHODS, methods=GATEWAY_METHODS,
"events": GATEWAY_EVENTS, events=GATEWAY_EVENTS,
} )

View File

@@ -4,15 +4,18 @@ from datetime import datetime
from uuid import UUID from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from sqlmodel import Session, select from sqlmodel import select
from sqlmodel.ext.asyncio.session import AsyncSession
from app.core.agent_tokens import generate_agent_token, hash_agent_token from app.core.agent_tokens import generate_agent_token, hash_agent_token
from app.core.auth import AuthContext, get_auth_context from app.core.auth import AuthContext, get_auth_context
from app.core.time import utcnow
from app.db.session import get_session from app.db.session import get_session
from app.integrations.openclaw_gateway import GatewayConfig as GatewayClientConfig from app.integrations.openclaw_gateway import GatewayConfig as GatewayClientConfig
from app.integrations.openclaw_gateway import OpenClawGatewayError, ensure_session, send_message from app.integrations.openclaw_gateway import OpenClawGatewayError, ensure_session, send_message
from app.models.agents import Agent from app.models.agents import Agent
from app.models.gateways import Gateway from app.models.gateways import Gateway
from app.schemas.common import OkResponse
from app.schemas.gateways import GatewayCreate, GatewayRead, GatewayUpdate from app.schemas.gateways import GatewayCreate, GatewayRead, GatewayUpdate
from app.services.agent_provisioning import DEFAULT_HEARTBEAT_CONFIG, provision_main_agent from app.services.agent_provisioning import DEFAULT_HEARTBEAT_CONFIG, provision_main_agent
@@ -235,21 +238,25 @@ def _main_agent_name(gateway: Gateway) -> str:
return f"{gateway.name} Main" return f"{gateway.name} Main"
def _find_main_agent( async def _find_main_agent(
session: Session, session: AsyncSession,
gateway: Gateway, gateway: Gateway,
previous_name: str | None = None, previous_name: str | None = None,
previous_session_key: str | None = None, previous_session_key: str | None = None,
) -> Agent | None: ) -> Agent | None:
if gateway.main_session_key: if gateway.main_session_key:
agent = session.exec( agent = (
select(Agent).where(Agent.openclaw_session_id == gateway.main_session_key) await session.exec(
select(Agent).where(Agent.openclaw_session_id == gateway.main_session_key)
)
).first() ).first()
if agent: if agent:
return agent return agent
if previous_session_key: if previous_session_key:
agent = session.exec( agent = (
select(Agent).where(Agent.openclaw_session_id == previous_session_key) await session.exec(
select(Agent).where(Agent.openclaw_session_id == previous_session_key)
)
).first() ).first()
if agent: if agent:
return agent return agent
@@ -257,14 +264,14 @@ def _find_main_agent(
if previous_name: if previous_name:
names.add(f"{previous_name} Main") names.add(f"{previous_name} Main")
for name in names: for name in names:
agent = session.exec(select(Agent).where(Agent.name == name)).first() agent = (await session.exec(select(Agent).where(Agent.name == name))).first()
if agent: if agent:
return agent return agent
return None return None
async def _ensure_main_agent( async def _ensure_main_agent(
session: Session, session: AsyncSession,
gateway: Gateway, gateway: Gateway,
auth: AuthContext, auth: AuthContext,
*, *,
@@ -274,7 +281,7 @@ async def _ensure_main_agent(
) -> Agent | None: ) -> Agent | None:
if not gateway.url or not gateway.main_session_key: if not gateway.url or not gateway.main_session_key:
return None return None
agent = _find_main_agent(session, gateway, previous_name, previous_session_key) agent = await _find_main_agent(session, gateway, previous_name, previous_session_key)
if agent is None: if agent is None:
agent = Agent( agent = Agent(
name=_main_agent_name(gateway), name=_main_agent_name(gateway),
@@ -294,14 +301,14 @@ async def _ensure_main_agent(
agent.openclaw_session_id = gateway.main_session_key agent.openclaw_session_id = gateway.main_session_key
raw_token = generate_agent_token() raw_token = generate_agent_token()
agent.agent_token_hash = hash_agent_token(raw_token) agent.agent_token_hash = hash_agent_token(raw_token)
agent.provision_requested_at = datetime.utcnow() agent.provision_requested_at = utcnow()
agent.provision_action = action agent.provision_action = action
agent.updated_at = datetime.utcnow() agent.updated_at = utcnow()
if agent.heartbeat_config is None: if agent.heartbeat_config is None:
agent.heartbeat_config = DEFAULT_HEARTBEAT_CONFIG.copy() agent.heartbeat_config = DEFAULT_HEARTBEAT_CONFIG.copy()
session.add(agent) session.add(agent)
session.commit() await session.commit()
session.refresh(agent) await session.refresh(agent)
try: try:
await provision_main_agent(agent, gateway, raw_token, auth.user, action=action) await provision_main_agent(agent, gateway, raw_token, auth.user, action=action)
await ensure_session( await ensure_session(
@@ -356,26 +363,24 @@ async def _send_skyll_disable_message(gateway: Gateway) -> None:
@router.get("", response_model=list[GatewayRead]) @router.get("", response_model=list[GatewayRead])
def list_gateways( async def list_gateways(
session: Session = Depends(get_session), session: AsyncSession = Depends(get_session),
auth: AuthContext = Depends(get_auth_context), auth: AuthContext = Depends(get_auth_context),
) -> list[Gateway]: ) -> list[Gateway]:
return list(session.exec(select(Gateway))) return list(await session.exec(select(Gateway)))
@router.post("", response_model=GatewayRead) @router.post("", response_model=GatewayRead)
async def create_gateway( async def create_gateway(
payload: GatewayCreate, payload: GatewayCreate,
session: Session = Depends(get_session), session: AsyncSession = Depends(get_session),
auth: AuthContext = Depends(get_auth_context), auth: AuthContext = Depends(get_auth_context),
) -> Gateway: ) -> Gateway:
data = payload.model_dump() data = payload.model_dump()
if data.get("token") == "":
data["token"] = None
gateway = Gateway.model_validate(data) gateway = Gateway.model_validate(data)
session.add(gateway) session.add(gateway)
session.commit() await session.commit()
session.refresh(gateway) await session.refresh(gateway)
await _ensure_main_agent(session, gateway, auth, action="provision") await _ensure_main_agent(session, gateway, auth, action="provision")
if gateway.skyll_enabled: if gateway.skyll_enabled:
try: try:
@@ -386,12 +391,12 @@ async def create_gateway(
@router.get("/{gateway_id}", response_model=GatewayRead) @router.get("/{gateway_id}", response_model=GatewayRead)
def get_gateway( async def get_gateway(
gateway_id: UUID, gateway_id: UUID,
session: Session = Depends(get_session), session: AsyncSession = Depends(get_session),
auth: AuthContext = Depends(get_auth_context), auth: AuthContext = Depends(get_auth_context),
) -> Gateway: ) -> Gateway:
gateway = session.get(Gateway, gateway_id) gateway = await session.get(Gateway, gateway_id)
if gateway is None: if gateway is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Gateway not found") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Gateway not found")
return gateway return gateway
@@ -401,23 +406,21 @@ def get_gateway(
async def update_gateway( async def update_gateway(
gateway_id: UUID, gateway_id: UUID,
payload: GatewayUpdate, payload: GatewayUpdate,
session: Session = Depends(get_session), session: AsyncSession = Depends(get_session),
auth: AuthContext = Depends(get_auth_context), auth: AuthContext = Depends(get_auth_context),
) -> Gateway: ) -> Gateway:
gateway = session.get(Gateway, gateway_id) gateway = await session.get(Gateway, gateway_id)
if gateway is None: if gateway is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Gateway not found") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Gateway not found")
previous_name = gateway.name previous_name = gateway.name
previous_session_key = gateway.main_session_key previous_session_key = gateway.main_session_key
previous_skyll_enabled = gateway.skyll_enabled previous_skyll_enabled = gateway.skyll_enabled
updates = payload.model_dump(exclude_unset=True) updates = payload.model_dump(exclude_unset=True)
if updates.get("token") == "":
updates["token"] = None
for key, value in updates.items(): for key, value in updates.items():
setattr(gateway, key, value) setattr(gateway, key, value)
session.add(gateway) session.add(gateway)
session.commit() await session.commit()
session.refresh(gateway) await session.refresh(gateway)
await _ensure_main_agent( await _ensure_main_agent(
session, session,
gateway, gateway,
@@ -439,15 +442,15 @@ async def update_gateway(
return gateway return gateway
@router.delete("/{gateway_id}") @router.delete("/{gateway_id}", response_model=OkResponse)
def delete_gateway( async def delete_gateway(
gateway_id: UUID, gateway_id: UUID,
session: Session = Depends(get_session), session: AsyncSession = Depends(get_session),
auth: AuthContext = Depends(get_auth_context), auth: AuthContext = Depends(get_auth_context),
) -> dict[str, bool]: ) -> OkResponse:
gateway = session.get(Gateway, gateway_id) gateway = await session.get(Gateway, gateway_id)
if gateway is None: if gateway is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Gateway not found") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Gateway not found")
session.delete(gateway) await session.delete(gateway)
session.commit() await session.commit()
return {"ok": True} return OkResponse()

View File

@@ -6,10 +6,12 @@ from typing import Literal
from fastapi import APIRouter, Depends, Query from fastapi import APIRouter, Depends, Query
from sqlalchemy import DateTime, case, cast, func from sqlalchemy import DateTime, case, cast, func
from sqlmodel import Session, col, select from sqlmodel import col, select
from sqlmodel.ext.asyncio.session import AsyncSession
from app.api.deps import require_admin_auth from app.api.deps import require_admin_auth
from app.core.auth import AuthContext from app.core.auth import AuthContext
from app.core.time import utcnow
from app.db.session import get_session from app.db.session import get_session
from app.models.activity_events import ActivityEvent from app.models.activity_events import ActivityEvent
from app.models.agents import Agent from app.models.agents import Agent
@@ -40,7 +42,7 @@ class RangeSpec:
def _resolve_range(range_key: Literal["24h", "7d"]) -> RangeSpec: def _resolve_range(range_key: Literal["24h", "7d"]) -> RangeSpec:
now = datetime.utcnow() now = utcnow()
if range_key == "7d": if range_key == "7d":
return RangeSpec( return RangeSpec(
key="7d", key="7d",
@@ -111,7 +113,7 @@ def _wip_series_from_mapping(
) )
def _query_throughput(session: Session, range_spec: RangeSpec) -> DashboardRangeSeries: async def _query_throughput(session: AsyncSession, range_spec: RangeSpec) -> DashboardRangeSeries:
bucket_col = func.date_trunc(range_spec.bucket, Task.updated_at).label("bucket") bucket_col = func.date_trunc(range_spec.bucket, Task.updated_at).label("bucket")
statement = ( statement = (
select(bucket_col, func.count()) select(bucket_col, func.count())
@@ -121,12 +123,12 @@ def _query_throughput(session: Session, range_spec: RangeSpec) -> DashboardRange
.group_by(bucket_col) .group_by(bucket_col)
.order_by(bucket_col) .order_by(bucket_col)
) )
results = session.exec(statement).all() results = (await session.exec(statement)).all()
mapping = {row[0]: float(row[1]) for row in results} mapping = {row[0]: float(row[1]) for row in results}
return _series_from_mapping(range_spec, mapping) return _series_from_mapping(range_spec, mapping)
def _query_cycle_time(session: Session, range_spec: RangeSpec) -> DashboardRangeSeries: async def _query_cycle_time(session: AsyncSession, range_spec: RangeSpec) -> DashboardRangeSeries:
bucket_col = func.date_trunc(range_spec.bucket, Task.updated_at).label("bucket") bucket_col = func.date_trunc(range_spec.bucket, Task.updated_at).label("bucket")
in_progress = cast(Task.in_progress_at, DateTime) in_progress = cast(Task.in_progress_at, DateTime)
duration_hours = func.extract("epoch", Task.updated_at - in_progress) / 3600.0 duration_hours = func.extract("epoch", Task.updated_at - in_progress) / 3600.0
@@ -139,12 +141,12 @@ def _query_cycle_time(session: Session, range_spec: RangeSpec) -> DashboardRange
.group_by(bucket_col) .group_by(bucket_col)
.order_by(bucket_col) .order_by(bucket_col)
) )
results = session.exec(statement).all() results = (await session.exec(statement)).all()
mapping = {row[0]: float(row[1] or 0) for row in results} mapping = {row[0]: float(row[1] or 0) for row in results}
return _series_from_mapping(range_spec, mapping) return _series_from_mapping(range_spec, mapping)
def _query_error_rate(session: Session, range_spec: RangeSpec) -> DashboardRangeSeries: async def _query_error_rate(session: AsyncSession, range_spec: RangeSpec) -> DashboardRangeSeries:
bucket_col = func.date_trunc(range_spec.bucket, ActivityEvent.created_at).label("bucket") bucket_col = func.date_trunc(range_spec.bucket, ActivityEvent.created_at).label("bucket")
error_case = case( error_case = case(
( (
@@ -160,7 +162,7 @@ def _query_error_rate(session: Session, range_spec: RangeSpec) -> DashboardRange
.group_by(bucket_col) .group_by(bucket_col)
.order_by(bucket_col) .order_by(bucket_col)
) )
results = session.exec(statement).all() results = (await session.exec(statement)).all()
mapping: dict[datetime, float] = {} mapping: dict[datetime, float] = {}
for bucket, errors, total in results: for bucket, errors, total in results:
total_count = float(total or 0) total_count = float(total or 0)
@@ -170,7 +172,7 @@ def _query_error_rate(session: Session, range_spec: RangeSpec) -> DashboardRange
return _series_from_mapping(range_spec, mapping) return _series_from_mapping(range_spec, mapping)
def _query_wip(session: Session, range_spec: RangeSpec) -> DashboardWipRangeSeries: async def _query_wip(session: AsyncSession, range_spec: RangeSpec) -> DashboardWipRangeSeries:
bucket_col = func.date_trunc(range_spec.bucket, Task.updated_at).label("bucket") bucket_col = func.date_trunc(range_spec.bucket, Task.updated_at).label("bucket")
inbox_case = case((col(Task.status) == "inbox", 1), else_=0) inbox_case = case((col(Task.status) == "inbox", 1), else_=0)
progress_case = case((col(Task.status) == "in_progress", 1), else_=0) progress_case = case((col(Task.status) == "in_progress", 1), else_=0)
@@ -187,7 +189,7 @@ def _query_wip(session: Session, range_spec: RangeSpec) -> DashboardWipRangeSeri
.group_by(bucket_col) .group_by(bucket_col)
.order_by(bucket_col) .order_by(bucket_col)
) )
results = session.exec(statement).all() results = (await session.exec(statement)).all()
mapping: dict[datetime, dict[str, int]] = {} mapping: dict[datetime, dict[str, int]] = {}
for bucket, inbox, in_progress, review in results: for bucket, inbox, in_progress, review in results:
mapping[bucket] = { mapping[bucket] = {
@@ -198,8 +200,8 @@ def _query_wip(session: Session, range_spec: RangeSpec) -> DashboardWipRangeSeri
return _wip_series_from_mapping(range_spec, mapping) return _wip_series_from_mapping(range_spec, mapping)
def _median_cycle_time_7d(session: Session) -> float | None: async def _median_cycle_time_7d(session: AsyncSession) -> float | None:
now = datetime.utcnow() now = utcnow()
start = now - timedelta(days=7) start = now - timedelta(days=7)
in_progress = cast(Task.in_progress_at, DateTime) in_progress = cast(Task.in_progress_at, DateTime)
duration_hours = func.extract("epoch", Task.updated_at - in_progress) / 3600.0 duration_hours = func.extract("epoch", Task.updated_at - in_progress) / 3600.0
@@ -210,7 +212,7 @@ def _median_cycle_time_7d(session: Session) -> float | None:
.where(col(Task.updated_at) >= start) .where(col(Task.updated_at) >= start)
.where(col(Task.updated_at) <= now) .where(col(Task.updated_at) <= now)
) )
value = session.exec(statement).one_or_none() value = (await session.exec(statement)).one_or_none()
if value is None: if value is None:
return None return None
if isinstance(value, tuple): if isinstance(value, tuple):
@@ -220,7 +222,7 @@ def _median_cycle_time_7d(session: Session) -> float | None:
return float(value) return float(value)
def _error_rate_kpi(session: Session, range_spec: RangeSpec) -> float: async def _error_rate_kpi(session: AsyncSession, range_spec: RangeSpec) -> float:
error_case = case( error_case = case(
( (
col(ActivityEvent.event_type).like(ERROR_EVENT_PATTERN), col(ActivityEvent.event_type).like(ERROR_EVENT_PATTERN),
@@ -233,7 +235,7 @@ def _error_rate_kpi(session: Session, range_spec: RangeSpec) -> float:
.where(col(ActivityEvent.created_at) >= range_spec.start) .where(col(ActivityEvent.created_at) >= range_spec.start)
.where(col(ActivityEvent.created_at) <= range_spec.end) .where(col(ActivityEvent.created_at) <= range_spec.end)
) )
result = session.exec(statement).one_or_none() result = (await session.exec(statement)).one_or_none()
if result is None: if result is None:
return 0.0 return 0.0
errors, total = result errors, total = result
@@ -242,58 +244,66 @@ def _error_rate_kpi(session: Session, range_spec: RangeSpec) -> float:
return (error_count / total_count) * 100 if total_count > 0 else 0.0 return (error_count / total_count) * 100 if total_count > 0 else 0.0
def _active_agents(session: Session) -> int: async def _active_agents(session: AsyncSession) -> int:
threshold = datetime.utcnow() - OFFLINE_AFTER threshold = utcnow() - OFFLINE_AFTER
statement = select(func.count()).where( statement = select(func.count()).where(
col(Agent.last_seen_at).is_not(None), col(Agent.last_seen_at).is_not(None),
col(Agent.last_seen_at) >= threshold, col(Agent.last_seen_at) >= threshold,
) )
result = session.exec(statement).one() result = (await session.exec(statement)).one()
return int(result) return int(result)
def _tasks_in_progress(session: Session) -> int: async def _tasks_in_progress(session: AsyncSession) -> int:
statement = select(func.count()).where(col(Task.status) == "in_progress") statement = select(func.count()).where(col(Task.status) == "in_progress")
result = session.exec(statement).one() result = (await session.exec(statement)).one()
return int(result) return int(result)
@router.get("/dashboard", response_model=DashboardMetrics) @router.get("/dashboard", response_model=DashboardMetrics)
def dashboard_metrics( async def dashboard_metrics(
range: Literal["24h", "7d"] = Query(default="24h"), range: Literal["24h", "7d"] = Query(default="24h"),
session: Session = Depends(get_session), session: AsyncSession = Depends(get_session),
auth: AuthContext = Depends(require_admin_auth), auth: AuthContext = Depends(require_admin_auth),
) -> DashboardMetrics: ) -> DashboardMetrics:
primary = _resolve_range(range) primary = _resolve_range(range)
comparison = _comparison_range(range) comparison = _comparison_range(range)
throughput_primary = await _query_throughput(session, primary)
throughput_comparison = await _query_throughput(session, comparison)
throughput = DashboardSeriesSet( throughput = DashboardSeriesSet(
primary=_query_throughput(session, primary), primary=throughput_primary,
comparison=_query_throughput(session, comparison), comparison=throughput_comparison,
) )
cycle_time_primary = await _query_cycle_time(session, primary)
cycle_time_comparison = await _query_cycle_time(session, comparison)
cycle_time = DashboardSeriesSet( cycle_time = DashboardSeriesSet(
primary=_query_cycle_time(session, primary), primary=cycle_time_primary,
comparison=_query_cycle_time(session, comparison), comparison=cycle_time_comparison,
) )
error_rate_primary = await _query_error_rate(session, primary)
error_rate_comparison = await _query_error_rate(session, comparison)
error_rate = DashboardSeriesSet( error_rate = DashboardSeriesSet(
primary=_query_error_rate(session, primary), primary=error_rate_primary,
comparison=_query_error_rate(session, comparison), comparison=error_rate_comparison,
) )
wip_primary = await _query_wip(session, primary)
wip_comparison = await _query_wip(session, comparison)
wip = DashboardWipSeriesSet( wip = DashboardWipSeriesSet(
primary=_query_wip(session, primary), primary=wip_primary,
comparison=_query_wip(session, comparison), comparison=wip_comparison,
) )
kpis = DashboardKpis( kpis = DashboardKpis(
active_agents=_active_agents(session), active_agents=await _active_agents(session),
tasks_in_progress=_tasks_in_progress(session), tasks_in_progress=await _tasks_in_progress(session),
error_rate_pct=_error_rate_kpi(session, primary), error_rate_pct=await _error_rate_kpi(session, primary),
median_cycle_time_hours_7d=_median_cycle_time_7d(session), median_cycle_time_hours_7d=await _median_cycle_time_7d(session),
) )
return DashboardMetrics( return DashboardMetrics(
range=primary.key, range=primary.key,
generated_at=datetime.utcnow(), generated_at=utcnow(),
kpis=kpis, kpis=kpis,
throughput=throughput, throughput=throughput,
cycle_time=cycle_time, cycle_time=cycle_time,

View File

@@ -4,14 +4,17 @@ import asyncio
import json import json
import re import re
from collections import deque from collections import deque
from collections.abc import AsyncIterator
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import cast
from uuid import UUID from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, Query, Request, status from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
from sqlalchemy import asc, delete, desc from sqlalchemy import asc, delete, desc
from sqlmodel import Session, col, select from sqlmodel import col, select
from sqlmodel.sql.expression import Select
from sqlmodel.ext.asyncio.session import AsyncSession
from sse_starlette.sse import EventSourceResponse from sse_starlette.sse import EventSourceResponse
from starlette.concurrency import run_in_threadpool
from app.api.deps import ( from app.api.deps import (
ActorContext, ActorContext,
@@ -21,7 +24,8 @@ from app.api.deps import (
require_admin_or_agent, require_admin_or_agent,
) )
from app.core.auth import AuthContext from app.core.auth import AuthContext
from app.db.session import engine, get_session from app.core.time import utcnow
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 GatewayConfig as GatewayClientConfig
from app.integrations.openclaw_gateway import OpenClawGatewayError, ensure_session, send_message from app.integrations.openclaw_gateway import OpenClawGatewayError, ensure_session, send_message
from app.models.activity_events import ActivityEvent from app.models.activity_events import ActivityEvent
@@ -30,6 +34,7 @@ from app.models.boards import Board
from app.models.gateways import Gateway from app.models.gateways import Gateway
from app.models.task_fingerprints import TaskFingerprint from app.models.task_fingerprints import TaskFingerprint
from app.models.tasks import Task from app.models.tasks import Task
from app.schemas.common import OkResponse
from app.schemas.tasks import TaskCommentCreate, TaskCommentRead, TaskCreate, TaskRead, TaskUpdate from app.schemas.tasks import TaskCommentCreate, TaskCommentRead, TaskCreate, TaskRead, TaskUpdate
from app.services.activity_log import record_activity from app.services.activity_log import record_activity
@@ -46,14 +51,6 @@ SSE_SEEN_MAX = 2000
MENTION_PATTERN = re.compile(r"@([A-Za-z][\w-]{0,31})") MENTION_PATTERN = re.compile(r"@([A-Za-z][\w-]{0,31})")
def validate_task_status(status_value: str) -> None:
if status_value not in ALLOWED_STATUSES:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Unsupported task status.",
)
def _comment_validation_error() -> HTTPException: def _comment_validation_error() -> HTTPException:
return HTTPException( return HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
@@ -61,8 +58,8 @@ def _comment_validation_error() -> HTTPException:
) )
def has_valid_recent_comment( async def has_valid_recent_comment(
session: Session, session: AsyncSession,
task: Task, task: Task,
agent_id: UUID | None, agent_id: UUID | None,
since: datetime | None, since: datetime | None,
@@ -77,7 +74,7 @@ def has_valid_recent_comment(
.where(col(ActivityEvent.created_at) >= since) .where(col(ActivityEvent.created_at) >= since)
.order_by(desc(col(ActivityEvent.created_at))) .order_by(desc(col(ActivityEvent.created_at)))
) )
event = session.exec(statement).first() event = (await session.exec(statement)).first()
if event is None or event.message is None: if event is None or event.message is None:
return False return False
return bool(event.message.strip()) return bool(event.message.strip())
@@ -116,8 +113,8 @@ def _matches_mention(agent: Agent, mentions: set[str]) -> bool:
return first in mentions return first in mentions
def _lead_was_mentioned( async def _lead_was_mentioned(
session: Session, session: AsyncSession,
task: Task, task: Task,
lead: Agent, lead: Agent,
) -> bool: ) -> bool:
@@ -127,7 +124,7 @@ def _lead_was_mentioned(
.where(col(ActivityEvent.event_type) == "task.comment") .where(col(ActivityEvent.event_type) == "task.comment")
.order_by(desc(col(ActivityEvent.created_at))) .order_by(desc(col(ActivityEvent.created_at)))
) )
for message in session.exec(statement): for message in await session.exec(statement):
if not message: if not message:
continue continue
mentions = _extract_mentions(message) mentions = _extract_mentions(message)
@@ -142,23 +139,24 @@ def _lead_created_task(task: Task, lead: Agent) -> bool:
return task.auto_reason == f"lead_agent:{lead.id}" return task.auto_reason == f"lead_agent:{lead.id}"
def _fetch_task_events( async def _fetch_task_events(
session: AsyncSession,
board_id: UUID, board_id: UUID,
since: datetime, since: datetime,
) -> list[tuple[ActivityEvent, Task | None]]: ) -> list[tuple[ActivityEvent, Task | None]]:
with Session(engine) as session: task_ids = list(await session.exec(select(Task.id).where(col(Task.board_id) == board_id)))
task_ids = list(session.exec(select(Task.id).where(col(Task.board_id) == board_id))) if not task_ids:
if not task_ids: return []
return [] statement = cast(
statement = ( Select[tuple[ActivityEvent, Task | None]],
select(ActivityEvent, Task) select(ActivityEvent, Task)
.outerjoin(Task, ActivityEvent.task_id == Task.id) .outerjoin(Task, col(ActivityEvent.task_id) == col(Task.id))
.where(col(ActivityEvent.task_id).in_(task_ids)) .where(col(ActivityEvent.task_id).in_(task_ids))
.where(col(ActivityEvent.event_type).in_(TASK_EVENT_TYPES)) .where(col(ActivityEvent.event_type).in_(TASK_EVENT_TYPES))
.where(col(ActivityEvent.created_at) >= since) .where(col(ActivityEvent.created_at) >= since)
.order_by(asc(col(ActivityEvent.created_at))) .order_by(asc(col(ActivityEvent.created_at))),
) )
return list(session.exec(statement)) return list(await session.exec(statement))
def _serialize_task(task: Task | None) -> dict[str, object] | None: def _serialize_task(task: Task | None) -> dict[str, object] | None:
@@ -171,10 +169,10 @@ def _serialize_comment(event: ActivityEvent) -> dict[str, object]:
return TaskCommentRead.model_validate(event).model_dump(mode="json") return TaskCommentRead.model_validate(event).model_dump(mode="json")
def _gateway_config(session: Session, board: Board) -> GatewayClientConfig | None: async def _gateway_config(session: AsyncSession, board: Board) -> GatewayClientConfig | None:
if not board.gateway_id: if not board.gateway_id:
return None return None
gateway = session.get(Gateway, board.gateway_id) gateway = await session.get(Gateway, board.gateway_id)
if gateway is None or not gateway.url: if gateway is None or not gateway.url:
return None return None
return GatewayClientConfig(url=gateway.url, token=gateway.token) return GatewayClientConfig(url=gateway.url, token=gateway.token)
@@ -201,16 +199,16 @@ async def _send_agent_task_message(
await send_message(message, session_key=session_key, config=config, deliver=False) await send_message(message, session_key=session_key, config=config, deliver=False)
def _notify_agent_on_task_assign( async def _notify_agent_on_task_assign(
*, *,
session: Session, session: AsyncSession,
board: Board, board: Board,
task: Task, task: Task,
agent: Agent, agent: Agent,
) -> None: ) -> None:
if not agent.openclaw_session_id: if not agent.openclaw_session_id:
return return
config = _gateway_config(session, board) config = await _gateway_config(session, board)
if config is None: if config is None:
return return
description = (task.description or "").strip() description = (task.description or "").strip()
@@ -230,13 +228,11 @@ def _notify_agent_on_task_assign(
+ "\n\nTake action: open the task and begin work. Post updates as task comments." + "\n\nTake action: open the task and begin work. Post updates as task comments."
) )
try: try:
asyncio.run( await _send_agent_task_message(
_send_agent_task_message( session_key=agent.openclaw_session_id,
session_key=agent.openclaw_session_id, config=config,
config=config, agent_name=agent.name,
agent_name=agent.name, message=message,
message=message,
)
) )
record_activity( record_activity(
session, session,
@@ -245,7 +241,7 @@ def _notify_agent_on_task_assign(
agent_id=agent.id, agent_id=agent.id,
task_id=task.id, task_id=task.id,
) )
session.commit() await session.commit()
except OpenClawGatewayError as exc: except OpenClawGatewayError as exc:
record_activity( record_activity(
session, session,
@@ -254,21 +250,25 @@ def _notify_agent_on_task_assign(
agent_id=agent.id, agent_id=agent.id,
task_id=task.id, task_id=task.id,
) )
session.commit() await session.commit()
def _notify_lead_on_task_create( async def _notify_lead_on_task_create(
*, *,
session: Session, session: AsyncSession,
board: Board, board: Board,
task: Task, task: Task,
) -> None: ) -> None:
lead = session.exec( lead = (
select(Agent).where(Agent.board_id == board.id).where(Agent.is_board_lead.is_(True)) await session.exec(
select(Agent)
.where(Agent.board_id == board.id)
.where(col(Agent.is_board_lead).is_(True))
)
).first() ).first()
if lead is None or not lead.openclaw_session_id: if lead is None or not lead.openclaw_session_id:
return return
config = _gateway_config(session, board) config = await _gateway_config(session, board)
if config is None: if config is None:
return return
description = (task.description or "").strip() description = (task.description or "").strip()
@@ -288,12 +288,10 @@ def _notify_lead_on_task_create(
+ "\n\nTake action: triage, assign, or plan next steps." + "\n\nTake action: triage, assign, or plan next steps."
) )
try: try:
asyncio.run( await _send_lead_task_message(
_send_lead_task_message( session_key=lead.openclaw_session_id,
session_key=lead.openclaw_session_id, config=config,
config=config, message=message,
message=message,
)
) )
record_activity( record_activity(
session, session,
@@ -302,7 +300,7 @@ def _notify_lead_on_task_create(
agent_id=lead.id, agent_id=lead.id,
task_id=task.id, task_id=task.id,
) )
session.commit() await session.commit()
except OpenClawGatewayError as exc: except OpenClawGatewayError as exc:
record_activity( record_activity(
session, session,
@@ -311,21 +309,25 @@ def _notify_lead_on_task_create(
agent_id=lead.id, agent_id=lead.id,
task_id=task.id, task_id=task.id,
) )
session.commit() await session.commit()
def _notify_lead_on_task_unassigned( async def _notify_lead_on_task_unassigned(
*, *,
session: Session, session: AsyncSession,
board: Board, board: Board,
task: Task, task: Task,
) -> None: ) -> None:
lead = session.exec( lead = (
select(Agent).where(Agent.board_id == board.id).where(Agent.is_board_lead.is_(True)) await session.exec(
select(Agent)
.where(Agent.board_id == board.id)
.where(col(Agent.is_board_lead).is_(True))
)
).first() ).first()
if lead is None or not lead.openclaw_session_id: if lead is None or not lead.openclaw_session_id:
return return
config = _gateway_config(session, board) config = await _gateway_config(session, board)
if config is None: if config is None:
return return
description = (task.description or "").strip() description = (task.description or "").strip()
@@ -345,12 +347,10 @@ def _notify_lead_on_task_unassigned(
+ "\n\nTake action: assign a new owner or adjust the plan." + "\n\nTake action: assign a new owner or adjust the plan."
) )
try: try:
asyncio.run( await _send_lead_task_message(
_send_lead_task_message( session_key=lead.openclaw_session_id,
session_key=lead.openclaw_session_id, config=config,
config=config, message=message,
message=message,
)
) )
record_activity( record_activity(
session, session,
@@ -359,7 +359,7 @@ def _notify_lead_on_task_unassigned(
agent_id=lead.id, agent_id=lead.id,
task_id=task.id, task_id=task.id,
) )
session.commit() await session.commit()
except OpenClawGatewayError as exc: except OpenClawGatewayError as exc:
record_activity( record_activity(
session, session,
@@ -368,7 +368,7 @@ def _notify_lead_on_task_unassigned(
agent_id=lead.id, agent_id=lead.id,
task_id=task.id, task_id=task.id,
) )
session.commit() await session.commit()
@router.get("/stream") @router.get("/stream")
@@ -378,16 +378,17 @@ async def stream_tasks(
actor: ActorContext = Depends(require_admin_or_agent), actor: ActorContext = Depends(require_admin_or_agent),
since: str | None = Query(default=None), since: str | None = Query(default=None),
) -> EventSourceResponse: ) -> EventSourceResponse:
since_dt = _parse_since(since) or datetime.utcnow() since_dt = _parse_since(since) or utcnow()
seen_ids: set[UUID] = set() seen_ids: set[UUID] = set()
seen_queue: deque[UUID] = deque() seen_queue: deque[UUID] = deque()
async def event_generator(): async def event_generator() -> AsyncIterator[dict[str, str]]:
last_seen = since_dt last_seen = since_dt
while True: while True:
if await request.is_disconnected(): if await request.is_disconnected():
break break
rows = await run_in_threadpool(_fetch_task_events, board.id, last_seen) async with async_session_maker() as session:
rows = await _fetch_task_events(session, board.id, last_seen)
for event, task in rows: for event, task in rows:
if event.id in seen_ids: if event.id in seen_ids:
continue continue
@@ -410,13 +411,13 @@ async def stream_tasks(
@router.get("", response_model=list[TaskRead]) @router.get("", response_model=list[TaskRead])
def list_tasks( async def list_tasks(
status_filter: str | None = Query(default=None, alias="status"), status_filter: str | None = Query(default=None, alias="status"),
assigned_agent_id: UUID | None = None, assigned_agent_id: UUID | None = None,
unassigned: bool | None = None, unassigned: bool | None = None,
limit: int | None = Query(default=None, ge=1, le=200), limit: int | None = Query(default=None, ge=1, le=200),
board: Board = Depends(get_board_or_404), board: Board = Depends(get_board_or_404),
session: Session = Depends(get_session), session: AsyncSession = Depends(get_session),
actor: ActorContext = Depends(require_admin_or_agent), actor: ActorContext = Depends(require_admin_or_agent),
) -> list[Task]: ) -> list[Task]:
statement = select(Task).where(Task.board_id == board.id) statement = select(Task).where(Task.board_id == board.id)
@@ -435,24 +436,23 @@ def list_tasks(
statement = statement.where(col(Task.assigned_agent_id).is_(None)) statement = statement.where(col(Task.assigned_agent_id).is_(None))
if limit is not None: if limit is not None:
statement = statement.limit(limit) statement = statement.limit(limit)
return list(session.exec(statement)) return list(await session.exec(statement))
@router.post("", response_model=TaskRead) @router.post("", response_model=TaskRead)
def create_task( async def create_task(
payload: TaskCreate, payload: TaskCreate,
board: Board = Depends(get_board_or_404), board: Board = Depends(get_board_or_404),
session: Session = Depends(get_session), session: AsyncSession = Depends(get_session),
auth: AuthContext = Depends(require_admin_auth), auth: AuthContext = Depends(require_admin_auth),
) -> Task: ) -> Task:
validate_task_status(payload.status)
task = Task.model_validate(payload) task = Task.model_validate(payload)
task.board_id = board.id task.board_id = board.id
if task.created_by_user_id is None and auth.user is not None: if task.created_by_user_id is None and auth.user is not None:
task.created_by_user_id = auth.user.id task.created_by_user_id = auth.user.id
session.add(task) session.add(task)
session.commit() await session.commit()
session.refresh(task) await session.refresh(task)
record_activity( record_activity(
session, session,
@@ -460,12 +460,12 @@ def create_task(
task_id=task.id, task_id=task.id,
message=f"Task created: {task.title}.", message=f"Task created: {task.title}.",
) )
session.commit() await session.commit()
_notify_lead_on_task_create(session=session, board=board, task=task) await _notify_lead_on_task_create(session=session, board=board, task=task)
if task.assigned_agent_id: if task.assigned_agent_id:
assigned_agent = session.get(Agent, task.assigned_agent_id) assigned_agent = await session.get(Agent, task.assigned_agent_id)
if assigned_agent: if assigned_agent:
_notify_agent_on_task_assign( await _notify_agent_on_task_assign(
session=session, session=session,
board=board, board=board,
task=task, task=task,
@@ -475,18 +475,16 @@ def create_task(
@router.patch("/{task_id}", response_model=TaskRead) @router.patch("/{task_id}", response_model=TaskRead)
def update_task( async def update_task(
payload: TaskUpdate, payload: TaskUpdate,
task: Task = Depends(get_task_or_404), task: Task = Depends(get_task_or_404),
session: Session = Depends(get_session), session: AsyncSession = Depends(get_session),
actor: ActorContext = Depends(require_admin_or_agent), actor: ActorContext = Depends(require_admin_or_agent),
) -> Task: ) -> Task:
previous_status = task.status previous_status = task.status
previous_assigned = task.assigned_agent_id previous_assigned = task.assigned_agent_id
updates = payload.model_dump(exclude_unset=True) updates = payload.model_dump(exclude_unset=True)
comment = updates.pop("comment", None) comment = updates.pop("comment", None)
if comment is not None and not comment.strip():
comment = None
if actor.actor_type == "agent" and actor.agent and actor.agent.is_board_lead: if actor.actor_type == "agent" and actor.agent and actor.agent.is_board_lead:
allowed_fields = {"assigned_agent_id", "status"} allowed_fields = {"assigned_agent_id", "status"}
@@ -498,7 +496,7 @@ def update_task(
if "assigned_agent_id" in updates: if "assigned_agent_id" in updates:
assigned_id = updates["assigned_agent_id"] assigned_id = updates["assigned_agent_id"]
if assigned_id: if assigned_id:
agent = session.get(Agent, assigned_id) agent = await session.get(Agent, assigned_id)
if agent is None: if agent is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
if agent.is_board_lead: if agent.is_board_lead:
@@ -512,7 +510,6 @@ def update_task(
else: else:
task.assigned_agent_id = None task.assigned_agent_id = None
if "status" in updates: if "status" in updates:
validate_task_status(updates["status"])
if task.status != "review": if task.status != "review":
raise HTTPException( raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN, status_code=status.HTTP_403_FORBIDDEN,
@@ -526,8 +523,8 @@ def update_task(
if updates["status"] == "inbox": if updates["status"] == "inbox":
task.assigned_agent_id = None task.assigned_agent_id = None
task.in_progress_at = None task.in_progress_at = None
task.status = updates["status"] task.status = updates["status"]
task.updated_at = datetime.utcnow() task.updated_at = utcnow()
session.add(task) session.add(task)
if task.status != previous_status: if task.status != previous_status:
event_type = "task.status_changed" event_type = "task.status_changed"
@@ -542,17 +539,17 @@ def update_task(
message=message, message=message,
agent_id=actor.agent.id, agent_id=actor.agent.id,
) )
session.commit() await session.commit()
session.refresh(task) await session.refresh(task)
if task.assigned_agent_id and task.assigned_agent_id != previous_assigned: if task.assigned_agent_id and task.assigned_agent_id != previous_assigned:
if actor.actor_type == "agent" and actor.agent and task.assigned_agent_id == actor.agent.id: if actor.actor_type == "agent" and actor.agent and task.assigned_agent_id == actor.agent.id:
return task return task
assigned_agent = session.get(Agent, task.assigned_agent_id) assigned_agent = await session.get(Agent, task.assigned_agent_id)
if assigned_agent: if assigned_agent:
board = session.get(Board, task.board_id) if task.board_id else None board = await session.get(Board, task.board_id) if task.board_id else None
if board: if board:
_notify_agent_on_task_assign( await _notify_agent_on_task_assign(
session=session, session=session,
board=board, board=board,
task=task, task=task,
@@ -567,37 +564,35 @@ def update_task(
if not set(updates).issubset(allowed_fields): if not set(updates).issubset(allowed_fields):
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
if "status" in updates: if "status" in updates:
validate_task_status(updates["status"])
if updates["status"] == "inbox": if updates["status"] == "inbox":
task.assigned_agent_id = None task.assigned_agent_id = None
task.in_progress_at = None task.in_progress_at = None
else: else:
task.assigned_agent_id = actor.agent.id if actor.agent else None task.assigned_agent_id = actor.agent.id if actor.agent else None
if updates["status"] == "in_progress": if updates["status"] == "in_progress":
task.in_progress_at = datetime.utcnow() task.in_progress_at = utcnow()
elif "status" in updates: elif "status" in updates:
validate_task_status(updates["status"])
if updates["status"] == "inbox": if updates["status"] == "inbox":
task.assigned_agent_id = None task.assigned_agent_id = None
task.in_progress_at = None task.in_progress_at = None
elif updates["status"] == "in_progress": elif updates["status"] == "in_progress":
task.in_progress_at = datetime.utcnow() task.in_progress_at = utcnow()
if "assigned_agent_id" in updates and updates["assigned_agent_id"]: if "assigned_agent_id" in updates and updates["assigned_agent_id"]:
agent = session.get(Agent, updates["assigned_agent_id"]) agent = await session.get(Agent, updates["assigned_agent_id"])
if agent is None: if agent is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
if agent.board_id and task.board_id and agent.board_id != task.board_id: if agent.board_id and task.board_id and agent.board_id != task.board_id:
raise HTTPException(status_code=status.HTTP_409_CONFLICT) raise HTTPException(status_code=status.HTTP_409_CONFLICT)
for key, value in updates.items(): for key, value in updates.items():
setattr(task, key, value) setattr(task, key, value)
task.updated_at = datetime.utcnow() task.updated_at = utcnow()
if "status" in updates and updates["status"] == "review": if "status" in updates and updates["status"] == "review":
if comment is not None and comment.strip(): if comment is not None and comment.strip():
if not comment.strip(): if not comment.strip():
raise _comment_validation_error() raise _comment_validation_error()
else: else:
if not has_valid_recent_comment( if not await has_valid_recent_comment(
session, session,
task, task,
task.assigned_agent_id, task.assigned_agent_id,
@@ -606,8 +601,8 @@ def update_task(
raise _comment_validation_error() raise _comment_validation_error()
session.add(task) session.add(task)
session.commit() await session.commit()
session.refresh(task) await session.refresh(task)
if comment is not None and comment.strip(): if comment is not None and comment.strip():
event = ActivityEvent( event = ActivityEvent(
@@ -617,7 +612,7 @@ def update_task(
agent_id=actor.agent.id if actor.actor_type == "agent" and actor.agent else None, agent_id=actor.agent.id if actor.actor_type == "agent" and actor.agent else None,
) )
session.add(event) session.add(event)
session.commit() await session.commit()
if "status" in updates and task.status != previous_status: if "status" in updates and task.status != previous_status:
event_type = "task.status_changed" event_type = "task.status_changed"
@@ -632,12 +627,12 @@ def update_task(
message=message, message=message,
agent_id=actor.agent.id if actor.actor_type == "agent" and actor.agent else None, agent_id=actor.agent.id if actor.actor_type == "agent" and actor.agent else None,
) )
session.commit() await session.commit()
if task.status == "inbox" and task.assigned_agent_id is None: if task.status == "inbox" and task.assigned_agent_id is None:
if previous_status != "inbox" or previous_assigned is not None: if previous_status != "inbox" or previous_assigned is not None:
board = session.get(Board, task.board_id) if task.board_id else None board = await session.get(Board, task.board_id) if task.board_id else None
if board: if board:
_notify_lead_on_task_unassigned( await _notify_lead_on_task_unassigned(
session=session, session=session,
board=board, board=board,
task=task, task=task,
@@ -645,11 +640,11 @@ def update_task(
if task.assigned_agent_id and task.assigned_agent_id != previous_assigned: if task.assigned_agent_id and task.assigned_agent_id != previous_assigned:
if actor.actor_type == "agent" and actor.agent and task.assigned_agent_id == actor.agent.id: if actor.actor_type == "agent" and actor.agent and task.assigned_agent_id == actor.agent.id:
return task return task
assigned_agent = session.get(Agent, task.assigned_agent_id) assigned_agent = await session.get(Agent, task.assigned_agent_id)
if assigned_agent: if assigned_agent:
board = session.get(Board, task.board_id) if task.board_id else None board = await session.get(Board, task.board_id) if task.board_id else None
if board: if board:
_notify_agent_on_task_assign( await _notify_agent_on_task_assign(
session=session, session=session,
board=board, board=board,
task=task, task=task,
@@ -658,23 +653,23 @@ def update_task(
return task return task
@router.delete("/{task_id}") @router.delete("/{task_id}", response_model=OkResponse)
def delete_task( async def delete_task(
session: Session = Depends(get_session), session: AsyncSession = Depends(get_session),
task: Task = Depends(get_task_or_404), task: Task = Depends(get_task_or_404),
auth: AuthContext = Depends(require_admin_auth), auth: AuthContext = Depends(require_admin_auth),
) -> dict[str, bool]: ) -> OkResponse:
session.execute(delete(ActivityEvent).where(col(ActivityEvent.task_id) == task.id)) await session.execute(delete(ActivityEvent).where(col(ActivityEvent.task_id) == task.id))
session.execute(delete(TaskFingerprint).where(col(TaskFingerprint.task_id) == task.id)) await session.execute(delete(TaskFingerprint).where(col(TaskFingerprint.task_id) == task.id))
session.delete(task) await session.delete(task)
session.commit() await session.commit()
return {"ok": True} return OkResponse()
@router.get("/{task_id}/comments", response_model=list[TaskCommentRead]) @router.get("/{task_id}/comments", response_model=list[TaskCommentRead])
def list_task_comments( async def list_task_comments(
task: Task = Depends(get_task_or_404), task: Task = Depends(get_task_or_404),
session: Session = Depends(get_session), session: AsyncSession = Depends(get_session),
actor: ActorContext = Depends(require_admin_or_agent), actor: ActorContext = Depends(require_admin_or_agent),
) -> list[ActivityEvent]: ) -> list[ActivityEvent]:
if actor.actor_type == "agent" and actor.agent: if actor.actor_type == "agent" and actor.agent:
@@ -686,19 +681,19 @@ def list_task_comments(
.where(col(ActivityEvent.event_type) == "task.comment") .where(col(ActivityEvent.event_type) == "task.comment")
.order_by(asc(col(ActivityEvent.created_at))) .order_by(asc(col(ActivityEvent.created_at)))
) )
return list(session.exec(statement)) return list(await session.exec(statement))
@router.post("/{task_id}/comments", response_model=TaskCommentRead) @router.post("/{task_id}/comments", response_model=TaskCommentRead)
def create_task_comment( async def create_task_comment(
payload: TaskCommentCreate, payload: TaskCommentCreate,
task: Task = Depends(get_task_or_404), task: Task = Depends(get_task_or_404),
session: Session = Depends(get_session), session: AsyncSession = Depends(get_session),
actor: ActorContext = Depends(require_admin_or_agent), actor: ActorContext = Depends(require_admin_or_agent),
) -> ActivityEvent: ) -> ActivityEvent:
if actor.actor_type == "agent" and actor.agent: if actor.actor_type == "agent" and actor.agent:
if actor.agent.is_board_lead and task.status != "review": if actor.agent.is_board_lead and task.status != "review":
if not _lead_was_mentioned(session, task, actor.agent) and not _lead_created_task( if not await _lead_was_mentioned(session, task, actor.agent) and not _lead_created_task(
task, actor.agent task, actor.agent
): ):
raise HTTPException( raise HTTPException(
@@ -709,8 +704,6 @@ def create_task_comment(
) )
if actor.agent.board_id and task.board_id and actor.agent.board_id != task.board_id: 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) raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
if not payload.message.strip():
raise _comment_validation_error()
event = ActivityEvent( event = ActivityEvent(
event_type="task.comment", event_type="task.comment",
message=payload.message, message=payload.message,
@@ -718,24 +711,24 @@ def create_task_comment(
agent_id=actor.agent.id if actor.actor_type == "agent" and actor.agent else None, agent_id=actor.agent.id if actor.actor_type == "agent" and actor.agent else None,
) )
session.add(event) session.add(event)
session.commit() await session.commit()
session.refresh(event) await session.refresh(event)
mention_names = _extract_mentions(payload.message) mention_names = _extract_mentions(payload.message)
targets: dict[UUID, Agent] = {} targets: dict[UUID, Agent] = {}
if mention_names and task.board_id: if mention_names and task.board_id:
statement = select(Agent).where(col(Agent.board_id) == task.board_id) statement = select(Agent).where(col(Agent.board_id) == task.board_id)
for agent in session.exec(statement): for agent in await session.exec(statement):
if _matches_mention(agent, mention_names): if _matches_mention(agent, mention_names):
targets[agent.id] = agent targets[agent.id] = agent
if not mention_names and task.assigned_agent_id: if not mention_names and task.assigned_agent_id:
assigned_agent = session.get(Agent, task.assigned_agent_id) assigned_agent = await session.get(Agent, task.assigned_agent_id)
if assigned_agent: if assigned_agent:
targets[assigned_agent.id] = assigned_agent targets[assigned_agent.id] = assigned_agent
if actor.actor_type == "agent" and actor.agent: if actor.actor_type == "agent" and actor.agent:
targets.pop(actor.agent.id, None) targets.pop(actor.agent.id, None)
if targets: if targets:
board = session.get(Board, task.board_id) if task.board_id else None board = await session.get(Board, task.board_id) if task.board_id else None
config = _gateway_config(session, board) if board else None config = await _gateway_config(session, board) if board else None
if board and config: if board and config:
snippet = payload.message.strip() snippet = payload.message.strip()
if len(snippet) > 500: if len(snippet) > 500:
@@ -762,13 +755,11 @@ def create_task_comment(
"If you are mentioned but not assigned, reply in the task thread but do not change task status." "If you are mentioned but not assigned, reply in the task thread but do not change task status."
) )
try: try:
asyncio.run( await _send_agent_task_message(
_send_agent_task_message( session_key=agent.openclaw_session_id,
session_key=agent.openclaw_session_id, config=config,
config=config, agent_name=agent.name,
agent_name=agent.name, message=message,
message=message,
)
) )
except OpenClawGatewayError: except OpenClawGatewayError:
pass pass

View File

@@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException, status from fastapi import APIRouter, Depends, HTTPException, status
from sqlmodel import Session from sqlmodel.ext.asyncio.session import AsyncSession
from app.core.auth import AuthContext, get_auth_context from app.core.auth import AuthContext, get_auth_context
from app.db.session import get_session from app.db.session import get_session
@@ -21,7 +21,7 @@ async def get_me(auth: AuthContext = Depends(get_auth_context)) -> UserRead:
@router.patch("/me", response_model=UserRead) @router.patch("/me", response_model=UserRead)
async def update_me( async def update_me(
payload: UserUpdate, payload: UserUpdate,
session: Session = Depends(get_session), session: AsyncSession = Depends(get_session),
auth: AuthContext = Depends(get_auth_context), auth: AuthContext = Depends(get_auth_context),
) -> UserRead: ) -> UserRead:
if auth.actor_type != "user" or auth.user is None: if auth.actor_type != "user" or auth.user is None:
@@ -31,6 +31,6 @@ async def update_me(
for key, value in updates.items(): for key, value in updates.items():
setattr(user, key, value) setattr(user, key, value)
session.add(user) session.add(user)
session.commit() await session.commit()
session.refresh(user) await session.refresh(user)
return UserRead.model_validate(user) return UserRead.model_validate(user)

View File

@@ -5,7 +5,8 @@ from dataclasses import dataclass
from typing import Literal from typing import Literal
from fastapi import Depends, Header, HTTPException, Request, status from fastapi import Depends, Header, HTTPException, Request, status
from sqlmodel import Session, col, select from sqlmodel import col, select
from sqlmodel.ext.asyncio.session import AsyncSession
from app.core.agent_tokens import verify_agent_token from app.core.agent_tokens import verify_agent_token
from app.db.session import get_session from app.db.session import get_session
@@ -20,8 +21,10 @@ class AgentAuthContext:
agent: Agent agent: Agent
def _find_agent_for_token(session: Session, token: str) -> Agent | None: async def _find_agent_for_token(session: AsyncSession, token: str) -> Agent | None:
agents = list(session.exec(select(Agent).where(col(Agent.agent_token_hash).is_not(None)))) agents = list(
await session.exec(select(Agent).where(col(Agent.agent_token_hash).is_not(None)))
)
for agent in agents: for agent in agents:
if agent.agent_token_hash and verify_agent_token(token, agent.agent_token_hash): if agent.agent_token_hash and verify_agent_token(token, agent.agent_token_hash):
return agent return agent
@@ -48,11 +51,11 @@ def _resolve_agent_token(
return None return None
def get_agent_auth_context( async def get_agent_auth_context(
request: Request, request: Request,
agent_token: str | None = Header(default=None, alias="X-Agent-Token"), agent_token: str | None = Header(default=None, alias="X-Agent-Token"),
authorization: str | None = Header(default=None, alias="Authorization"), authorization: str | None = Header(default=None, alias="Authorization"),
session: Session = Depends(get_session), session: AsyncSession = Depends(get_session),
) -> AgentAuthContext: ) -> AgentAuthContext:
resolved = _resolve_agent_token(agent_token, authorization, accept_authorization=True) resolved = _resolve_agent_token(agent_token, authorization, accept_authorization=True)
if not resolved: if not resolved:
@@ -63,7 +66,7 @@ def get_agent_auth_context(
bool(authorization), bool(authorization),
) )
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED) raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
agent = _find_agent_for_token(session, resolved) agent = await _find_agent_for_token(session, resolved)
if agent is None: if agent is None:
logger.warning( logger.warning(
"agent auth invalid token path=%s token_prefix=%s", "agent auth invalid token path=%s token_prefix=%s",
@@ -74,11 +77,11 @@ def get_agent_auth_context(
return AgentAuthContext(actor_type="agent", agent=agent) return AgentAuthContext(actor_type="agent", agent=agent)
def get_agent_auth_context_optional( async def get_agent_auth_context_optional(
request: Request, request: Request,
agent_token: str | None = Header(default=None, alias="X-Agent-Token"), agent_token: str | None = Header(default=None, alias="X-Agent-Token"),
authorization: str | None = Header(default=None, alias="Authorization"), authorization: str | None = Header(default=None, alias="Authorization"),
session: Session = Depends(get_session), session: AsyncSession = Depends(get_session),
) -> AgentAuthContext | None: ) -> AgentAuthContext | None:
resolved = _resolve_agent_token( resolved = _resolve_agent_token(
agent_token, agent_token,
@@ -94,7 +97,7 @@ def get_agent_auth_context_optional(
bool(authorization), bool(authorization),
) )
return None return None
agent = _find_agent_for_token(session, resolved) agent = await _find_agent_for_token(session, resolved)
if agent is None: if agent is None:
logger.warning( logger.warning(
"agent auth optional invalid token path=%s token_prefix=%s", "agent auth optional invalid token path=%s token_prefix=%s",

View File

@@ -9,9 +9,10 @@ from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from fastapi_clerk_auth import ClerkConfig, ClerkHTTPBearer from fastapi_clerk_auth import ClerkConfig, ClerkHTTPBearer
from fastapi_clerk_auth import HTTPAuthorizationCredentials as ClerkCredentials from fastapi_clerk_auth import HTTPAuthorizationCredentials as ClerkCredentials
from pydantic import BaseModel, ValidationError from pydantic import BaseModel, ValidationError
from sqlmodel import Session, select from sqlmodel.ext.asyncio.session import AsyncSession
from app.core.config import settings from app.core.config import settings
from app.db import crud
from app.db.session import get_session from app.db.session import get_session
from app.models.users import User from app.models.users import User
@@ -44,7 +45,9 @@ def _resolve_clerk_auth(
request: Request, fallback: ClerkCredentials | None request: Request, fallback: ClerkCredentials | None
) -> ClerkCredentials | None: ) -> ClerkCredentials | None:
auth_data = getattr(request.state, "clerk_auth", None) auth_data = getattr(request.state, "clerk_auth", None)
return auth_data or fallback if isinstance(auth_data, ClerkCredentials):
return auth_data
return fallback
def _parse_subject(auth_data: ClerkCredentials | None) -> str | None: def _parse_subject(auth_data: ClerkCredentials | None) -> str | None:
@@ -57,7 +60,7 @@ def _parse_subject(auth_data: ClerkCredentials | None) -> str | None:
async def get_auth_context( async def get_auth_context(
request: Request, request: Request,
credentials: HTTPAuthorizationCredentials | None = Depends(security), credentials: HTTPAuthorizationCredentials | None = Depends(security),
session: Session = Depends(get_session), session: AsyncSession = Depends(get_session),
) -> AuthContext: ) -> AuthContext:
if credentials is None: if credentials is None:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED) raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
@@ -79,17 +82,21 @@ async def get_auth_context(
if not clerk_user_id: if not clerk_user_id:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED) raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
user = session.exec(select(User).where(User.clerk_user_id == clerk_user_id)).first() claims: dict[str, object] = {}
if user is None: if auth_data and auth_data.decoded:
claims = auth_data.decoded if auth_data and auth_data.decoded else {} claims = auth_data.decoded
user = User( email_obj = claims.get("email")
clerk_user_id=clerk_user_id, name_obj = claims.get("name")
email=claims.get("email"), defaults: dict[str, object | None] = {
name=claims.get("name"), "email": email_obj if isinstance(email_obj, str) else None,
) "name": name_obj if isinstance(name_obj, str) else None,
session.add(user) }
session.commit() user, _created = await crud.get_or_create(
session.refresh(user) session,
User,
clerk_user_id=clerk_user_id,
defaults=defaults,
)
return AuthContext( return AuthContext(
actor_type="user", actor_type="user",
@@ -100,7 +107,7 @@ async def get_auth_context(
async def get_auth_context_optional( async def get_auth_context_optional(
request: Request, request: Request,
credentials: HTTPAuthorizationCredentials | None = Depends(security), credentials: HTTPAuthorizationCredentials | None = Depends(security),
session: Session = Depends(get_session), session: AsyncSession = Depends(get_session),
) -> AuthContext | None: ) -> AuthContext | None:
if request.headers.get("X-Agent-Token"): if request.headers.get("X-Agent-Token"):
return None return None
@@ -124,17 +131,21 @@ async def get_auth_context_optional(
if not clerk_user_id: if not clerk_user_id:
return None return None
user = session.exec(select(User).where(User.clerk_user_id == clerk_user_id)).first() claims: dict[str, object] = {}
if user is None: if auth_data and auth_data.decoded:
claims = auth_data.decoded if auth_data and auth_data.decoded else {} claims = auth_data.decoded
user = User( email_obj = claims.get("email")
clerk_user_id=clerk_user_id, name_obj = claims.get("name")
email=claims.get("email"), defaults: dict[str, object | None] = {
name=claims.get("name"), "email": email_obj if isinstance(email_obj, str) else None,
) "name": name_obj if isinstance(name_obj, str) else None,
session.add(user) }
session.commit() user, _created = await crud.get_or_create(
session.refresh(user) session,
User,
clerk_user_id=clerk_user_id,
defaults=defaults,
)
return AuthContext( return AuthContext(
actor_type="user", actor_type="user",

View File

@@ -20,7 +20,7 @@ def _trace(self: logging.Logger, message: str, *args: Any, **kwargs: Any) -> Non
self._log(TRACE_LEVEL, message, args, **kwargs) self._log(TRACE_LEVEL, message, args, **kwargs)
logging.Logger.trace = _trace # type: ignore[attr-defined] setattr(logging.Logger, "trace", _trace)
_STANDARD_LOG_RECORD_ATTRS = { _STANDARD_LOG_RECORD_ATTRS = {
"args", "args",

11
backend/app/core/time.py Normal file
View File

@@ -0,0 +1,11 @@
from __future__ import annotations
from datetime import UTC, datetime
def utcnow() -> datetime:
"""Return a naive UTC datetime without using deprecated datetime.utcnow()."""
# Keep naive UTC values for compatibility with existing DB schema/queries.
return datetime.now(UTC).replace(tzinfo=None)

125
backend/app/db/crud.py Normal file
View File

@@ -0,0 +1,125 @@
from __future__ import annotations
from collections.abc import Mapping
from typing import Any, TypeVar
from sqlalchemy.exc import IntegrityError
from sqlmodel import SQLModel, select
from sqlmodel.ext.asyncio.session import AsyncSession
ModelT = TypeVar("ModelT", bound=SQLModel)
class DoesNotExist(LookupError):
pass
class MultipleObjectsReturned(LookupError):
pass
async def get_by_id(session: AsyncSession, model: type[ModelT], obj_id: Any) -> ModelT | None:
return await session.get(model, obj_id)
async def get(session: AsyncSession, model: type[ModelT], **lookup: Any) -> ModelT:
stmt = select(model)
for key, value in lookup.items():
stmt = stmt.where(getattr(model, key) == value)
stmt = stmt.limit(2)
items = (await session.exec(stmt)).all()
if not items:
raise DoesNotExist(f"{model.__name__} matching query does not exist.")
if len(items) > 1:
raise MultipleObjectsReturned(
f"Multiple {model.__name__} objects returned for lookup {lookup!r}."
)
return items[0]
async def get_one_by(session: AsyncSession, model: type[ModelT], **lookup: Any) -> ModelT | None:
stmt = select(model)
for key, value in lookup.items():
stmt = stmt.where(getattr(model, key) == value)
return (await session.exec(stmt)).first()
async def create(
session: AsyncSession,
model: type[ModelT],
*,
commit: bool = True,
refresh: bool = True,
**data: Any,
) -> ModelT:
obj = model.model_validate(data)
session.add(obj)
await session.flush()
if commit:
await session.commit()
if refresh:
await session.refresh(obj)
return obj
async def save(
session: AsyncSession,
obj: ModelT,
*,
commit: bool = True,
refresh: bool = True,
) -> ModelT:
session.add(obj)
await session.flush()
if commit:
await session.commit()
if refresh:
await session.refresh(obj)
return obj
async def delete(session: AsyncSession, obj: ModelT, *, commit: bool = True) -> None:
await session.delete(obj)
if commit:
await session.commit()
async def get_or_create(
session: AsyncSession,
model: type[ModelT],
*,
defaults: Mapping[str, Any] | None = None,
commit: bool = True,
refresh: bool = True,
**lookup: Any,
) -> tuple[ModelT, bool]:
stmt = select(model)
for key, value in lookup.items():
stmt = stmt.where(getattr(model, key) == value)
existing = (await session.exec(stmt)).first()
if existing is not None:
return existing, False
payload: dict[str, Any] = dict(lookup)
if defaults:
for key, value in defaults.items():
payload.setdefault(key, value)
obj = model.model_validate(payload)
session.add(obj)
try:
await session.flush()
if commit:
await session.commit()
except IntegrityError:
# If another concurrent request inserted the same unique row, surface that row.
await session.rollback()
existing = (await session.exec(stmt)).first()
if existing is not None:
return existing, False
raise
if refresh:
await session.refresh(obj)
return obj, True

View File

@@ -1,17 +1,38 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
from collections.abc import Generator from collections.abc import AsyncGenerator
from pathlib import Path from pathlib import Path
from sqlmodel import Session, SQLModel, create_engine import anyio
from sqlalchemy.ext.asyncio import AsyncEngine, async_sessionmaker, create_async_engine
from sqlmodel import SQLModel
from sqlmodel.ext.asyncio.session import AsyncSession
from alembic import command from alembic import command
from alembic.config import Config from alembic.config import Config
from app import models # noqa: F401 from app import models # noqa: F401
from app.core.config import settings from app.core.config import settings
engine = create_engine(settings.database_url, pool_pre_ping=True)
def _normalize_database_url(database_url: str) -> str:
if "://" not in database_url:
return database_url
scheme, rest = database_url.split("://", 1)
if scheme == "postgresql":
return f"postgresql+psycopg://{rest}"
return database_url
async_engine: AsyncEngine = create_async_engine(
_normalize_database_url(settings.database_url),
pool_pre_ping=True,
)
async_session_maker = async_sessionmaker(
async_engine,
class_=AsyncSession,
expire_on_commit=False,
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -28,18 +49,19 @@ def run_migrations() -> None:
logger.info("Database migrations complete.") logger.info("Database migrations complete.")
def init_db() -> None: async def init_db() -> None:
if settings.db_auto_migrate: if settings.db_auto_migrate:
versions_dir = Path(__file__).resolve().parents[2] / "alembic" / "versions" versions_dir = Path(__file__).resolve().parents[2] / "alembic" / "versions"
if any(versions_dir.glob("*.py")): if any(versions_dir.glob("*.py")):
logger.info("Running Alembic migrations on startup") logger.info("Running Alembic migrations on startup")
run_migrations() await anyio.to_thread.run_sync(run_migrations)
return return
logger.warning("No Alembic revisions found; falling back to create_all") logger.warning("No Alembic revisions found; falling back to create_all")
SQLModel.metadata.create_all(engine) async with async_engine.begin() as conn:
await conn.run_sync(SQLModel.metadata.create_all)
def get_session() -> Generator[Session, None, None]: async def get_session() -> AsyncGenerator[AsyncSession, None]:
with Session(engine) as session: async with async_session_maker() as session:
yield session yield session

View File

@@ -1,5 +1,6 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from fastapi import APIRouter, FastAPI from fastapi import APIRouter, FastAPI
@@ -26,8 +27,8 @@ configure_logging()
@asynccontextmanager @asynccontextmanager
async def lifespan(_: FastAPI): async def lifespan(_: FastAPI) -> AsyncIterator[None]:
init_db() await init_db()
yield yield

View File

@@ -5,6 +5,8 @@ from uuid import UUID, uuid4
from sqlmodel import Field, SQLModel from sqlmodel import Field, SQLModel
from app.core.time import utcnow
class ActivityEvent(SQLModel, table=True): class ActivityEvent(SQLModel, table=True):
__tablename__ = "activity_events" __tablename__ = "activity_events"
@@ -14,4 +16,4 @@ class ActivityEvent(SQLModel, table=True):
message: str | None = None message: str | None = None
agent_id: UUID | None = Field(default=None, foreign_key="agents.id", index=True) agent_id: UUID | None = Field(default=None, foreign_key="agents.id", index=True)
task_id: UUID | None = Field(default=None, foreign_key="tasks.id", index=True) task_id: UUID | None = Field(default=None, foreign_key="tasks.id", index=True)
created_at: datetime = Field(default_factory=datetime.utcnow) created_at: datetime = Field(default_factory=utcnow)

View File

@@ -7,6 +7,8 @@ from uuid import UUID, uuid4
from sqlalchemy import JSON, Column, Text from sqlalchemy import JSON, Column, Text
from sqlmodel import Field, SQLModel from sqlmodel import Field, SQLModel
from app.core.time import utcnow
class Agent(SQLModel, table=True): class Agent(SQLModel, table=True):
__tablename__ = "agents" __tablename__ = "agents"
@@ -28,5 +30,5 @@ class Agent(SQLModel, table=True):
delete_confirm_token_hash: str | None = Field(default=None, index=True) delete_confirm_token_hash: str | None = Field(default=None, index=True)
last_seen_at: datetime | None = Field(default=None) last_seen_at: datetime | None = Field(default=None)
is_board_lead: bool = Field(default=False, index=True) is_board_lead: bool = Field(default=False, index=True)
created_at: datetime = Field(default_factory=datetime.utcnow) created_at: datetime = Field(default_factory=utcnow)
updated_at: datetime = Field(default_factory=datetime.utcnow) updated_at: datetime = Field(default_factory=utcnow)

View File

@@ -6,6 +6,8 @@ from uuid import UUID, uuid4
from sqlalchemy import JSON, Column from sqlalchemy import JSON, Column
from sqlmodel import Field, SQLModel from sqlmodel import Field, SQLModel
from app.core.time import utcnow
class Approval(SQLModel, table=True): class Approval(SQLModel, table=True):
__tablename__ = "approvals" __tablename__ = "approvals"
@@ -18,5 +20,5 @@ class Approval(SQLModel, table=True):
confidence: int confidence: int
rubric_scores: dict[str, int] | None = Field(default=None, sa_column=Column(JSON)) rubric_scores: dict[str, int] | None = Field(default=None, sa_column=Column(JSON))
status: str = Field(default="pending", index=True) status: str = Field(default="pending", index=True)
created_at: datetime = Field(default_factory=datetime.utcnow) created_at: datetime = Field(default_factory=utcnow)
resolved_at: datetime | None = None resolved_at: datetime | None = None

View File

@@ -6,6 +6,8 @@ from uuid import UUID, uuid4
from sqlalchemy import JSON, Column from sqlalchemy import JSON, Column
from sqlmodel import Field, SQLModel from sqlmodel import Field, SQLModel
from app.core.time import utcnow
class BoardMemory(SQLModel, table=True): class BoardMemory(SQLModel, table=True):
__tablename__ = "board_memory" __tablename__ = "board_memory"
@@ -15,4 +17,4 @@ class BoardMemory(SQLModel, table=True):
content: str content: str
tags: list[str] | None = Field(default=None, sa_column=Column(JSON)) tags: list[str] | None = Field(default=None, sa_column=Column(JSON))
source: str | None = None source: str | None = None
created_at: datetime = Field(default_factory=datetime.utcnow) created_at: datetime = Field(default_factory=utcnow)

View File

@@ -6,6 +6,8 @@ from uuid import UUID, uuid4
from sqlalchemy import JSON, Column from sqlalchemy import JSON, Column
from sqlmodel import Field, SQLModel from sqlmodel import Field, SQLModel
from app.core.time import utcnow
class BoardOnboardingSession(SQLModel, table=True): class BoardOnboardingSession(SQLModel, table=True):
__tablename__ = "board_onboarding_sessions" __tablename__ = "board_onboarding_sessions"
@@ -16,5 +18,5 @@ class BoardOnboardingSession(SQLModel, table=True):
status: str = Field(default="active", index=True) status: str = Field(default="active", index=True)
messages: list[dict[str, object]] | None = Field(default=None, sa_column=Column(JSON)) messages: list[dict[str, object]] | None = Field(default=None, sa_column=Column(JSON))
draft_goal: dict[str, object] | None = Field(default=None, sa_column=Column(JSON)) draft_goal: dict[str, object] | None = Field(default=None, sa_column=Column(JSON))
created_at: datetime = Field(default_factory=datetime.utcnow) created_at: datetime = Field(default_factory=utcnow)
updated_at: datetime = Field(default_factory=datetime.utcnow) updated_at: datetime = Field(default_factory=utcnow)

View File

@@ -6,6 +6,7 @@ from uuid import UUID, uuid4
from sqlalchemy import JSON, Column from sqlalchemy import JSON, Column
from sqlmodel import Field from sqlmodel import Field
from app.core.time import utcnow
from app.models.tenancy import TenantScoped from app.models.tenancy import TenantScoped
@@ -22,5 +23,5 @@ class Board(TenantScoped, table=True):
target_date: datetime | None = None target_date: datetime | None = None
goal_confirmed: bool = Field(default=False) goal_confirmed: bool = Field(default=False)
goal_source: str | None = None goal_source: str | None = None
created_at: datetime = Field(default_factory=datetime.utcnow) created_at: datetime = Field(default_factory=utcnow)
updated_at: datetime = Field(default_factory=datetime.utcnow) updated_at: datetime = Field(default_factory=utcnow)

View File

@@ -5,6 +5,8 @@ from uuid import UUID, uuid4
from sqlmodel import Field, SQLModel from sqlmodel import Field, SQLModel
from app.core.time import utcnow
class Gateway(SQLModel, table=True): class Gateway(SQLModel, table=True):
__tablename__ = "gateways" __tablename__ = "gateways"
@@ -16,5 +18,5 @@ class Gateway(SQLModel, table=True):
main_session_key: str main_session_key: str
workspace_root: str workspace_root: str
skyll_enabled: bool = Field(default=False) skyll_enabled: bool = Field(default=False)
created_at: datetime = Field(default_factory=datetime.utcnow) created_at: datetime = Field(default_factory=utcnow)
updated_at: datetime = Field(default_factory=datetime.utcnow) updated_at: datetime = Field(default_factory=utcnow)

View File

@@ -5,6 +5,8 @@ from uuid import UUID, uuid4
from sqlmodel import Field, SQLModel from sqlmodel import Field, SQLModel
from app.core.time import utcnow
class TaskFingerprint(SQLModel, table=True): class TaskFingerprint(SQLModel, table=True):
__tablename__ = "task_fingerprints" __tablename__ = "task_fingerprints"
@@ -13,4 +15,4 @@ class TaskFingerprint(SQLModel, table=True):
board_id: UUID = Field(foreign_key="boards.id", index=True) board_id: UUID = Field(foreign_key="boards.id", index=True)
fingerprint_hash: str = Field(index=True) fingerprint_hash: str = Field(index=True)
task_id: UUID = Field(foreign_key="tasks.id") task_id: UUID = Field(foreign_key="tasks.id")
created_at: datetime = Field(default_factory=datetime.utcnow) created_at: datetime = Field(default_factory=utcnow)

View File

@@ -6,6 +6,7 @@ from uuid import UUID, uuid4
from sqlmodel import Field from sqlmodel import Field
from app.models.tenancy import TenantScoped from app.models.tenancy import TenantScoped
from app.core.time import utcnow
class Task(TenantScoped, table=True): class Task(TenantScoped, table=True):
@@ -26,5 +27,5 @@ class Task(TenantScoped, table=True):
auto_created: bool = Field(default=False) auto_created: bool = Field(default=False)
auto_reason: str | None = None auto_reason: str | None = None
created_at: datetime = Field(default_factory=datetime.utcnow) created_at: datetime = Field(default_factory=utcnow)
updated_at: datetime = Field(default_factory=datetime.utcnow) updated_at: datetime = Field(default_factory=utcnow)

View File

@@ -1,21 +1,64 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Mapping
from datetime import datetime from datetime import datetime
from typing import Any from typing import Any
from uuid import UUID from uuid import UUID
from pydantic import field_validator
from sqlmodel import SQLModel from sqlmodel import SQLModel
from app.schemas.common import NonEmptyStr
def _normalize_identity_profile(
profile: object,
) -> dict[str, str] | None:
if not isinstance(profile, Mapping):
return None
normalized: dict[str, str] = {}
for raw_key, raw in profile.items():
if raw is None:
continue
key = str(raw_key).strip()
if not key:
continue
if isinstance(raw, list):
parts = [str(item).strip() for item in raw if str(item).strip()]
if not parts:
continue
normalized[key] = ", ".join(parts)
continue
value = str(raw).strip()
if value:
normalized[key] = value
return normalized or None
class AgentBase(SQLModel): class AgentBase(SQLModel):
board_id: UUID | None = None board_id: UUID | None = None
name: str name: NonEmptyStr
status: str = "provisioning" status: str = "provisioning"
heartbeat_config: dict[str, Any] | None = None heartbeat_config: dict[str, Any] | None = None
identity_profile: dict[str, Any] | None = None identity_profile: dict[str, Any] | None = None
identity_template: str | None = None identity_template: str | None = None
soul_template: str | None = None soul_template: str | None = None
@field_validator("identity_template", "soul_template", mode="before")
@classmethod
def normalize_templates(cls, value: Any) -> Any:
if value is None:
return None
if isinstance(value, str):
value = value.strip()
return value or None
return value
@field_validator("identity_profile", mode="before")
@classmethod
def normalize_identity_profile(cls, value: Any) -> Any:
return _normalize_identity_profile(value)
class AgentCreate(AgentBase): class AgentCreate(AgentBase):
pass pass
@@ -24,13 +67,28 @@ class AgentCreate(AgentBase):
class AgentUpdate(SQLModel): class AgentUpdate(SQLModel):
board_id: UUID | None = None board_id: UUID | None = None
is_gateway_main: bool | None = None is_gateway_main: bool | None = None
name: str | None = None name: NonEmptyStr | None = None
status: str | None = None status: str | None = None
heartbeat_config: dict[str, Any] | None = None heartbeat_config: dict[str, Any] | None = None
identity_profile: dict[str, Any] | None = None identity_profile: dict[str, Any] | None = None
identity_template: str | None = None identity_template: str | None = None
soul_template: str | None = None soul_template: str | None = None
@field_validator("identity_template", "soul_template", mode="before")
@classmethod
def normalize_templates(cls, value: Any) -> Any:
if value is None:
return None
if isinstance(value, str):
value = value.strip()
return value or None
return value
@field_validator("identity_profile", mode="before")
@classmethod
def normalize_identity_profile(cls, value: Any) -> Any:
return _normalize_identity_profile(value)
class AgentRead(AgentBase): class AgentRead(AgentBase):
id: UUID id: UUID
@@ -47,9 +105,9 @@ class AgentHeartbeat(SQLModel):
class AgentHeartbeatCreate(AgentHeartbeat): class AgentHeartbeatCreate(AgentHeartbeat):
name: str name: NonEmptyStr
board_id: UUID | None = None board_id: UUID | None = None
class AgentNudge(SQLModel): class AgentNudge(SQLModel):
message: str message: NonEmptyStr

View File

@@ -1,17 +1,22 @@
from __future__ import annotations from __future__ import annotations
from datetime import datetime from datetime import datetime
from typing import Literal, Self
from uuid import UUID from uuid import UUID
from pydantic import model_validator
from sqlmodel import SQLModel from sqlmodel import SQLModel
ApprovalStatus = Literal["pending", "approved", "rejected"]
class ApprovalBase(SQLModel): class ApprovalBase(SQLModel):
action_type: str action_type: str
payload: dict[str, object] | None = None payload: dict[str, object] | None = None
confidence: int confidence: int
rubric_scores: dict[str, int] | None = None rubric_scores: dict[str, int] | None = None
status: str = "pending" status: ApprovalStatus = "pending"
class ApprovalCreate(ApprovalBase): class ApprovalCreate(ApprovalBase):
@@ -19,7 +24,13 @@ class ApprovalCreate(ApprovalBase):
class ApprovalUpdate(SQLModel): class ApprovalUpdate(SQLModel):
status: str | None = None status: ApprovalStatus | None = None
@model_validator(mode="after")
def validate_status(self) -> Self:
if "status" in self.model_fields_set and self.status is None:
raise ValueError("status is required")
return self
class ApprovalRead(ApprovalBase): class ApprovalRead(ApprovalBase):

View File

@@ -5,9 +5,11 @@ from uuid import UUID
from sqlmodel import SQLModel from sqlmodel import SQLModel
from app.schemas.common import NonEmptyStr
class BoardMemoryCreate(SQLModel): class BoardMemoryCreate(SQLModel):
content: str content: NonEmptyStr
tags: list[str] | None = None tags: list[str] | None = None
source: str | None = None source: str | None = None

View File

@@ -1,18 +1,21 @@
from __future__ import annotations from __future__ import annotations
from datetime import datetime from datetime import datetime
from typing import Literal, Self
from uuid import UUID from uuid import UUID
from pydantic import model_validator from pydantic import Field, model_validator
from sqlmodel import SQLModel from sqlmodel import SQLModel
from app.schemas.common import NonEmptyStr
class BoardOnboardingStart(SQLModel): class BoardOnboardingStart(SQLModel):
pass pass
class BoardOnboardingAnswer(SQLModel): class BoardOnboardingAnswer(SQLModel):
answer: str answer: NonEmptyStr
other_text: str | None = None other_text: str | None = None
@@ -23,13 +26,30 @@ class BoardOnboardingConfirm(SQLModel):
target_date: datetime | None = None target_date: datetime | None = None
@model_validator(mode="after") @model_validator(mode="after")
def validate_goal_fields(self): def validate_goal_fields(self) -> Self:
if self.board_type == "goal": if self.board_type == "goal":
if not self.objective or not self.success_metrics: if not self.objective or not self.success_metrics:
raise ValueError("Confirmed goal boards require objective and success_metrics") raise ValueError("Confirmed goal boards require objective and success_metrics")
return self return self
class BoardOnboardingQuestionOption(SQLModel):
id: NonEmptyStr
label: NonEmptyStr
class BoardOnboardingAgentQuestion(SQLModel):
question: NonEmptyStr
options: list[BoardOnboardingQuestionOption] = Field(min_length=1)
class BoardOnboardingAgentComplete(BoardOnboardingConfirm):
status: Literal["complete"]
BoardOnboardingAgentUpdate = BoardOnboardingAgentComplete | BoardOnboardingAgentQuestion
class BoardOnboardingRead(SQLModel): class BoardOnboardingRead(SQLModel):
id: UUID id: UUID
board_id: UUID board_id: UUID

View File

@@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
from datetime import datetime from datetime import datetime
from typing import Self
from uuid import UUID from uuid import UUID
from pydantic import model_validator from pydantic import model_validator
@@ -20,8 +21,10 @@ class BoardBase(SQLModel):
class BoardCreate(BoardBase): class BoardCreate(BoardBase):
gateway_id: UUID
@model_validator(mode="after") @model_validator(mode="after")
def validate_goal_fields(self): def validate_goal_fields(self) -> Self:
if self.board_type == "goal" and self.goal_confirmed: if self.board_type == "goal" and self.goal_confirmed:
if not self.objective or not self.success_metrics: if not self.objective or not self.success_metrics:
raise ValueError("Confirmed goal boards require objective and success_metrics") raise ValueError("Confirmed goal boards require objective and success_metrics")
@@ -39,6 +42,13 @@ class BoardUpdate(SQLModel):
goal_confirmed: bool | None = None goal_confirmed: bool | None = None
goal_source: str | None = None goal_source: str | None = None
@model_validator(mode="after")
def validate_gateway_id(self) -> Self:
# Treat explicit null like "unset" is invalid for patch updates.
if "gateway_id" in self.model_fields_set and self.gateway_id is None:
raise ValueError("gateway_id is required")
return self
class BoardRead(BoardBase): class BoardRead(BoardBase):
id: UUID id: UUID

View File

@@ -0,0 +1,13 @@
from __future__ import annotations
from typing import Annotated
from pydantic import StringConstraints
from sqlmodel import SQLModel
# Reusable string type for request payloads where blank/whitespace-only values are invalid.
NonEmptyStr = Annotated[str, StringConstraints(strip_whitespace=True, min_length=1)]
class OkResponse(SQLModel):
ok: bool = True

View File

@@ -0,0 +1,47 @@
from __future__ import annotations
from sqlmodel import SQLModel
from app.schemas.common import NonEmptyStr
class GatewaySessionMessageRequest(SQLModel):
content: NonEmptyStr
class GatewayResolveQuery(SQLModel):
board_id: str | None = None
gateway_url: str | None = None
gateway_token: str | None = None
gateway_main_session_key: str | None = None
class GatewaysStatusResponse(SQLModel):
connected: bool
gateway_url: str
sessions_count: int | None = None
sessions: list[object] | None = None
main_session_key: str | None = None
main_session: object | None = None
main_session_error: str | None = None
error: str | None = None
class GatewaySessionsResponse(SQLModel):
sessions: list[object]
main_session_key: str | None = None
main_session: object | None = None
class GatewaySessionResponse(SQLModel):
session: object
class GatewaySessionHistoryResponse(SQLModel):
history: list[object]
class GatewayCommandsResponse(SQLModel):
protocol_version: int
methods: list[str]
events: list[str]

View File

@@ -1,8 +1,10 @@
from __future__ import annotations from __future__ import annotations
from datetime import datetime from datetime import datetime
from typing import Any
from uuid import UUID from uuid import UUID
from pydantic import field_validator
from sqlmodel import SQLModel from sqlmodel import SQLModel
@@ -17,6 +19,16 @@ class GatewayBase(SQLModel):
class GatewayCreate(GatewayBase): class GatewayCreate(GatewayBase):
token: str | None = None token: str | None = None
@field_validator("token", mode="before")
@classmethod
def normalize_token(cls, value: Any) -> Any:
if value is None:
return None
if isinstance(value, str):
value = value.strip()
return value or None
return value
class GatewayUpdate(SQLModel): class GatewayUpdate(SQLModel):
name: str | None = None name: str | None = None
@@ -26,6 +38,16 @@ class GatewayUpdate(SQLModel):
workspace_root: str | None = None workspace_root: str | None = None
skyll_enabled: bool | None = None skyll_enabled: bool | None = None
@field_validator("token", mode="before")
@classmethod
def normalize_token(cls, value: Any) -> Any:
if value is None:
return None
if isinstance(value, str):
value = value.strip()
return value or None
return value
class GatewayRead(GatewayBase): class GatewayRead(GatewayBase):
id: UUID id: UUID

View File

@@ -1,15 +1,22 @@
from __future__ import annotations from __future__ import annotations
from datetime import datetime from datetime import datetime
from typing import Any, Literal, Self
from uuid import UUID from uuid import UUID
from pydantic import field_validator, model_validator
from sqlmodel import SQLModel from sqlmodel import SQLModel
from app.schemas.common import NonEmptyStr
TaskStatus = Literal["inbox", "in_progress", "review", "done"]
class TaskBase(SQLModel): class TaskBase(SQLModel):
title: str title: str
description: str | None = None description: str | None = None
status: str = "inbox" status: TaskStatus = "inbox"
priority: str = "medium" priority: str = "medium"
due_at: datetime | None = None due_at: datetime | None = None
assigned_agent_id: UUID | None = None assigned_agent_id: UUID | None = None
@@ -22,11 +29,26 @@ class TaskCreate(TaskBase):
class TaskUpdate(SQLModel): class TaskUpdate(SQLModel):
title: str | None = None title: str | None = None
description: str | None = None description: str | None = None
status: str | None = None status: TaskStatus | None = None
priority: str | None = None priority: str | None = None
due_at: datetime | None = None due_at: datetime | None = None
assigned_agent_id: UUID | None = None assigned_agent_id: UUID | None = None
comment: str | None = None comment: NonEmptyStr | None = None
@field_validator("comment", mode="before")
@classmethod
def normalize_comment(cls, value: Any) -> Any:
if value is None:
return None
if isinstance(value, str) and not value.strip():
return None
return value
@model_validator(mode="after")
def validate_status(self) -> Self:
if "status" in self.model_fields_set and self.status is None:
raise ValueError("status is required")
return self
class TaskRead(TaskBase): class TaskRead(TaskBase):
@@ -39,7 +61,7 @@ class TaskRead(TaskBase):
class TaskCommentCreate(SQLModel): class TaskCommentCreate(SQLModel):
message: str message: NonEmptyStr
class TaskCommentRead(SQLModel): class TaskCommentRead(SQLModel):

View File

@@ -2,13 +2,13 @@ from __future__ import annotations
from uuid import UUID from uuid import UUID
from sqlmodel import Session from sqlmodel.ext.asyncio.session import AsyncSession
from app.models.activity_events import ActivityEvent from app.models.activity_events import ActivityEvent
def record_activity( def record_activity(
session: Session, session: AsyncSession,
*, *,
event_type: str, event_type: str,
message: str, message: str,

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
import json import json
import re import re
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any, cast
from uuid import uuid4 from uuid import uuid4
from jinja2 import Environment, FileSystemLoader, StrictUndefined, select_autoescape from jinja2 import Environment, FileSystemLoader, StrictUndefined, select_autoescape
@@ -240,7 +240,13 @@ async def _supported_gateway_files(config: GatewayClientConfig) -> set[str]:
) )
if isinstance(files_payload, dict): if isinstance(files_payload, dict):
files = files_payload.get("files") or [] files = files_payload.get("files") or []
supported = {item.get("name") for item in files if isinstance(item, dict)} supported: set[str] = set()
for item in files:
if not isinstance(item, dict):
continue
name = item.get("name")
if isinstance(name, str) and name:
supported.add(name)
return supported or set(DEFAULT_GATEWAY_FILES) return supported or set(DEFAULT_GATEWAY_FILES)
except OpenClawGatewayError: except OpenClawGatewayError:
pass pass
@@ -260,11 +266,14 @@ async def _gateway_agent_files_index(
payload = await openclaw_call("agents.files.list", {"agentId": agent_id}, config=config) payload = await openclaw_call("agents.files.list", {"agentId": agent_id}, config=config)
if isinstance(payload, dict): if isinstance(payload, dict):
files = payload.get("files") or [] files = payload.get("files") or []
return { index: dict[str, dict[str, Any]] = {}
item.get("name"): item for item in files:
for item in files if not isinstance(item, dict):
if isinstance(item, dict) and item.get("name") continue
} name = item.get("name")
if isinstance(name, str) and name:
index[name] = cast(dict[str, Any], item)
return index
except OpenClawGatewayError: except OpenClawGatewayError:
pass pass
return {} return {}
@@ -294,7 +303,7 @@ def _render_agent_files(
continue continue
if name == "HEARTBEAT.md": if name == "HEARTBEAT.md":
heartbeat_template = ( heartbeat_template = (
template_overrides.get(name) template_overrides[name]
if template_overrides and name in template_overrides if template_overrides and name in template_overrides
else _heartbeat_template_name(agent) else _heartbeat_template_name(agent)
) )
@@ -307,7 +316,7 @@ def _render_agent_files(
rendered[name] = env.from_string(override).render(**context).strip() rendered[name] = env.from_string(override).render(**context).strip()
continue continue
template_name = ( template_name = (
template_overrides.get(name) template_overrides[name]
if template_overrides and name in template_overrides if template_overrides and name in template_overrides
else name else name
) )
@@ -329,13 +338,15 @@ async def _gateway_default_agent_id(
if not isinstance(payload, dict): if not isinstance(payload, dict):
return None return None
default_id = payload.get("defaultId") or payload.get("default_id") default_id = payload.get("defaultId") or payload.get("default_id")
if default_id: if isinstance(default_id, str) and default_id:
return default_id return default_id
agents = payload.get("agents") or [] agents = payload.get("agents") or []
if isinstance(agents, list) and agents: if isinstance(agents, list) and agents:
first = agents[0] first = agents[0]
if isinstance(first, dict): if isinstance(first, dict):
return first.get("id") agent_id = first.get("id")
if isinstance(agent_id, str) and agent_id:
return agent_id
return None return None
@@ -539,7 +550,7 @@ async def cleanup_agent(
gateway: Gateway, gateway: Gateway,
) -> str | None: ) -> str | None:
if not gateway.url: if not gateway.url:
return return None
if not gateway.workspace_root: if not gateway.workspace_root:
raise ValueError("gateway_workspace_root is required") raise ValueError("gateway_workspace_root is required")
client_config = GatewayClientConfig(url=gateway.url, token=gateway.token) client_config = GatewayClientConfig(url=gateway.url, token=gateway.token)

View File

@@ -41,12 +41,11 @@ dev = [
[tool.mypy] [tool.mypy]
python_version = "3.12" python_version = "3.12"
ignore_missing_imports = true strict = true
warn_unused_ignores = true
warn_redundant_casts = true
warn_unused_configs = true
check_untyped_defs = true
plugins = ["pydantic.mypy"] plugins = ["pydantic.mypy"]
files = ["app", "scripts"]
mypy_path = ["typings"]
show_error_codes = true
[tool.pytest.ini_options] [tool.pytest.ini_options]
asyncio_default_fixture_loop_scope = "function" asyncio_default_fixture_loop_scope = "function"

View File

@@ -1,25 +1,41 @@
from __future__ import annotations from __future__ import annotations
import asyncio
from uuid import uuid4 from uuid import uuid4
from sqlmodel import Session from app.db.session import async_session_maker, init_db
from app.models.agents import Agent
from app.db.session import engine from app.models.boards import Board
from app.models.orgs import Org, Workspace from app.models.gateways import Gateway
from app.models.users import Membership, User from app.models.users import User
def run() -> None: async def run() -> None:
with Session(engine) as session: await init_db()
org = Org(name="Demo Org", slug="demo-org") async with async_session_maker() as session:
session.add(org) gateway = Gateway(
session.commit() name="Demo Gateway",
session.refresh(org) url="http://localhost:8080",
token=None,
main_session_key="demo:main",
workspace_root="/tmp/openclaw-demo",
skyll_enabled=False,
)
session.add(gateway)
await session.commit()
await session.refresh(gateway)
workspace = Workspace(org_id=org.id, name="Demo Workspace", slug="demo-workspace") board = Board(
session.add(workspace) name="Demo Board",
session.commit() slug="demo-board",
session.refresh(workspace) gateway_id=gateway.id,
board_type="goal",
objective="Demo objective",
success_metrics={"demo": True},
)
session.add(board)
await session.commit()
await session.refresh(board)
user = User( user = User(
clerk_user_id=f"demo-{uuid4()}", clerk_user_id=f"demo-{uuid4()}",
@@ -28,19 +44,18 @@ def run() -> None:
is_super_admin=True, is_super_admin=True,
) )
session.add(user) session.add(user)
session.commit() await session.commit()
session.refresh(user) await session.refresh(user)
membership = Membership( lead = Agent(
org_id=org.id, board_id=board.id,
workspace_id=workspace.id, name="Lead Agent",
user_id=user.id, status="online",
role="admin", is_board_lead=True,
) )
session.add(membership) session.add(lead)
await session.commit()
session.commit()
if __name__ == "__main__": if __name__ == "__main__":
run() asyncio.run(run())

View File

@@ -1,4 +1,5 @@
import pytest import pytest
from uuid import uuid4
from app.schemas.board_onboarding import BoardOnboardingConfirm from app.schemas.board_onboarding import BoardOnboardingConfirm
from app.schemas.boards import BoardCreate from app.schemas.boards import BoardCreate
@@ -9,6 +10,7 @@ def test_goal_board_requires_objective_and_metrics_when_confirmed():
BoardCreate( BoardCreate(
name="Goal Board", name="Goal Board",
slug="goal", slug="goal",
gateway_id=uuid4(),
board_type="goal", board_type="goal",
goal_confirmed=True, goal_confirmed=True,
) )
@@ -16,6 +18,7 @@ def test_goal_board_requires_objective_and_metrics_when_confirmed():
BoardCreate( BoardCreate(
name="Goal Board", name="Goal Board",
slug="goal", slug="goal",
gateway_id=uuid4(),
board_type="goal", board_type="goal",
goal_confirmed=True, goal_confirmed=True,
objective="Launch onboarding", objective="Launch onboarding",
@@ -24,11 +27,11 @@ def test_goal_board_requires_objective_and_metrics_when_confirmed():
def test_goal_board_allows_missing_objective_before_confirmation(): def test_goal_board_allows_missing_objective_before_confirmation():
BoardCreate(name="Draft", slug="draft", board_type="goal") BoardCreate(name="Draft", slug="draft", gateway_id=uuid4(), board_type="goal")
def test_general_board_allows_missing_objective(): def test_general_board_allows_missing_objective():
BoardCreate(name="General", slug="general", board_type="general") BoardCreate(name="General", slug="general", gateway_id=uuid4(), board_type="general")
def test_onboarding_confirm_requires_goal_fields(): def test_onboarding_confirm_requires_goal_fields():

View File

@@ -0,0 +1,37 @@
from __future__ import annotations
from dataclasses import dataclass
from starlette.requests import Request
@dataclass
class ClerkConfig:
jwks_url: str
verify_iat: bool = ...
leeway: float = ...
class HTTPAuthorizationCredentials:
scheme: str
credentials: str
decoded: dict[str, object] | None
def __init__(
self,
scheme: str,
credentials: str,
decoded: dict[str, object] | None = ...,
) -> None: ...
class ClerkHTTPBearer:
def __init__(
self,
config: ClerkConfig,
auto_error: bool = ...,
add_state: bool = ...,
) -> None: ...
async def __call__(self, request: Request) -> HTTPAuthorizationCredentials | None: ...

File diff suppressed because it is too large Load Diff

View File

@@ -22,16 +22,14 @@ import type {
import type { import type {
AgentCreate, AgentCreate,
AgentDeleteConfirm,
AgentHeartbeat, AgentHeartbeat,
AgentHeartbeatCreate, AgentHeartbeatCreate,
AgentProvisionConfirm,
AgentRead, AgentRead,
AgentUpdate, AgentUpdate,
ConfirmDeleteAgentApiV1AgentsAgentIdDeleteConfirmPost200,
ConfirmProvisionAgentApiV1AgentsAgentIdProvisionConfirmPost200,
DeleteAgentApiV1AgentsAgentIdDelete200,
HTTPValidationError, HTTPValidationError,
OkResponse,
StreamAgentsApiV1AgentsStreamGetParams,
UpdateAgentApiV1AgentsAgentIdPatchParams,
} from ".././model"; } from ".././model";
import { customFetch } from "../../mutator"; import { customFetch } from "../../mutator";
@@ -326,6 +324,217 @@ export const useCreateAgentApiV1AgentsPost = <
queryClient, queryClient,
); );
}; };
/**
* @summary Stream Agents
*/
export type streamAgentsApiV1AgentsStreamGetResponse200 = {
data: unknown;
status: 200;
};
export type streamAgentsApiV1AgentsStreamGetResponse422 = {
data: HTTPValidationError;
status: 422;
};
export type streamAgentsApiV1AgentsStreamGetResponseSuccess =
streamAgentsApiV1AgentsStreamGetResponse200 & {
headers: Headers;
};
export type streamAgentsApiV1AgentsStreamGetResponseError =
streamAgentsApiV1AgentsStreamGetResponse422 & {
headers: Headers;
};
export type streamAgentsApiV1AgentsStreamGetResponse =
| streamAgentsApiV1AgentsStreamGetResponseSuccess
| streamAgentsApiV1AgentsStreamGetResponseError;
export const getStreamAgentsApiV1AgentsStreamGetUrl = (
params?: StreamAgentsApiV1AgentsStreamGetParams,
) => {
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/stream?${stringifiedParams}`
: `/api/v1/agents/stream`;
};
export const streamAgentsApiV1AgentsStreamGet = async (
params?: StreamAgentsApiV1AgentsStreamGetParams,
options?: RequestInit,
): Promise<streamAgentsApiV1AgentsStreamGetResponse> => {
return customFetch<streamAgentsApiV1AgentsStreamGetResponse>(
getStreamAgentsApiV1AgentsStreamGetUrl(params),
{
...options,
method: "GET",
},
);
};
export const getStreamAgentsApiV1AgentsStreamGetQueryKey = (
params?: StreamAgentsApiV1AgentsStreamGetParams,
) => {
return [`/api/v1/agents/stream`, ...(params ? [params] : [])] as const;
};
export const getStreamAgentsApiV1AgentsStreamGetQueryOptions = <
TData = Awaited<ReturnType<typeof streamAgentsApiV1AgentsStreamGet>>,
TError = HTTPValidationError,
>(
params?: StreamAgentsApiV1AgentsStreamGetParams,
options?: {
query?: Partial<
UseQueryOptions<
Awaited<ReturnType<typeof streamAgentsApiV1AgentsStreamGet>>,
TError,
TData
>
>;
request?: SecondParameter<typeof customFetch>;
},
) => {
const { query: queryOptions, request: requestOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ??
getStreamAgentsApiV1AgentsStreamGetQueryKey(params);
const queryFn: QueryFunction<
Awaited<ReturnType<typeof streamAgentsApiV1AgentsStreamGet>>
> = ({ signal }) =>
streamAgentsApiV1AgentsStreamGet(params, { signal, ...requestOptions });
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof streamAgentsApiV1AgentsStreamGet>>,
TError,
TData
> & { queryKey: DataTag<QueryKey, TData, TError> };
};
export type StreamAgentsApiV1AgentsStreamGetQueryResult = NonNullable<
Awaited<ReturnType<typeof streamAgentsApiV1AgentsStreamGet>>
>;
export type StreamAgentsApiV1AgentsStreamGetQueryError = HTTPValidationError;
export function useStreamAgentsApiV1AgentsStreamGet<
TData = Awaited<ReturnType<typeof streamAgentsApiV1AgentsStreamGet>>,
TError = HTTPValidationError,
>(
params: undefined | StreamAgentsApiV1AgentsStreamGetParams,
options: {
query: Partial<
UseQueryOptions<
Awaited<ReturnType<typeof streamAgentsApiV1AgentsStreamGet>>,
TError,
TData
>
> &
Pick<
DefinedInitialDataOptions<
Awaited<ReturnType<typeof streamAgentsApiV1AgentsStreamGet>>,
TError,
Awaited<ReturnType<typeof streamAgentsApiV1AgentsStreamGet>>
>,
"initialData"
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): DefinedUseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
export function useStreamAgentsApiV1AgentsStreamGet<
TData = Awaited<ReturnType<typeof streamAgentsApiV1AgentsStreamGet>>,
TError = HTTPValidationError,
>(
params?: StreamAgentsApiV1AgentsStreamGetParams,
options?: {
query?: Partial<
UseQueryOptions<
Awaited<ReturnType<typeof streamAgentsApiV1AgentsStreamGet>>,
TError,
TData
>
> &
Pick<
UndefinedInitialDataOptions<
Awaited<ReturnType<typeof streamAgentsApiV1AgentsStreamGet>>,
TError,
Awaited<ReturnType<typeof streamAgentsApiV1AgentsStreamGet>>
>,
"initialData"
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
export function useStreamAgentsApiV1AgentsStreamGet<
TData = Awaited<ReturnType<typeof streamAgentsApiV1AgentsStreamGet>>,
TError = HTTPValidationError,
>(
params?: StreamAgentsApiV1AgentsStreamGetParams,
options?: {
query?: Partial<
UseQueryOptions<
Awaited<ReturnType<typeof streamAgentsApiV1AgentsStreamGet>>,
TError,
TData
>
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
/**
* @summary Stream Agents
*/
export function useStreamAgentsApiV1AgentsStreamGet<
TData = Awaited<ReturnType<typeof streamAgentsApiV1AgentsStreamGet>>,
TError = HTTPValidationError,
>(
params?: StreamAgentsApiV1AgentsStreamGetParams,
options?: {
query?: Partial<
UseQueryOptions<
Awaited<ReturnType<typeof streamAgentsApiV1AgentsStreamGet>>,
TError,
TData
>
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
} {
const queryOptions = getStreamAgentsApiV1AgentsStreamGetQueryOptions(
params,
options,
);
const query = useQuery(queryOptions, queryClient) as UseQueryResult<
TData,
TError
> & { queryKey: DataTag<QueryKey, TData, TError> };
return { ...query, queryKey: queryOptions.queryKey };
}
/** /**
* @summary Get Agent * @summary Get Agent
*/ */
@@ -551,17 +760,33 @@ export type updateAgentApiV1AgentsAgentIdPatchResponse =
| updateAgentApiV1AgentsAgentIdPatchResponseSuccess | updateAgentApiV1AgentsAgentIdPatchResponseSuccess
| updateAgentApiV1AgentsAgentIdPatchResponseError; | updateAgentApiV1AgentsAgentIdPatchResponseError;
export const getUpdateAgentApiV1AgentsAgentIdPatchUrl = (agentId: string) => { export const getUpdateAgentApiV1AgentsAgentIdPatchUrl = (
return `/api/v1/agents/${agentId}`; agentId: string,
params?: UpdateAgentApiV1AgentsAgentIdPatchParams,
) => {
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/${agentId}?${stringifiedParams}`
: `/api/v1/agents/${agentId}`;
}; };
export const updateAgentApiV1AgentsAgentIdPatch = async ( export const updateAgentApiV1AgentsAgentIdPatch = async (
agentId: string, agentId: string,
agentUpdate: AgentUpdate, agentUpdate: AgentUpdate,
params?: UpdateAgentApiV1AgentsAgentIdPatchParams,
options?: RequestInit, options?: RequestInit,
): Promise<updateAgentApiV1AgentsAgentIdPatchResponse> => { ): Promise<updateAgentApiV1AgentsAgentIdPatchResponse> => {
return customFetch<updateAgentApiV1AgentsAgentIdPatchResponse>( return customFetch<updateAgentApiV1AgentsAgentIdPatchResponse>(
getUpdateAgentApiV1AgentsAgentIdPatchUrl(agentId), getUpdateAgentApiV1AgentsAgentIdPatchUrl(agentId, params),
{ {
...options, ...options,
method: "PATCH", method: "PATCH",
@@ -578,14 +803,22 @@ export const getUpdateAgentApiV1AgentsAgentIdPatchMutationOptions = <
mutation?: UseMutationOptions< mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateAgentApiV1AgentsAgentIdPatch>>, Awaited<ReturnType<typeof updateAgentApiV1AgentsAgentIdPatch>>,
TError, TError,
{ agentId: string; data: AgentUpdate }, {
agentId: string;
data: AgentUpdate;
params?: UpdateAgentApiV1AgentsAgentIdPatchParams;
},
TContext TContext
>; >;
request?: SecondParameter<typeof customFetch>; request?: SecondParameter<typeof customFetch>;
}): UseMutationOptions< }): UseMutationOptions<
Awaited<ReturnType<typeof updateAgentApiV1AgentsAgentIdPatch>>, Awaited<ReturnType<typeof updateAgentApiV1AgentsAgentIdPatch>>,
TError, TError,
{ agentId: string; data: AgentUpdate }, {
agentId: string;
data: AgentUpdate;
params?: UpdateAgentApiV1AgentsAgentIdPatchParams;
},
TContext TContext
> => { > => {
const mutationKey = ["updateAgentApiV1AgentsAgentIdPatch"]; const mutationKey = ["updateAgentApiV1AgentsAgentIdPatch"];
@@ -599,11 +832,20 @@ export const getUpdateAgentApiV1AgentsAgentIdPatchMutationOptions = <
const mutationFn: MutationFunction< const mutationFn: MutationFunction<
Awaited<ReturnType<typeof updateAgentApiV1AgentsAgentIdPatch>>, Awaited<ReturnType<typeof updateAgentApiV1AgentsAgentIdPatch>>,
{ agentId: string; data: AgentUpdate } {
agentId: string;
data: AgentUpdate;
params?: UpdateAgentApiV1AgentsAgentIdPatchParams;
}
> = (props) => { > = (props) => {
const { agentId, data } = props ?? {}; const { agentId, data, params } = props ?? {};
return updateAgentApiV1AgentsAgentIdPatch(agentId, data, requestOptions); return updateAgentApiV1AgentsAgentIdPatch(
agentId,
data,
params,
requestOptions,
);
}; };
return { mutationFn, ...mutationOptions }; return { mutationFn, ...mutationOptions };
@@ -627,7 +869,11 @@ export const useUpdateAgentApiV1AgentsAgentIdPatch = <
mutation?: UseMutationOptions< mutation?: UseMutationOptions<
Awaited<ReturnType<typeof updateAgentApiV1AgentsAgentIdPatch>>, Awaited<ReturnType<typeof updateAgentApiV1AgentsAgentIdPatch>>,
TError, TError,
{ agentId: string; data: AgentUpdate }, {
agentId: string;
data: AgentUpdate;
params?: UpdateAgentApiV1AgentsAgentIdPatchParams;
},
TContext TContext
>; >;
request?: SecondParameter<typeof customFetch>; request?: SecondParameter<typeof customFetch>;
@@ -636,7 +882,11 @@ export const useUpdateAgentApiV1AgentsAgentIdPatch = <
): UseMutationResult< ): UseMutationResult<
Awaited<ReturnType<typeof updateAgentApiV1AgentsAgentIdPatch>>, Awaited<ReturnType<typeof updateAgentApiV1AgentsAgentIdPatch>>,
TError, TError,
{ agentId: string; data: AgentUpdate }, {
agentId: string;
data: AgentUpdate;
params?: UpdateAgentApiV1AgentsAgentIdPatchParams;
},
TContext TContext
> => { > => {
return useMutation( return useMutation(
@@ -648,7 +898,7 @@ export const useUpdateAgentApiV1AgentsAgentIdPatch = <
* @summary Delete Agent * @summary Delete Agent
*/ */
export type deleteAgentApiV1AgentsAgentIdDeleteResponse200 = { export type deleteAgentApiV1AgentsAgentIdDeleteResponse200 = {
data: DeleteAgentApiV1AgentsAgentIdDelete200; data: OkResponse;
status: 200; status: 200;
}; };
@@ -1014,302 +1264,3 @@ export const useHeartbeatOrCreateAgentApiV1AgentsHeartbeatPost = <
queryClient, queryClient,
); );
}; };
/**
* @summary Confirm Provision Agent
*/
export type confirmProvisionAgentApiV1AgentsAgentIdProvisionConfirmPostResponse200 =
{
data: ConfirmProvisionAgentApiV1AgentsAgentIdProvisionConfirmPost200;
status: 200;
};
export type confirmProvisionAgentApiV1AgentsAgentIdProvisionConfirmPostResponse422 =
{
data: HTTPValidationError;
status: 422;
};
export type confirmProvisionAgentApiV1AgentsAgentIdProvisionConfirmPostResponseSuccess =
confirmProvisionAgentApiV1AgentsAgentIdProvisionConfirmPostResponse200 & {
headers: Headers;
};
export type confirmProvisionAgentApiV1AgentsAgentIdProvisionConfirmPostResponseError =
confirmProvisionAgentApiV1AgentsAgentIdProvisionConfirmPostResponse422 & {
headers: Headers;
};
export type confirmProvisionAgentApiV1AgentsAgentIdProvisionConfirmPostResponse =
| confirmProvisionAgentApiV1AgentsAgentIdProvisionConfirmPostResponseSuccess
| confirmProvisionAgentApiV1AgentsAgentIdProvisionConfirmPostResponseError;
export const getConfirmProvisionAgentApiV1AgentsAgentIdProvisionConfirmPostUrl =
(agentId: string) => {
return `/api/v1/agents/${agentId}/provision/confirm`;
};
export const confirmProvisionAgentApiV1AgentsAgentIdProvisionConfirmPost =
async (
agentId: string,
agentProvisionConfirm: AgentProvisionConfirm,
options?: RequestInit,
): Promise<confirmProvisionAgentApiV1AgentsAgentIdProvisionConfirmPostResponse> => {
return customFetch<confirmProvisionAgentApiV1AgentsAgentIdProvisionConfirmPostResponse>(
getConfirmProvisionAgentApiV1AgentsAgentIdProvisionConfirmPostUrl(
agentId,
),
{
...options,
method: "POST",
headers: { "Content-Type": "application/json", ...options?.headers },
body: JSON.stringify(agentProvisionConfirm),
},
);
};
export const getConfirmProvisionAgentApiV1AgentsAgentIdProvisionConfirmPostMutationOptions =
<TError = HTTPValidationError, TContext = unknown>(options?: {
mutation?: UseMutationOptions<
Awaited<
ReturnType<
typeof confirmProvisionAgentApiV1AgentsAgentIdProvisionConfirmPost
>
>,
TError,
{ agentId: string; data: AgentProvisionConfirm },
TContext
>;
request?: SecondParameter<typeof customFetch>;
}): UseMutationOptions<
Awaited<
ReturnType<
typeof confirmProvisionAgentApiV1AgentsAgentIdProvisionConfirmPost
>
>,
TError,
{ agentId: string; data: AgentProvisionConfirm },
TContext
> => {
const mutationKey = [
"confirmProvisionAgentApiV1AgentsAgentIdProvisionConfirmPost",
];
const { mutation: mutationOptions, request: requestOptions } = options
? options.mutation &&
"mutationKey" in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey }, request: undefined };
const mutationFn: MutationFunction<
Awaited<
ReturnType<
typeof confirmProvisionAgentApiV1AgentsAgentIdProvisionConfirmPost
>
>,
{ agentId: string; data: AgentProvisionConfirm }
> = (props) => {
const { agentId, data } = props ?? {};
return confirmProvisionAgentApiV1AgentsAgentIdProvisionConfirmPost(
agentId,
data,
requestOptions,
);
};
return { mutationFn, ...mutationOptions };
};
export type ConfirmProvisionAgentApiV1AgentsAgentIdProvisionConfirmPostMutationResult =
NonNullable<
Awaited<
ReturnType<
typeof confirmProvisionAgentApiV1AgentsAgentIdProvisionConfirmPost
>
>
>;
export type ConfirmProvisionAgentApiV1AgentsAgentIdProvisionConfirmPostMutationBody =
AgentProvisionConfirm;
export type ConfirmProvisionAgentApiV1AgentsAgentIdProvisionConfirmPostMutationError =
HTTPValidationError;
/**
* @summary Confirm Provision Agent
*/
export const useConfirmProvisionAgentApiV1AgentsAgentIdProvisionConfirmPost = <
TError = HTTPValidationError,
TContext = unknown,
>(
options?: {
mutation?: UseMutationOptions<
Awaited<
ReturnType<
typeof confirmProvisionAgentApiV1AgentsAgentIdProvisionConfirmPost
>
>,
TError,
{ agentId: string; data: AgentProvisionConfirm },
TContext
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseMutationResult<
Awaited<
ReturnType<
typeof confirmProvisionAgentApiV1AgentsAgentIdProvisionConfirmPost
>
>,
TError,
{ agentId: string; data: AgentProvisionConfirm },
TContext
> => {
return useMutation(
getConfirmProvisionAgentApiV1AgentsAgentIdProvisionConfirmPostMutationOptions(
options,
),
queryClient,
);
};
/**
* @summary Confirm Delete Agent
*/
export type confirmDeleteAgentApiV1AgentsAgentIdDeleteConfirmPostResponse200 = {
data: ConfirmDeleteAgentApiV1AgentsAgentIdDeleteConfirmPost200;
status: 200;
};
export type confirmDeleteAgentApiV1AgentsAgentIdDeleteConfirmPostResponse422 = {
data: HTTPValidationError;
status: 422;
};
export type confirmDeleteAgentApiV1AgentsAgentIdDeleteConfirmPostResponseSuccess =
confirmDeleteAgentApiV1AgentsAgentIdDeleteConfirmPostResponse200 & {
headers: Headers;
};
export type confirmDeleteAgentApiV1AgentsAgentIdDeleteConfirmPostResponseError =
confirmDeleteAgentApiV1AgentsAgentIdDeleteConfirmPostResponse422 & {
headers: Headers;
};
export type confirmDeleteAgentApiV1AgentsAgentIdDeleteConfirmPostResponse =
| confirmDeleteAgentApiV1AgentsAgentIdDeleteConfirmPostResponseSuccess
| confirmDeleteAgentApiV1AgentsAgentIdDeleteConfirmPostResponseError;
export const getConfirmDeleteAgentApiV1AgentsAgentIdDeleteConfirmPostUrl = (
agentId: string,
) => {
return `/api/v1/agents/${agentId}/delete/confirm`;
};
export const confirmDeleteAgentApiV1AgentsAgentIdDeleteConfirmPost = async (
agentId: string,
agentDeleteConfirm: AgentDeleteConfirm,
options?: RequestInit,
): Promise<confirmDeleteAgentApiV1AgentsAgentIdDeleteConfirmPostResponse> => {
return customFetch<confirmDeleteAgentApiV1AgentsAgentIdDeleteConfirmPostResponse>(
getConfirmDeleteAgentApiV1AgentsAgentIdDeleteConfirmPostUrl(agentId),
{
...options,
method: "POST",
headers: { "Content-Type": "application/json", ...options?.headers },
body: JSON.stringify(agentDeleteConfirm),
},
);
};
export const getConfirmDeleteAgentApiV1AgentsAgentIdDeleteConfirmPostMutationOptions =
<TError = HTTPValidationError, TContext = unknown>(options?: {
mutation?: UseMutationOptions<
Awaited<
ReturnType<typeof confirmDeleteAgentApiV1AgentsAgentIdDeleteConfirmPost>
>,
TError,
{ agentId: string; data: AgentDeleteConfirm },
TContext
>;
request?: SecondParameter<typeof customFetch>;
}): UseMutationOptions<
Awaited<
ReturnType<typeof confirmDeleteAgentApiV1AgentsAgentIdDeleteConfirmPost>
>,
TError,
{ agentId: string; data: AgentDeleteConfirm },
TContext
> => {
const mutationKey = [
"confirmDeleteAgentApiV1AgentsAgentIdDeleteConfirmPost",
];
const { mutation: mutationOptions, request: requestOptions } = options
? options.mutation &&
"mutationKey" in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey }, request: undefined };
const mutationFn: MutationFunction<
Awaited<
ReturnType<typeof confirmDeleteAgentApiV1AgentsAgentIdDeleteConfirmPost>
>,
{ agentId: string; data: AgentDeleteConfirm }
> = (props) => {
const { agentId, data } = props ?? {};
return confirmDeleteAgentApiV1AgentsAgentIdDeleteConfirmPost(
agentId,
data,
requestOptions,
);
};
return { mutationFn, ...mutationOptions };
};
export type ConfirmDeleteAgentApiV1AgentsAgentIdDeleteConfirmPostMutationResult =
NonNullable<
Awaited<
ReturnType<typeof confirmDeleteAgentApiV1AgentsAgentIdDeleteConfirmPost>
>
>;
export type ConfirmDeleteAgentApiV1AgentsAgentIdDeleteConfirmPostMutationBody =
AgentDeleteConfirm;
export type ConfirmDeleteAgentApiV1AgentsAgentIdDeleteConfirmPostMutationError =
HTTPValidationError;
/**
* @summary Confirm Delete Agent
*/
export const useConfirmDeleteAgentApiV1AgentsAgentIdDeleteConfirmPost = <
TError = HTTPValidationError,
TContext = unknown,
>(
options?: {
mutation?: UseMutationOptions<
Awaited<
ReturnType<typeof confirmDeleteAgentApiV1AgentsAgentIdDeleteConfirmPost>
>,
TError,
{ agentId: string; data: AgentDeleteConfirm },
TContext
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseMutationResult<
Awaited<
ReturnType<typeof confirmDeleteAgentApiV1AgentsAgentIdDeleteConfirmPost>
>,
TError,
{ agentId: string; data: AgentDeleteConfirm },
TContext
> => {
return useMutation(
getConfirmDeleteAgentApiV1AgentsAgentIdDeleteConfirmPostMutationOptions(
options,
),
queryClient,
);
};

View File

@@ -0,0 +1,855 @@
/**
* Generated by orval v8.2.0 🍺
* Do not edit manually.
* Mission Control API
* OpenAPI spec version: 0.1.0
*/
import { useMutation, useQuery } from "@tanstack/react-query";
import type {
DataTag,
DefinedInitialDataOptions,
DefinedUseQueryResult,
MutationFunction,
QueryClient,
QueryFunction,
QueryKey,
UndefinedInitialDataOptions,
UseMutationOptions,
UseMutationResult,
UseQueryOptions,
UseQueryResult,
} from "@tanstack/react-query";
import type {
ApprovalCreate,
ApprovalRead,
ApprovalUpdate,
HTTPValidationError,
ListApprovalsApiV1BoardsBoardIdApprovalsGetParams,
StreamApprovalsApiV1BoardsBoardIdApprovalsStreamGetParams,
} from ".././model";
import { customFetch } from "../../mutator";
type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1];
/**
* @summary List Approvals
*/
export type listApprovalsApiV1BoardsBoardIdApprovalsGetResponse200 = {
data: ApprovalRead[];
status: 200;
};
export type listApprovalsApiV1BoardsBoardIdApprovalsGetResponse422 = {
data: HTTPValidationError;
status: 422;
};
export type listApprovalsApiV1BoardsBoardIdApprovalsGetResponseSuccess =
listApprovalsApiV1BoardsBoardIdApprovalsGetResponse200 & {
headers: Headers;
};
export type listApprovalsApiV1BoardsBoardIdApprovalsGetResponseError =
listApprovalsApiV1BoardsBoardIdApprovalsGetResponse422 & {
headers: Headers;
};
export type listApprovalsApiV1BoardsBoardIdApprovalsGetResponse =
| listApprovalsApiV1BoardsBoardIdApprovalsGetResponseSuccess
| listApprovalsApiV1BoardsBoardIdApprovalsGetResponseError;
export const getListApprovalsApiV1BoardsBoardIdApprovalsGetUrl = (
boardId: string,
params?: ListApprovalsApiV1BoardsBoardIdApprovalsGetParams,
) => {
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}/approvals?${stringifiedParams}`
: `/api/v1/boards/${boardId}/approvals`;
};
export const listApprovalsApiV1BoardsBoardIdApprovalsGet = async (
boardId: string,
params?: ListApprovalsApiV1BoardsBoardIdApprovalsGetParams,
options?: RequestInit,
): Promise<listApprovalsApiV1BoardsBoardIdApprovalsGetResponse> => {
return customFetch<listApprovalsApiV1BoardsBoardIdApprovalsGetResponse>(
getListApprovalsApiV1BoardsBoardIdApprovalsGetUrl(boardId, params),
{
...options,
method: "GET",
},
);
};
export const getListApprovalsApiV1BoardsBoardIdApprovalsGetQueryKey = (
boardId: string,
params?: ListApprovalsApiV1BoardsBoardIdApprovalsGetParams,
) => {
return [
`/api/v1/boards/${boardId}/approvals`,
...(params ? [params] : []),
] as const;
};
export const getListApprovalsApiV1BoardsBoardIdApprovalsGetQueryOptions = <
TData = Awaited<
ReturnType<typeof listApprovalsApiV1BoardsBoardIdApprovalsGet>
>,
TError = HTTPValidationError,
>(
boardId: string,
params?: ListApprovalsApiV1BoardsBoardIdApprovalsGetParams,
options?: {
query?: Partial<
UseQueryOptions<
Awaited<ReturnType<typeof listApprovalsApiV1BoardsBoardIdApprovalsGet>>,
TError,
TData
>
>;
request?: SecondParameter<typeof customFetch>;
},
) => {
const { query: queryOptions, request: requestOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ??
getListApprovalsApiV1BoardsBoardIdApprovalsGetQueryKey(boardId, params);
const queryFn: QueryFunction<
Awaited<ReturnType<typeof listApprovalsApiV1BoardsBoardIdApprovalsGet>>
> = ({ signal }) =>
listApprovalsApiV1BoardsBoardIdApprovalsGet(boardId, params, {
signal,
...requestOptions,
});
return {
queryKey,
queryFn,
enabled: !!boardId,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof listApprovalsApiV1BoardsBoardIdApprovalsGet>>,
TError,
TData
> & { queryKey: DataTag<QueryKey, TData, TError> };
};
export type ListApprovalsApiV1BoardsBoardIdApprovalsGetQueryResult =
NonNullable<
Awaited<ReturnType<typeof listApprovalsApiV1BoardsBoardIdApprovalsGet>>
>;
export type ListApprovalsApiV1BoardsBoardIdApprovalsGetQueryError =
HTTPValidationError;
export function useListApprovalsApiV1BoardsBoardIdApprovalsGet<
TData = Awaited<
ReturnType<typeof listApprovalsApiV1BoardsBoardIdApprovalsGet>
>,
TError = HTTPValidationError,
>(
boardId: string,
params: undefined | ListApprovalsApiV1BoardsBoardIdApprovalsGetParams,
options: {
query: Partial<
UseQueryOptions<
Awaited<ReturnType<typeof listApprovalsApiV1BoardsBoardIdApprovalsGet>>,
TError,
TData
>
> &
Pick<
DefinedInitialDataOptions<
Awaited<
ReturnType<typeof listApprovalsApiV1BoardsBoardIdApprovalsGet>
>,
TError,
Awaited<
ReturnType<typeof listApprovalsApiV1BoardsBoardIdApprovalsGet>
>
>,
"initialData"
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): DefinedUseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
export function useListApprovalsApiV1BoardsBoardIdApprovalsGet<
TData = Awaited<
ReturnType<typeof listApprovalsApiV1BoardsBoardIdApprovalsGet>
>,
TError = HTTPValidationError,
>(
boardId: string,
params?: ListApprovalsApiV1BoardsBoardIdApprovalsGetParams,
options?: {
query?: Partial<
UseQueryOptions<
Awaited<ReturnType<typeof listApprovalsApiV1BoardsBoardIdApprovalsGet>>,
TError,
TData
>
> &
Pick<
UndefinedInitialDataOptions<
Awaited<
ReturnType<typeof listApprovalsApiV1BoardsBoardIdApprovalsGet>
>,
TError,
Awaited<
ReturnType<typeof listApprovalsApiV1BoardsBoardIdApprovalsGet>
>
>,
"initialData"
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
export function useListApprovalsApiV1BoardsBoardIdApprovalsGet<
TData = Awaited<
ReturnType<typeof listApprovalsApiV1BoardsBoardIdApprovalsGet>
>,
TError = HTTPValidationError,
>(
boardId: string,
params?: ListApprovalsApiV1BoardsBoardIdApprovalsGetParams,
options?: {
query?: Partial<
UseQueryOptions<
Awaited<ReturnType<typeof listApprovalsApiV1BoardsBoardIdApprovalsGet>>,
TError,
TData
>
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
/**
* @summary List Approvals
*/
export function useListApprovalsApiV1BoardsBoardIdApprovalsGet<
TData = Awaited<
ReturnType<typeof listApprovalsApiV1BoardsBoardIdApprovalsGet>
>,
TError = HTTPValidationError,
>(
boardId: string,
params?: ListApprovalsApiV1BoardsBoardIdApprovalsGetParams,
options?: {
query?: Partial<
UseQueryOptions<
Awaited<ReturnType<typeof listApprovalsApiV1BoardsBoardIdApprovalsGet>>,
TError,
TData
>
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
} {
const queryOptions =
getListApprovalsApiV1BoardsBoardIdApprovalsGetQueryOptions(
boardId,
params,
options,
);
const query = useQuery(queryOptions, queryClient) as UseQueryResult<
TData,
TError
> & { queryKey: DataTag<QueryKey, TData, TError> };
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Create Approval
*/
export type createApprovalApiV1BoardsBoardIdApprovalsPostResponse200 = {
data: ApprovalRead;
status: 200;
};
export type createApprovalApiV1BoardsBoardIdApprovalsPostResponse422 = {
data: HTTPValidationError;
status: 422;
};
export type createApprovalApiV1BoardsBoardIdApprovalsPostResponseSuccess =
createApprovalApiV1BoardsBoardIdApprovalsPostResponse200 & {
headers: Headers;
};
export type createApprovalApiV1BoardsBoardIdApprovalsPostResponseError =
createApprovalApiV1BoardsBoardIdApprovalsPostResponse422 & {
headers: Headers;
};
export type createApprovalApiV1BoardsBoardIdApprovalsPostResponse =
| createApprovalApiV1BoardsBoardIdApprovalsPostResponseSuccess
| createApprovalApiV1BoardsBoardIdApprovalsPostResponseError;
export const getCreateApprovalApiV1BoardsBoardIdApprovalsPostUrl = (
boardId: string,
) => {
return `/api/v1/boards/${boardId}/approvals`;
};
export const createApprovalApiV1BoardsBoardIdApprovalsPost = async (
boardId: string,
approvalCreate: ApprovalCreate,
options?: RequestInit,
): Promise<createApprovalApiV1BoardsBoardIdApprovalsPostResponse> => {
return customFetch<createApprovalApiV1BoardsBoardIdApprovalsPostResponse>(
getCreateApprovalApiV1BoardsBoardIdApprovalsPostUrl(boardId),
{
...options,
method: "POST",
headers: { "Content-Type": "application/json", ...options?.headers },
body: JSON.stringify(approvalCreate),
},
);
};
export const getCreateApprovalApiV1BoardsBoardIdApprovalsPostMutationOptions = <
TError = HTTPValidationError,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createApprovalApiV1BoardsBoardIdApprovalsPost>>,
TError,
{ boardId: string; data: ApprovalCreate },
TContext
>;
request?: SecondParameter<typeof customFetch>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createApprovalApiV1BoardsBoardIdApprovalsPost>>,
TError,
{ boardId: string; data: ApprovalCreate },
TContext
> => {
const mutationKey = ["createApprovalApiV1BoardsBoardIdApprovalsPost"];
const { mutation: mutationOptions, request: requestOptions } = options
? options.mutation &&
"mutationKey" in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey }, request: undefined };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof createApprovalApiV1BoardsBoardIdApprovalsPost>>,
{ boardId: string; data: ApprovalCreate }
> = (props) => {
const { boardId, data } = props ?? {};
return createApprovalApiV1BoardsBoardIdApprovalsPost(
boardId,
data,
requestOptions,
);
};
return { mutationFn, ...mutationOptions };
};
export type CreateApprovalApiV1BoardsBoardIdApprovalsPostMutationResult =
NonNullable<
Awaited<ReturnType<typeof createApprovalApiV1BoardsBoardIdApprovalsPost>>
>;
export type CreateApprovalApiV1BoardsBoardIdApprovalsPostMutationBody =
ApprovalCreate;
export type CreateApprovalApiV1BoardsBoardIdApprovalsPostMutationError =
HTTPValidationError;
/**
* @summary Create Approval
*/
export const useCreateApprovalApiV1BoardsBoardIdApprovalsPost = <
TError = HTTPValidationError,
TContext = unknown,
>(
options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createApprovalApiV1BoardsBoardIdApprovalsPost>>,
TError,
{ boardId: string; data: ApprovalCreate },
TContext
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseMutationResult<
Awaited<ReturnType<typeof createApprovalApiV1BoardsBoardIdApprovalsPost>>,
TError,
{ boardId: string; data: ApprovalCreate },
TContext
> => {
return useMutation(
getCreateApprovalApiV1BoardsBoardIdApprovalsPostMutationOptions(options),
queryClient,
);
};
/**
* @summary Stream Approvals
*/
export type streamApprovalsApiV1BoardsBoardIdApprovalsStreamGetResponse200 = {
data: unknown;
status: 200;
};
export type streamApprovalsApiV1BoardsBoardIdApprovalsStreamGetResponse422 = {
data: HTTPValidationError;
status: 422;
};
export type streamApprovalsApiV1BoardsBoardIdApprovalsStreamGetResponseSuccess =
streamApprovalsApiV1BoardsBoardIdApprovalsStreamGetResponse200 & {
headers: Headers;
};
export type streamApprovalsApiV1BoardsBoardIdApprovalsStreamGetResponseError =
streamApprovalsApiV1BoardsBoardIdApprovalsStreamGetResponse422 & {
headers: Headers;
};
export type streamApprovalsApiV1BoardsBoardIdApprovalsStreamGetResponse =
| streamApprovalsApiV1BoardsBoardIdApprovalsStreamGetResponseSuccess
| streamApprovalsApiV1BoardsBoardIdApprovalsStreamGetResponseError;
export const getStreamApprovalsApiV1BoardsBoardIdApprovalsStreamGetUrl = (
boardId: string,
params?: StreamApprovalsApiV1BoardsBoardIdApprovalsStreamGetParams,
) => {
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}/approvals/stream?${stringifiedParams}`
: `/api/v1/boards/${boardId}/approvals/stream`;
};
export const streamApprovalsApiV1BoardsBoardIdApprovalsStreamGet = async (
boardId: string,
params?: StreamApprovalsApiV1BoardsBoardIdApprovalsStreamGetParams,
options?: RequestInit,
): Promise<streamApprovalsApiV1BoardsBoardIdApprovalsStreamGetResponse> => {
return customFetch<streamApprovalsApiV1BoardsBoardIdApprovalsStreamGetResponse>(
getStreamApprovalsApiV1BoardsBoardIdApprovalsStreamGetUrl(boardId, params),
{
...options,
method: "GET",
},
);
};
export const getStreamApprovalsApiV1BoardsBoardIdApprovalsStreamGetQueryKey = (
boardId: string,
params?: StreamApprovalsApiV1BoardsBoardIdApprovalsStreamGetParams,
) => {
return [
`/api/v1/boards/${boardId}/approvals/stream`,
...(params ? [params] : []),
] as const;
};
export const getStreamApprovalsApiV1BoardsBoardIdApprovalsStreamGetQueryOptions =
<
TData = Awaited<
ReturnType<typeof streamApprovalsApiV1BoardsBoardIdApprovalsStreamGet>
>,
TError = HTTPValidationError,
>(
boardId: string,
params?: StreamApprovalsApiV1BoardsBoardIdApprovalsStreamGetParams,
options?: {
query?: Partial<
UseQueryOptions<
Awaited<
ReturnType<
typeof streamApprovalsApiV1BoardsBoardIdApprovalsStreamGet
>
>,
TError,
TData
>
>;
request?: SecondParameter<typeof customFetch>;
},
) => {
const { query: queryOptions, request: requestOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ??
getStreamApprovalsApiV1BoardsBoardIdApprovalsStreamGetQueryKey(
boardId,
params,
);
const queryFn: QueryFunction<
Awaited<
ReturnType<typeof streamApprovalsApiV1BoardsBoardIdApprovalsStreamGet>
>
> = ({ signal }) =>
streamApprovalsApiV1BoardsBoardIdApprovalsStreamGet(boardId, params, {
signal,
...requestOptions,
});
return {
queryKey,
queryFn,
enabled: !!boardId,
...queryOptions,
} as UseQueryOptions<
Awaited<
ReturnType<typeof streamApprovalsApiV1BoardsBoardIdApprovalsStreamGet>
>,
TError,
TData
> & { queryKey: DataTag<QueryKey, TData, TError> };
};
export type StreamApprovalsApiV1BoardsBoardIdApprovalsStreamGetQueryResult =
NonNullable<
Awaited<
ReturnType<typeof streamApprovalsApiV1BoardsBoardIdApprovalsStreamGet>
>
>;
export type StreamApprovalsApiV1BoardsBoardIdApprovalsStreamGetQueryError =
HTTPValidationError;
export function useStreamApprovalsApiV1BoardsBoardIdApprovalsStreamGet<
TData = Awaited<
ReturnType<typeof streamApprovalsApiV1BoardsBoardIdApprovalsStreamGet>
>,
TError = HTTPValidationError,
>(
boardId: string,
params: undefined | StreamApprovalsApiV1BoardsBoardIdApprovalsStreamGetParams,
options: {
query: Partial<
UseQueryOptions<
Awaited<
ReturnType<typeof streamApprovalsApiV1BoardsBoardIdApprovalsStreamGet>
>,
TError,
TData
>
> &
Pick<
DefinedInitialDataOptions<
Awaited<
ReturnType<
typeof streamApprovalsApiV1BoardsBoardIdApprovalsStreamGet
>
>,
TError,
Awaited<
ReturnType<
typeof streamApprovalsApiV1BoardsBoardIdApprovalsStreamGet
>
>
>,
"initialData"
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): DefinedUseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
export function useStreamApprovalsApiV1BoardsBoardIdApprovalsStreamGet<
TData = Awaited<
ReturnType<typeof streamApprovalsApiV1BoardsBoardIdApprovalsStreamGet>
>,
TError = HTTPValidationError,
>(
boardId: string,
params?: StreamApprovalsApiV1BoardsBoardIdApprovalsStreamGetParams,
options?: {
query?: Partial<
UseQueryOptions<
Awaited<
ReturnType<typeof streamApprovalsApiV1BoardsBoardIdApprovalsStreamGet>
>,
TError,
TData
>
> &
Pick<
UndefinedInitialDataOptions<
Awaited<
ReturnType<
typeof streamApprovalsApiV1BoardsBoardIdApprovalsStreamGet
>
>,
TError,
Awaited<
ReturnType<
typeof streamApprovalsApiV1BoardsBoardIdApprovalsStreamGet
>
>
>,
"initialData"
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
export function useStreamApprovalsApiV1BoardsBoardIdApprovalsStreamGet<
TData = Awaited<
ReturnType<typeof streamApprovalsApiV1BoardsBoardIdApprovalsStreamGet>
>,
TError = HTTPValidationError,
>(
boardId: string,
params?: StreamApprovalsApiV1BoardsBoardIdApprovalsStreamGetParams,
options?: {
query?: Partial<
UseQueryOptions<
Awaited<
ReturnType<typeof streamApprovalsApiV1BoardsBoardIdApprovalsStreamGet>
>,
TError,
TData
>
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
/**
* @summary Stream Approvals
*/
export function useStreamApprovalsApiV1BoardsBoardIdApprovalsStreamGet<
TData = Awaited<
ReturnType<typeof streamApprovalsApiV1BoardsBoardIdApprovalsStreamGet>
>,
TError = HTTPValidationError,
>(
boardId: string,
params?: StreamApprovalsApiV1BoardsBoardIdApprovalsStreamGetParams,
options?: {
query?: Partial<
UseQueryOptions<
Awaited<
ReturnType<typeof streamApprovalsApiV1BoardsBoardIdApprovalsStreamGet>
>,
TError,
TData
>
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
} {
const queryOptions =
getStreamApprovalsApiV1BoardsBoardIdApprovalsStreamGetQueryOptions(
boardId,
params,
options,
);
const query = useQuery(queryOptions, queryClient) as UseQueryResult<
TData,
TError
> & { queryKey: DataTag<QueryKey, TData, TError> };
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Update Approval
*/
export type updateApprovalApiV1BoardsBoardIdApprovalsApprovalIdPatchResponse200 =
{
data: ApprovalRead;
status: 200;
};
export type updateApprovalApiV1BoardsBoardIdApprovalsApprovalIdPatchResponse422 =
{
data: HTTPValidationError;
status: 422;
};
export type updateApprovalApiV1BoardsBoardIdApprovalsApprovalIdPatchResponseSuccess =
updateApprovalApiV1BoardsBoardIdApprovalsApprovalIdPatchResponse200 & {
headers: Headers;
};
export type updateApprovalApiV1BoardsBoardIdApprovalsApprovalIdPatchResponseError =
updateApprovalApiV1BoardsBoardIdApprovalsApprovalIdPatchResponse422 & {
headers: Headers;
};
export type updateApprovalApiV1BoardsBoardIdApprovalsApprovalIdPatchResponse =
| updateApprovalApiV1BoardsBoardIdApprovalsApprovalIdPatchResponseSuccess
| updateApprovalApiV1BoardsBoardIdApprovalsApprovalIdPatchResponseError;
export const getUpdateApprovalApiV1BoardsBoardIdApprovalsApprovalIdPatchUrl = (
boardId: string,
approvalId: string,
) => {
return `/api/v1/boards/${boardId}/approvals/${approvalId}`;
};
export const updateApprovalApiV1BoardsBoardIdApprovalsApprovalIdPatch = async (
boardId: string,
approvalId: string,
approvalUpdate: ApprovalUpdate,
options?: RequestInit,
): Promise<updateApprovalApiV1BoardsBoardIdApprovalsApprovalIdPatchResponse> => {
return customFetch<updateApprovalApiV1BoardsBoardIdApprovalsApprovalIdPatchResponse>(
getUpdateApprovalApiV1BoardsBoardIdApprovalsApprovalIdPatchUrl(
boardId,
approvalId,
),
{
...options,
method: "PATCH",
headers: { "Content-Type": "application/json", ...options?.headers },
body: JSON.stringify(approvalUpdate),
},
);
};
export const getUpdateApprovalApiV1BoardsBoardIdApprovalsApprovalIdPatchMutationOptions =
<TError = HTTPValidationError, TContext = unknown>(options?: {
mutation?: UseMutationOptions<
Awaited<
ReturnType<
typeof updateApprovalApiV1BoardsBoardIdApprovalsApprovalIdPatch
>
>,
TError,
{ boardId: string; approvalId: string; data: ApprovalUpdate },
TContext
>;
request?: SecondParameter<typeof customFetch>;
}): UseMutationOptions<
Awaited<
ReturnType<
typeof updateApprovalApiV1BoardsBoardIdApprovalsApprovalIdPatch
>
>,
TError,
{ boardId: string; approvalId: string; data: ApprovalUpdate },
TContext
> => {
const mutationKey = [
"updateApprovalApiV1BoardsBoardIdApprovalsApprovalIdPatch",
];
const { mutation: mutationOptions, request: requestOptions } = options
? options.mutation &&
"mutationKey" in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey }, request: undefined };
const mutationFn: MutationFunction<
Awaited<
ReturnType<
typeof updateApprovalApiV1BoardsBoardIdApprovalsApprovalIdPatch
>
>,
{ boardId: string; approvalId: string; data: ApprovalUpdate }
> = (props) => {
const { boardId, approvalId, data } = props ?? {};
return updateApprovalApiV1BoardsBoardIdApprovalsApprovalIdPatch(
boardId,
approvalId,
data,
requestOptions,
);
};
return { mutationFn, ...mutationOptions };
};
export type UpdateApprovalApiV1BoardsBoardIdApprovalsApprovalIdPatchMutationResult =
NonNullable<
Awaited<
ReturnType<
typeof updateApprovalApiV1BoardsBoardIdApprovalsApprovalIdPatch
>
>
>;
export type UpdateApprovalApiV1BoardsBoardIdApprovalsApprovalIdPatchMutationBody =
ApprovalUpdate;
export type UpdateApprovalApiV1BoardsBoardIdApprovalsApprovalIdPatchMutationError =
HTTPValidationError;
/**
* @summary Update Approval
*/
export const useUpdateApprovalApiV1BoardsBoardIdApprovalsApprovalIdPatch = <
TError = HTTPValidationError,
TContext = unknown,
>(
options?: {
mutation?: UseMutationOptions<
Awaited<
ReturnType<
typeof updateApprovalApiV1BoardsBoardIdApprovalsApprovalIdPatch
>
>,
TError,
{ boardId: string; approvalId: string; data: ApprovalUpdate },
TContext
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseMutationResult<
Awaited<
ReturnType<typeof updateApprovalApiV1BoardsBoardIdApprovalsApprovalIdPatch>
>,
TError,
{ boardId: string; approvalId: string; data: ApprovalUpdate },
TContext
> => {
return useMutation(
getUpdateApprovalApiV1BoardsBoardIdApprovalsApprovalIdPatchMutationOptions(
options,
),
queryClient,
);
};

View File

@@ -0,0 +1,689 @@
/**
* Generated by orval v8.2.0 🍺
* Do not edit manually.
* Mission Control API
* OpenAPI spec version: 0.1.0
*/
import { useMutation, useQuery } from "@tanstack/react-query";
import type {
DataTag,
DefinedInitialDataOptions,
DefinedUseQueryResult,
MutationFunction,
QueryClient,
QueryFunction,
QueryKey,
UndefinedInitialDataOptions,
UseMutationOptions,
UseMutationResult,
UseQueryOptions,
UseQueryResult,
} from "@tanstack/react-query";
import type {
BoardMemoryCreate,
BoardMemoryRead,
HTTPValidationError,
ListBoardMemoryApiV1BoardsBoardIdMemoryGetParams,
StreamBoardMemoryApiV1BoardsBoardIdMemoryStreamGetParams,
} from ".././model";
import { customFetch } from "../../mutator";
type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1];
/**
* @summary List Board Memory
*/
export type listBoardMemoryApiV1BoardsBoardIdMemoryGetResponse200 = {
data: BoardMemoryRead[];
status: 200;
};
export type listBoardMemoryApiV1BoardsBoardIdMemoryGetResponse422 = {
data: HTTPValidationError;
status: 422;
};
export type listBoardMemoryApiV1BoardsBoardIdMemoryGetResponseSuccess =
listBoardMemoryApiV1BoardsBoardIdMemoryGetResponse200 & {
headers: Headers;
};
export type listBoardMemoryApiV1BoardsBoardIdMemoryGetResponseError =
listBoardMemoryApiV1BoardsBoardIdMemoryGetResponse422 & {
headers: Headers;
};
export type listBoardMemoryApiV1BoardsBoardIdMemoryGetResponse =
| listBoardMemoryApiV1BoardsBoardIdMemoryGetResponseSuccess
| listBoardMemoryApiV1BoardsBoardIdMemoryGetResponseError;
export const getListBoardMemoryApiV1BoardsBoardIdMemoryGetUrl = (
boardId: string,
params?: ListBoardMemoryApiV1BoardsBoardIdMemoryGetParams,
) => {
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}/memory?${stringifiedParams}`
: `/api/v1/boards/${boardId}/memory`;
};
export const listBoardMemoryApiV1BoardsBoardIdMemoryGet = async (
boardId: string,
params?: ListBoardMemoryApiV1BoardsBoardIdMemoryGetParams,
options?: RequestInit,
): Promise<listBoardMemoryApiV1BoardsBoardIdMemoryGetResponse> => {
return customFetch<listBoardMemoryApiV1BoardsBoardIdMemoryGetResponse>(
getListBoardMemoryApiV1BoardsBoardIdMemoryGetUrl(boardId, params),
{
...options,
method: "GET",
},
);
};
export const getListBoardMemoryApiV1BoardsBoardIdMemoryGetQueryKey = (
boardId: string,
params?: ListBoardMemoryApiV1BoardsBoardIdMemoryGetParams,
) => {
return [
`/api/v1/boards/${boardId}/memory`,
...(params ? [params] : []),
] as const;
};
export const getListBoardMemoryApiV1BoardsBoardIdMemoryGetQueryOptions = <
TData = Awaited<
ReturnType<typeof listBoardMemoryApiV1BoardsBoardIdMemoryGet>
>,
TError = HTTPValidationError,
>(
boardId: string,
params?: ListBoardMemoryApiV1BoardsBoardIdMemoryGetParams,
options?: {
query?: Partial<
UseQueryOptions<
Awaited<ReturnType<typeof listBoardMemoryApiV1BoardsBoardIdMemoryGet>>,
TError,
TData
>
>;
request?: SecondParameter<typeof customFetch>;
},
) => {
const { query: queryOptions, request: requestOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ??
getListBoardMemoryApiV1BoardsBoardIdMemoryGetQueryKey(boardId, params);
const queryFn: QueryFunction<
Awaited<ReturnType<typeof listBoardMemoryApiV1BoardsBoardIdMemoryGet>>
> = ({ signal }) =>
listBoardMemoryApiV1BoardsBoardIdMemoryGet(boardId, params, {
signal,
...requestOptions,
});
return {
queryKey,
queryFn,
enabled: !!boardId,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof listBoardMemoryApiV1BoardsBoardIdMemoryGet>>,
TError,
TData
> & { queryKey: DataTag<QueryKey, TData, TError> };
};
export type ListBoardMemoryApiV1BoardsBoardIdMemoryGetQueryResult = NonNullable<
Awaited<ReturnType<typeof listBoardMemoryApiV1BoardsBoardIdMemoryGet>>
>;
export type ListBoardMemoryApiV1BoardsBoardIdMemoryGetQueryError =
HTTPValidationError;
export function useListBoardMemoryApiV1BoardsBoardIdMemoryGet<
TData = Awaited<
ReturnType<typeof listBoardMemoryApiV1BoardsBoardIdMemoryGet>
>,
TError = HTTPValidationError,
>(
boardId: string,
params: undefined | ListBoardMemoryApiV1BoardsBoardIdMemoryGetParams,
options: {
query: Partial<
UseQueryOptions<
Awaited<ReturnType<typeof listBoardMemoryApiV1BoardsBoardIdMemoryGet>>,
TError,
TData
>
> &
Pick<
DefinedInitialDataOptions<
Awaited<
ReturnType<typeof listBoardMemoryApiV1BoardsBoardIdMemoryGet>
>,
TError,
Awaited<ReturnType<typeof listBoardMemoryApiV1BoardsBoardIdMemoryGet>>
>,
"initialData"
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): DefinedUseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
export function useListBoardMemoryApiV1BoardsBoardIdMemoryGet<
TData = Awaited<
ReturnType<typeof listBoardMemoryApiV1BoardsBoardIdMemoryGet>
>,
TError = HTTPValidationError,
>(
boardId: string,
params?: ListBoardMemoryApiV1BoardsBoardIdMemoryGetParams,
options?: {
query?: Partial<
UseQueryOptions<
Awaited<ReturnType<typeof listBoardMemoryApiV1BoardsBoardIdMemoryGet>>,
TError,
TData
>
> &
Pick<
UndefinedInitialDataOptions<
Awaited<
ReturnType<typeof listBoardMemoryApiV1BoardsBoardIdMemoryGet>
>,
TError,
Awaited<ReturnType<typeof listBoardMemoryApiV1BoardsBoardIdMemoryGet>>
>,
"initialData"
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
export function useListBoardMemoryApiV1BoardsBoardIdMemoryGet<
TData = Awaited<
ReturnType<typeof listBoardMemoryApiV1BoardsBoardIdMemoryGet>
>,
TError = HTTPValidationError,
>(
boardId: string,
params?: ListBoardMemoryApiV1BoardsBoardIdMemoryGetParams,
options?: {
query?: Partial<
UseQueryOptions<
Awaited<ReturnType<typeof listBoardMemoryApiV1BoardsBoardIdMemoryGet>>,
TError,
TData
>
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
/**
* @summary List Board Memory
*/
export function useListBoardMemoryApiV1BoardsBoardIdMemoryGet<
TData = Awaited<
ReturnType<typeof listBoardMemoryApiV1BoardsBoardIdMemoryGet>
>,
TError = HTTPValidationError,
>(
boardId: string,
params?: ListBoardMemoryApiV1BoardsBoardIdMemoryGetParams,
options?: {
query?: Partial<
UseQueryOptions<
Awaited<ReturnType<typeof listBoardMemoryApiV1BoardsBoardIdMemoryGet>>,
TError,
TData
>
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
} {
const queryOptions =
getListBoardMemoryApiV1BoardsBoardIdMemoryGetQueryOptions(
boardId,
params,
options,
);
const query = useQuery(queryOptions, queryClient) as UseQueryResult<
TData,
TError
> & { queryKey: DataTag<QueryKey, TData, TError> };
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Create Board Memory
*/
export type createBoardMemoryApiV1BoardsBoardIdMemoryPostResponse200 = {
data: BoardMemoryRead;
status: 200;
};
export type createBoardMemoryApiV1BoardsBoardIdMemoryPostResponse422 = {
data: HTTPValidationError;
status: 422;
};
export type createBoardMemoryApiV1BoardsBoardIdMemoryPostResponseSuccess =
createBoardMemoryApiV1BoardsBoardIdMemoryPostResponse200 & {
headers: Headers;
};
export type createBoardMemoryApiV1BoardsBoardIdMemoryPostResponseError =
createBoardMemoryApiV1BoardsBoardIdMemoryPostResponse422 & {
headers: Headers;
};
export type createBoardMemoryApiV1BoardsBoardIdMemoryPostResponse =
| createBoardMemoryApiV1BoardsBoardIdMemoryPostResponseSuccess
| createBoardMemoryApiV1BoardsBoardIdMemoryPostResponseError;
export const getCreateBoardMemoryApiV1BoardsBoardIdMemoryPostUrl = (
boardId: string,
) => {
return `/api/v1/boards/${boardId}/memory`;
};
export const createBoardMemoryApiV1BoardsBoardIdMemoryPost = async (
boardId: string,
boardMemoryCreate: BoardMemoryCreate,
options?: RequestInit,
): Promise<createBoardMemoryApiV1BoardsBoardIdMemoryPostResponse> => {
return customFetch<createBoardMemoryApiV1BoardsBoardIdMemoryPostResponse>(
getCreateBoardMemoryApiV1BoardsBoardIdMemoryPostUrl(boardId),
{
...options,
method: "POST",
headers: { "Content-Type": "application/json", ...options?.headers },
body: JSON.stringify(boardMemoryCreate),
},
);
};
export const getCreateBoardMemoryApiV1BoardsBoardIdMemoryPostMutationOptions = <
TError = HTTPValidationError,
TContext = unknown,
>(options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createBoardMemoryApiV1BoardsBoardIdMemoryPost>>,
TError,
{ boardId: string; data: BoardMemoryCreate },
TContext
>;
request?: SecondParameter<typeof customFetch>;
}): UseMutationOptions<
Awaited<ReturnType<typeof createBoardMemoryApiV1BoardsBoardIdMemoryPost>>,
TError,
{ boardId: string; data: BoardMemoryCreate },
TContext
> => {
const mutationKey = ["createBoardMemoryApiV1BoardsBoardIdMemoryPost"];
const { mutation: mutationOptions, request: requestOptions } = options
? options.mutation &&
"mutationKey" in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey }, request: undefined };
const mutationFn: MutationFunction<
Awaited<ReturnType<typeof createBoardMemoryApiV1BoardsBoardIdMemoryPost>>,
{ boardId: string; data: BoardMemoryCreate }
> = (props) => {
const { boardId, data } = props ?? {};
return createBoardMemoryApiV1BoardsBoardIdMemoryPost(
boardId,
data,
requestOptions,
);
};
return { mutationFn, ...mutationOptions };
};
export type CreateBoardMemoryApiV1BoardsBoardIdMemoryPostMutationResult =
NonNullable<
Awaited<ReturnType<typeof createBoardMemoryApiV1BoardsBoardIdMemoryPost>>
>;
export type CreateBoardMemoryApiV1BoardsBoardIdMemoryPostMutationBody =
BoardMemoryCreate;
export type CreateBoardMemoryApiV1BoardsBoardIdMemoryPostMutationError =
HTTPValidationError;
/**
* @summary Create Board Memory
*/
export const useCreateBoardMemoryApiV1BoardsBoardIdMemoryPost = <
TError = HTTPValidationError,
TContext = unknown,
>(
options?: {
mutation?: UseMutationOptions<
Awaited<ReturnType<typeof createBoardMemoryApiV1BoardsBoardIdMemoryPost>>,
TError,
{ boardId: string; data: BoardMemoryCreate },
TContext
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseMutationResult<
Awaited<ReturnType<typeof createBoardMemoryApiV1BoardsBoardIdMemoryPost>>,
TError,
{ boardId: string; data: BoardMemoryCreate },
TContext
> => {
return useMutation(
getCreateBoardMemoryApiV1BoardsBoardIdMemoryPostMutationOptions(options),
queryClient,
);
};
/**
* @summary Stream Board Memory
*/
export type streamBoardMemoryApiV1BoardsBoardIdMemoryStreamGetResponse200 = {
data: unknown;
status: 200;
};
export type streamBoardMemoryApiV1BoardsBoardIdMemoryStreamGetResponse422 = {
data: HTTPValidationError;
status: 422;
};
export type streamBoardMemoryApiV1BoardsBoardIdMemoryStreamGetResponseSuccess =
streamBoardMemoryApiV1BoardsBoardIdMemoryStreamGetResponse200 & {
headers: Headers;
};
export type streamBoardMemoryApiV1BoardsBoardIdMemoryStreamGetResponseError =
streamBoardMemoryApiV1BoardsBoardIdMemoryStreamGetResponse422 & {
headers: Headers;
};
export type streamBoardMemoryApiV1BoardsBoardIdMemoryStreamGetResponse =
| streamBoardMemoryApiV1BoardsBoardIdMemoryStreamGetResponseSuccess
| streamBoardMemoryApiV1BoardsBoardIdMemoryStreamGetResponseError;
export const getStreamBoardMemoryApiV1BoardsBoardIdMemoryStreamGetUrl = (
boardId: string,
params?: StreamBoardMemoryApiV1BoardsBoardIdMemoryStreamGetParams,
) => {
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}/memory/stream?${stringifiedParams}`
: `/api/v1/boards/${boardId}/memory/stream`;
};
export const streamBoardMemoryApiV1BoardsBoardIdMemoryStreamGet = async (
boardId: string,
params?: StreamBoardMemoryApiV1BoardsBoardIdMemoryStreamGetParams,
options?: RequestInit,
): Promise<streamBoardMemoryApiV1BoardsBoardIdMemoryStreamGetResponse> => {
return customFetch<streamBoardMemoryApiV1BoardsBoardIdMemoryStreamGetResponse>(
getStreamBoardMemoryApiV1BoardsBoardIdMemoryStreamGetUrl(boardId, params),
{
...options,
method: "GET",
},
);
};
export const getStreamBoardMemoryApiV1BoardsBoardIdMemoryStreamGetQueryKey = (
boardId: string,
params?: StreamBoardMemoryApiV1BoardsBoardIdMemoryStreamGetParams,
) => {
return [
`/api/v1/boards/${boardId}/memory/stream`,
...(params ? [params] : []),
] as const;
};
export const getStreamBoardMemoryApiV1BoardsBoardIdMemoryStreamGetQueryOptions =
<
TData = Awaited<
ReturnType<typeof streamBoardMemoryApiV1BoardsBoardIdMemoryStreamGet>
>,
TError = HTTPValidationError,
>(
boardId: string,
params?: StreamBoardMemoryApiV1BoardsBoardIdMemoryStreamGetParams,
options?: {
query?: Partial<
UseQueryOptions<
Awaited<
ReturnType<
typeof streamBoardMemoryApiV1BoardsBoardIdMemoryStreamGet
>
>,
TError,
TData
>
>;
request?: SecondParameter<typeof customFetch>;
},
) => {
const { query: queryOptions, request: requestOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ??
getStreamBoardMemoryApiV1BoardsBoardIdMemoryStreamGetQueryKey(
boardId,
params,
);
const queryFn: QueryFunction<
Awaited<
ReturnType<typeof streamBoardMemoryApiV1BoardsBoardIdMemoryStreamGet>
>
> = ({ signal }) =>
streamBoardMemoryApiV1BoardsBoardIdMemoryStreamGet(boardId, params, {
signal,
...requestOptions,
});
return {
queryKey,
queryFn,
enabled: !!boardId,
...queryOptions,
} as UseQueryOptions<
Awaited<
ReturnType<typeof streamBoardMemoryApiV1BoardsBoardIdMemoryStreamGet>
>,
TError,
TData
> & { queryKey: DataTag<QueryKey, TData, TError> };
};
export type StreamBoardMemoryApiV1BoardsBoardIdMemoryStreamGetQueryResult =
NonNullable<
Awaited<
ReturnType<typeof streamBoardMemoryApiV1BoardsBoardIdMemoryStreamGet>
>
>;
export type StreamBoardMemoryApiV1BoardsBoardIdMemoryStreamGetQueryError =
HTTPValidationError;
export function useStreamBoardMemoryApiV1BoardsBoardIdMemoryStreamGet<
TData = Awaited<
ReturnType<typeof streamBoardMemoryApiV1BoardsBoardIdMemoryStreamGet>
>,
TError = HTTPValidationError,
>(
boardId: string,
params: undefined | StreamBoardMemoryApiV1BoardsBoardIdMemoryStreamGetParams,
options: {
query: Partial<
UseQueryOptions<
Awaited<
ReturnType<typeof streamBoardMemoryApiV1BoardsBoardIdMemoryStreamGet>
>,
TError,
TData
>
> &
Pick<
DefinedInitialDataOptions<
Awaited<
ReturnType<
typeof streamBoardMemoryApiV1BoardsBoardIdMemoryStreamGet
>
>,
TError,
Awaited<
ReturnType<
typeof streamBoardMemoryApiV1BoardsBoardIdMemoryStreamGet
>
>
>,
"initialData"
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): DefinedUseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
export function useStreamBoardMemoryApiV1BoardsBoardIdMemoryStreamGet<
TData = Awaited<
ReturnType<typeof streamBoardMemoryApiV1BoardsBoardIdMemoryStreamGet>
>,
TError = HTTPValidationError,
>(
boardId: string,
params?: StreamBoardMemoryApiV1BoardsBoardIdMemoryStreamGetParams,
options?: {
query?: Partial<
UseQueryOptions<
Awaited<
ReturnType<typeof streamBoardMemoryApiV1BoardsBoardIdMemoryStreamGet>
>,
TError,
TData
>
> &
Pick<
UndefinedInitialDataOptions<
Awaited<
ReturnType<
typeof streamBoardMemoryApiV1BoardsBoardIdMemoryStreamGet
>
>,
TError,
Awaited<
ReturnType<
typeof streamBoardMemoryApiV1BoardsBoardIdMemoryStreamGet
>
>
>,
"initialData"
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
export function useStreamBoardMemoryApiV1BoardsBoardIdMemoryStreamGet<
TData = Awaited<
ReturnType<typeof streamBoardMemoryApiV1BoardsBoardIdMemoryStreamGet>
>,
TError = HTTPValidationError,
>(
boardId: string,
params?: StreamBoardMemoryApiV1BoardsBoardIdMemoryStreamGetParams,
options?: {
query?: Partial<
UseQueryOptions<
Awaited<
ReturnType<typeof streamBoardMemoryApiV1BoardsBoardIdMemoryStreamGet>
>,
TError,
TData
>
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
/**
* @summary Stream Board Memory
*/
export function useStreamBoardMemoryApiV1BoardsBoardIdMemoryStreamGet<
TData = Awaited<
ReturnType<typeof streamBoardMemoryApiV1BoardsBoardIdMemoryStreamGet>
>,
TError = HTTPValidationError,
>(
boardId: string,
params?: StreamBoardMemoryApiV1BoardsBoardIdMemoryStreamGetParams,
options?: {
query?: Partial<
UseQueryOptions<
Awaited<
ReturnType<typeof streamBoardMemoryApiV1BoardsBoardIdMemoryStreamGet>
>,
TError,
TData
>
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
} {
const queryOptions =
getStreamBoardMemoryApiV1BoardsBoardIdMemoryStreamGetQueryOptions(
boardId,
params,
options,
);
const query = useQuery(queryOptions, queryClient) as UseQueryResult<
TData,
TError
> & { queryKey: DataTag<QueryKey, TData, TError> };
return { ...query, queryKey: queryOptions.queryKey };
}

View File

@@ -0,0 +1,892 @@
/**
* Generated by orval v8.2.0 🍺
* Do not edit manually.
* Mission Control API
* OpenAPI spec version: 0.1.0
*/
import { useMutation, useQuery } from "@tanstack/react-query";
import type {
DataTag,
DefinedInitialDataOptions,
DefinedUseQueryResult,
MutationFunction,
QueryClient,
QueryFunction,
QueryKey,
UndefinedInitialDataOptions,
UseMutationOptions,
UseMutationResult,
UseQueryOptions,
UseQueryResult,
} from "@tanstack/react-query";
import type {
BoardOnboardingAgentComplete,
BoardOnboardingAgentQuestion,
BoardOnboardingAnswer,
BoardOnboardingConfirm,
BoardOnboardingRead,
BoardOnboardingStart,
BoardRead,
HTTPValidationError,
} from ".././model";
import { customFetch } from "../../mutator";
type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1];
/**
* @summary Get Onboarding
*/
export type getOnboardingApiV1BoardsBoardIdOnboardingGetResponse200 = {
data: BoardOnboardingRead;
status: 200;
};
export type getOnboardingApiV1BoardsBoardIdOnboardingGetResponse422 = {
data: HTTPValidationError;
status: 422;
};
export type getOnboardingApiV1BoardsBoardIdOnboardingGetResponseSuccess =
getOnboardingApiV1BoardsBoardIdOnboardingGetResponse200 & {
headers: Headers;
};
export type getOnboardingApiV1BoardsBoardIdOnboardingGetResponseError =
getOnboardingApiV1BoardsBoardIdOnboardingGetResponse422 & {
headers: Headers;
};
export type getOnboardingApiV1BoardsBoardIdOnboardingGetResponse =
| getOnboardingApiV1BoardsBoardIdOnboardingGetResponseSuccess
| getOnboardingApiV1BoardsBoardIdOnboardingGetResponseError;
export const getGetOnboardingApiV1BoardsBoardIdOnboardingGetUrl = (
boardId: string,
) => {
return `/api/v1/boards/${boardId}/onboarding`;
};
export const getOnboardingApiV1BoardsBoardIdOnboardingGet = async (
boardId: string,
options?: RequestInit,
): Promise<getOnboardingApiV1BoardsBoardIdOnboardingGetResponse> => {
return customFetch<getOnboardingApiV1BoardsBoardIdOnboardingGetResponse>(
getGetOnboardingApiV1BoardsBoardIdOnboardingGetUrl(boardId),
{
...options,
method: "GET",
},
);
};
export const getGetOnboardingApiV1BoardsBoardIdOnboardingGetQueryKey = (
boardId: string,
) => {
return [`/api/v1/boards/${boardId}/onboarding`] as const;
};
export const getGetOnboardingApiV1BoardsBoardIdOnboardingGetQueryOptions = <
TData = Awaited<
ReturnType<typeof getOnboardingApiV1BoardsBoardIdOnboardingGet>
>,
TError = HTTPValidationError,
>(
boardId: string,
options?: {
query?: Partial<
UseQueryOptions<
Awaited<
ReturnType<typeof getOnboardingApiV1BoardsBoardIdOnboardingGet>
>,
TError,
TData
>
>;
request?: SecondParameter<typeof customFetch>;
},
) => {
const { query: queryOptions, request: requestOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ??
getGetOnboardingApiV1BoardsBoardIdOnboardingGetQueryKey(boardId);
const queryFn: QueryFunction<
Awaited<ReturnType<typeof getOnboardingApiV1BoardsBoardIdOnboardingGet>>
> = ({ signal }) =>
getOnboardingApiV1BoardsBoardIdOnboardingGet(boardId, {
signal,
...requestOptions,
});
return {
queryKey,
queryFn,
enabled: !!boardId,
...queryOptions,
} as UseQueryOptions<
Awaited<ReturnType<typeof getOnboardingApiV1BoardsBoardIdOnboardingGet>>,
TError,
TData
> & { queryKey: DataTag<QueryKey, TData, TError> };
};
export type GetOnboardingApiV1BoardsBoardIdOnboardingGetQueryResult =
NonNullable<
Awaited<ReturnType<typeof getOnboardingApiV1BoardsBoardIdOnboardingGet>>
>;
export type GetOnboardingApiV1BoardsBoardIdOnboardingGetQueryError =
HTTPValidationError;
export function useGetOnboardingApiV1BoardsBoardIdOnboardingGet<
TData = Awaited<
ReturnType<typeof getOnboardingApiV1BoardsBoardIdOnboardingGet>
>,
TError = HTTPValidationError,
>(
boardId: string,
options: {
query: Partial<
UseQueryOptions<
Awaited<
ReturnType<typeof getOnboardingApiV1BoardsBoardIdOnboardingGet>
>,
TError,
TData
>
> &
Pick<
DefinedInitialDataOptions<
Awaited<
ReturnType<typeof getOnboardingApiV1BoardsBoardIdOnboardingGet>
>,
TError,
Awaited<
ReturnType<typeof getOnboardingApiV1BoardsBoardIdOnboardingGet>
>
>,
"initialData"
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): DefinedUseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
export function useGetOnboardingApiV1BoardsBoardIdOnboardingGet<
TData = Awaited<
ReturnType<typeof getOnboardingApiV1BoardsBoardIdOnboardingGet>
>,
TError = HTTPValidationError,
>(
boardId: string,
options?: {
query?: Partial<
UseQueryOptions<
Awaited<
ReturnType<typeof getOnboardingApiV1BoardsBoardIdOnboardingGet>
>,
TError,
TData
>
> &
Pick<
UndefinedInitialDataOptions<
Awaited<
ReturnType<typeof getOnboardingApiV1BoardsBoardIdOnboardingGet>
>,
TError,
Awaited<
ReturnType<typeof getOnboardingApiV1BoardsBoardIdOnboardingGet>
>
>,
"initialData"
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
export function useGetOnboardingApiV1BoardsBoardIdOnboardingGet<
TData = Awaited<
ReturnType<typeof getOnboardingApiV1BoardsBoardIdOnboardingGet>
>,
TError = HTTPValidationError,
>(
boardId: string,
options?: {
query?: Partial<
UseQueryOptions<
Awaited<
ReturnType<typeof getOnboardingApiV1BoardsBoardIdOnboardingGet>
>,
TError,
TData
>
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
/**
* @summary Get Onboarding
*/
export function useGetOnboardingApiV1BoardsBoardIdOnboardingGet<
TData = Awaited<
ReturnType<typeof getOnboardingApiV1BoardsBoardIdOnboardingGet>
>,
TError = HTTPValidationError,
>(
boardId: string,
options?: {
query?: Partial<
UseQueryOptions<
Awaited<
ReturnType<typeof getOnboardingApiV1BoardsBoardIdOnboardingGet>
>,
TError,
TData
>
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
} {
const queryOptions =
getGetOnboardingApiV1BoardsBoardIdOnboardingGetQueryOptions(
boardId,
options,
);
const query = useQuery(queryOptions, queryClient) as UseQueryResult<
TData,
TError
> & { queryKey: DataTag<QueryKey, TData, TError> };
return { ...query, queryKey: queryOptions.queryKey };
}
/**
* @summary Start Onboarding
*/
export type startOnboardingApiV1BoardsBoardIdOnboardingStartPostResponse200 = {
data: BoardOnboardingRead;
status: 200;
};
export type startOnboardingApiV1BoardsBoardIdOnboardingStartPostResponse422 = {
data: HTTPValidationError;
status: 422;
};
export type startOnboardingApiV1BoardsBoardIdOnboardingStartPostResponseSuccess =
startOnboardingApiV1BoardsBoardIdOnboardingStartPostResponse200 & {
headers: Headers;
};
export type startOnboardingApiV1BoardsBoardIdOnboardingStartPostResponseError =
startOnboardingApiV1BoardsBoardIdOnboardingStartPostResponse422 & {
headers: Headers;
};
export type startOnboardingApiV1BoardsBoardIdOnboardingStartPostResponse =
| startOnboardingApiV1BoardsBoardIdOnboardingStartPostResponseSuccess
| startOnboardingApiV1BoardsBoardIdOnboardingStartPostResponseError;
export const getStartOnboardingApiV1BoardsBoardIdOnboardingStartPostUrl = (
boardId: string,
) => {
return `/api/v1/boards/${boardId}/onboarding/start`;
};
export const startOnboardingApiV1BoardsBoardIdOnboardingStartPost = async (
boardId: string,
boardOnboardingStart: BoardOnboardingStart,
options?: RequestInit,
): Promise<startOnboardingApiV1BoardsBoardIdOnboardingStartPostResponse> => {
return customFetch<startOnboardingApiV1BoardsBoardIdOnboardingStartPostResponse>(
getStartOnboardingApiV1BoardsBoardIdOnboardingStartPostUrl(boardId),
{
...options,
method: "POST",
headers: { "Content-Type": "application/json", ...options?.headers },
body: JSON.stringify(boardOnboardingStart),
},
);
};
export const getStartOnboardingApiV1BoardsBoardIdOnboardingStartPostMutationOptions =
<TError = HTTPValidationError, TContext = unknown>(options?: {
mutation?: UseMutationOptions<
Awaited<
ReturnType<typeof startOnboardingApiV1BoardsBoardIdOnboardingStartPost>
>,
TError,
{ boardId: string; data: BoardOnboardingStart },
TContext
>;
request?: SecondParameter<typeof customFetch>;
}): UseMutationOptions<
Awaited<
ReturnType<typeof startOnboardingApiV1BoardsBoardIdOnboardingStartPost>
>,
TError,
{ boardId: string; data: BoardOnboardingStart },
TContext
> => {
const mutationKey = [
"startOnboardingApiV1BoardsBoardIdOnboardingStartPost",
];
const { mutation: mutationOptions, request: requestOptions } = options
? options.mutation &&
"mutationKey" in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey }, request: undefined };
const mutationFn: MutationFunction<
Awaited<
ReturnType<typeof startOnboardingApiV1BoardsBoardIdOnboardingStartPost>
>,
{ boardId: string; data: BoardOnboardingStart }
> = (props) => {
const { boardId, data } = props ?? {};
return startOnboardingApiV1BoardsBoardIdOnboardingStartPost(
boardId,
data,
requestOptions,
);
};
return { mutationFn, ...mutationOptions };
};
export type StartOnboardingApiV1BoardsBoardIdOnboardingStartPostMutationResult =
NonNullable<
Awaited<
ReturnType<typeof startOnboardingApiV1BoardsBoardIdOnboardingStartPost>
>
>;
export type StartOnboardingApiV1BoardsBoardIdOnboardingStartPostMutationBody =
BoardOnboardingStart;
export type StartOnboardingApiV1BoardsBoardIdOnboardingStartPostMutationError =
HTTPValidationError;
/**
* @summary Start Onboarding
*/
export const useStartOnboardingApiV1BoardsBoardIdOnboardingStartPost = <
TError = HTTPValidationError,
TContext = unknown,
>(
options?: {
mutation?: UseMutationOptions<
Awaited<
ReturnType<typeof startOnboardingApiV1BoardsBoardIdOnboardingStartPost>
>,
TError,
{ boardId: string; data: BoardOnboardingStart },
TContext
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseMutationResult<
Awaited<
ReturnType<typeof startOnboardingApiV1BoardsBoardIdOnboardingStartPost>
>,
TError,
{ boardId: string; data: BoardOnboardingStart },
TContext
> => {
return useMutation(
getStartOnboardingApiV1BoardsBoardIdOnboardingStartPostMutationOptions(
options,
),
queryClient,
);
};
/**
* @summary Answer Onboarding
*/
export type answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPostResponse200 =
{
data: BoardOnboardingRead;
status: 200;
};
export type answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPostResponse422 =
{
data: HTTPValidationError;
status: 422;
};
export type answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPostResponseSuccess =
answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPostResponse200 & {
headers: Headers;
};
export type answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPostResponseError =
answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPostResponse422 & {
headers: Headers;
};
export type answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPostResponse =
| answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPostResponseSuccess
| answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPostResponseError;
export const getAnswerOnboardingApiV1BoardsBoardIdOnboardingAnswerPostUrl = (
boardId: string,
) => {
return `/api/v1/boards/${boardId}/onboarding/answer`;
};
export const answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPost = async (
boardId: string,
boardOnboardingAnswer: BoardOnboardingAnswer,
options?: RequestInit,
): Promise<answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPostResponse> => {
return customFetch<answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPostResponse>(
getAnswerOnboardingApiV1BoardsBoardIdOnboardingAnswerPostUrl(boardId),
{
...options,
method: "POST",
headers: { "Content-Type": "application/json", ...options?.headers },
body: JSON.stringify(boardOnboardingAnswer),
},
);
};
export const getAnswerOnboardingApiV1BoardsBoardIdOnboardingAnswerPostMutationOptions =
<TError = HTTPValidationError, TContext = unknown>(options?: {
mutation?: UseMutationOptions<
Awaited<
ReturnType<
typeof answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPost
>
>,
TError,
{ boardId: string; data: BoardOnboardingAnswer },
TContext
>;
request?: SecondParameter<typeof customFetch>;
}): UseMutationOptions<
Awaited<
ReturnType<typeof answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPost>
>,
TError,
{ boardId: string; data: BoardOnboardingAnswer },
TContext
> => {
const mutationKey = [
"answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPost",
];
const { mutation: mutationOptions, request: requestOptions } = options
? options.mutation &&
"mutationKey" in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey }, request: undefined };
const mutationFn: MutationFunction<
Awaited<
ReturnType<
typeof answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPost
>
>,
{ boardId: string; data: BoardOnboardingAnswer }
> = (props) => {
const { boardId, data } = props ?? {};
return answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPost(
boardId,
data,
requestOptions,
);
};
return { mutationFn, ...mutationOptions };
};
export type AnswerOnboardingApiV1BoardsBoardIdOnboardingAnswerPostMutationResult =
NonNullable<
Awaited<
ReturnType<typeof answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPost>
>
>;
export type AnswerOnboardingApiV1BoardsBoardIdOnboardingAnswerPostMutationBody =
BoardOnboardingAnswer;
export type AnswerOnboardingApiV1BoardsBoardIdOnboardingAnswerPostMutationError =
HTTPValidationError;
/**
* @summary Answer Onboarding
*/
export const useAnswerOnboardingApiV1BoardsBoardIdOnboardingAnswerPost = <
TError = HTTPValidationError,
TContext = unknown,
>(
options?: {
mutation?: UseMutationOptions<
Awaited<
ReturnType<
typeof answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPost
>
>,
TError,
{ boardId: string; data: BoardOnboardingAnswer },
TContext
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseMutationResult<
Awaited<
ReturnType<typeof answerOnboardingApiV1BoardsBoardIdOnboardingAnswerPost>
>,
TError,
{ boardId: string; data: BoardOnboardingAnswer },
TContext
> => {
return useMutation(
getAnswerOnboardingApiV1BoardsBoardIdOnboardingAnswerPostMutationOptions(
options,
),
queryClient,
);
};
/**
* @summary Agent Onboarding Update
*/
export type agentOnboardingUpdateApiV1BoardsBoardIdOnboardingAgentPostResponse200 =
{
data: BoardOnboardingRead;
status: 200;
};
export type agentOnboardingUpdateApiV1BoardsBoardIdOnboardingAgentPostResponse422 =
{
data: HTTPValidationError;
status: 422;
};
export type agentOnboardingUpdateApiV1BoardsBoardIdOnboardingAgentPostResponseSuccess =
agentOnboardingUpdateApiV1BoardsBoardIdOnboardingAgentPostResponse200 & {
headers: Headers;
};
export type agentOnboardingUpdateApiV1BoardsBoardIdOnboardingAgentPostResponseError =
agentOnboardingUpdateApiV1BoardsBoardIdOnboardingAgentPostResponse422 & {
headers: Headers;
};
export type agentOnboardingUpdateApiV1BoardsBoardIdOnboardingAgentPostResponse =
| agentOnboardingUpdateApiV1BoardsBoardIdOnboardingAgentPostResponseSuccess
| agentOnboardingUpdateApiV1BoardsBoardIdOnboardingAgentPostResponseError;
export const getAgentOnboardingUpdateApiV1BoardsBoardIdOnboardingAgentPostUrl =
(boardId: string) => {
return `/api/v1/boards/${boardId}/onboarding/agent`;
};
export const agentOnboardingUpdateApiV1BoardsBoardIdOnboardingAgentPost =
async (
boardId: string,
boardOnboardingAgentCompleteBoardOnboardingAgentQuestion:
| BoardOnboardingAgentComplete
| BoardOnboardingAgentQuestion,
options?: RequestInit,
): Promise<agentOnboardingUpdateApiV1BoardsBoardIdOnboardingAgentPostResponse> => {
return customFetch<agentOnboardingUpdateApiV1BoardsBoardIdOnboardingAgentPostResponse>(
getAgentOnboardingUpdateApiV1BoardsBoardIdOnboardingAgentPostUrl(boardId),
{
...options,
method: "POST",
headers: { "Content-Type": "application/json", ...options?.headers },
body: JSON.stringify(
boardOnboardingAgentCompleteBoardOnboardingAgentQuestion,
),
},
);
};
export const getAgentOnboardingUpdateApiV1BoardsBoardIdOnboardingAgentPostMutationOptions =
<TError = HTTPValidationError, TContext = unknown>(options?: {
mutation?: UseMutationOptions<
Awaited<
ReturnType<
typeof agentOnboardingUpdateApiV1BoardsBoardIdOnboardingAgentPost
>
>,
TError,
{
boardId: string;
data: BoardOnboardingAgentComplete | BoardOnboardingAgentQuestion;
},
TContext
>;
request?: SecondParameter<typeof customFetch>;
}): UseMutationOptions<
Awaited<
ReturnType<
typeof agentOnboardingUpdateApiV1BoardsBoardIdOnboardingAgentPost
>
>,
TError,
{
boardId: string;
data: BoardOnboardingAgentComplete | BoardOnboardingAgentQuestion;
},
TContext
> => {
const mutationKey = [
"agentOnboardingUpdateApiV1BoardsBoardIdOnboardingAgentPost",
];
const { mutation: mutationOptions, request: requestOptions } = options
? options.mutation &&
"mutationKey" in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey }, request: undefined };
const mutationFn: MutationFunction<
Awaited<
ReturnType<
typeof agentOnboardingUpdateApiV1BoardsBoardIdOnboardingAgentPost
>
>,
{
boardId: string;
data: BoardOnboardingAgentComplete | BoardOnboardingAgentQuestion;
}
> = (props) => {
const { boardId, data } = props ?? {};
return agentOnboardingUpdateApiV1BoardsBoardIdOnboardingAgentPost(
boardId,
data,
requestOptions,
);
};
return { mutationFn, ...mutationOptions };
};
export type AgentOnboardingUpdateApiV1BoardsBoardIdOnboardingAgentPostMutationResult =
NonNullable<
Awaited<
ReturnType<
typeof agentOnboardingUpdateApiV1BoardsBoardIdOnboardingAgentPost
>
>
>;
export type AgentOnboardingUpdateApiV1BoardsBoardIdOnboardingAgentPostMutationBody =
BoardOnboardingAgentComplete | BoardOnboardingAgentQuestion;
export type AgentOnboardingUpdateApiV1BoardsBoardIdOnboardingAgentPostMutationError =
HTTPValidationError;
/**
* @summary Agent Onboarding Update
*/
export const useAgentOnboardingUpdateApiV1BoardsBoardIdOnboardingAgentPost = <
TError = HTTPValidationError,
TContext = unknown,
>(
options?: {
mutation?: UseMutationOptions<
Awaited<
ReturnType<
typeof agentOnboardingUpdateApiV1BoardsBoardIdOnboardingAgentPost
>
>,
TError,
{
boardId: string;
data: BoardOnboardingAgentComplete | BoardOnboardingAgentQuestion;
},
TContext
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseMutationResult<
Awaited<
ReturnType<
typeof agentOnboardingUpdateApiV1BoardsBoardIdOnboardingAgentPost
>
>,
TError,
{
boardId: string;
data: BoardOnboardingAgentComplete | BoardOnboardingAgentQuestion;
},
TContext
> => {
return useMutation(
getAgentOnboardingUpdateApiV1BoardsBoardIdOnboardingAgentPostMutationOptions(
options,
),
queryClient,
);
};
/**
* @summary Confirm Onboarding
*/
export type confirmOnboardingApiV1BoardsBoardIdOnboardingConfirmPostResponse200 =
{
data: BoardRead;
status: 200;
};
export type confirmOnboardingApiV1BoardsBoardIdOnboardingConfirmPostResponse422 =
{
data: HTTPValidationError;
status: 422;
};
export type confirmOnboardingApiV1BoardsBoardIdOnboardingConfirmPostResponseSuccess =
confirmOnboardingApiV1BoardsBoardIdOnboardingConfirmPostResponse200 & {
headers: Headers;
};
export type confirmOnboardingApiV1BoardsBoardIdOnboardingConfirmPostResponseError =
confirmOnboardingApiV1BoardsBoardIdOnboardingConfirmPostResponse422 & {
headers: Headers;
};
export type confirmOnboardingApiV1BoardsBoardIdOnboardingConfirmPostResponse =
| confirmOnboardingApiV1BoardsBoardIdOnboardingConfirmPostResponseSuccess
| confirmOnboardingApiV1BoardsBoardIdOnboardingConfirmPostResponseError;
export const getConfirmOnboardingApiV1BoardsBoardIdOnboardingConfirmPostUrl = (
boardId: string,
) => {
return `/api/v1/boards/${boardId}/onboarding/confirm`;
};
export const confirmOnboardingApiV1BoardsBoardIdOnboardingConfirmPost = async (
boardId: string,
boardOnboardingConfirm: BoardOnboardingConfirm,
options?: RequestInit,
): Promise<confirmOnboardingApiV1BoardsBoardIdOnboardingConfirmPostResponse> => {
return customFetch<confirmOnboardingApiV1BoardsBoardIdOnboardingConfirmPostResponse>(
getConfirmOnboardingApiV1BoardsBoardIdOnboardingConfirmPostUrl(boardId),
{
...options,
method: "POST",
headers: { "Content-Type": "application/json", ...options?.headers },
body: JSON.stringify(boardOnboardingConfirm),
},
);
};
export const getConfirmOnboardingApiV1BoardsBoardIdOnboardingConfirmPostMutationOptions =
<TError = HTTPValidationError, TContext = unknown>(options?: {
mutation?: UseMutationOptions<
Awaited<
ReturnType<
typeof confirmOnboardingApiV1BoardsBoardIdOnboardingConfirmPost
>
>,
TError,
{ boardId: string; data: BoardOnboardingConfirm },
TContext
>;
request?: SecondParameter<typeof customFetch>;
}): UseMutationOptions<
Awaited<
ReturnType<
typeof confirmOnboardingApiV1BoardsBoardIdOnboardingConfirmPost
>
>,
TError,
{ boardId: string; data: BoardOnboardingConfirm },
TContext
> => {
const mutationKey = [
"confirmOnboardingApiV1BoardsBoardIdOnboardingConfirmPost",
];
const { mutation: mutationOptions, request: requestOptions } = options
? options.mutation &&
"mutationKey" in options.mutation &&
options.mutation.mutationKey
? options
: { ...options, mutation: { ...options.mutation, mutationKey } }
: { mutation: { mutationKey }, request: undefined };
const mutationFn: MutationFunction<
Awaited<
ReturnType<
typeof confirmOnboardingApiV1BoardsBoardIdOnboardingConfirmPost
>
>,
{ boardId: string; data: BoardOnboardingConfirm }
> = (props) => {
const { boardId, data } = props ?? {};
return confirmOnboardingApiV1BoardsBoardIdOnboardingConfirmPost(
boardId,
data,
requestOptions,
);
};
return { mutationFn, ...mutationOptions };
};
export type ConfirmOnboardingApiV1BoardsBoardIdOnboardingConfirmPostMutationResult =
NonNullable<
Awaited<
ReturnType<
typeof confirmOnboardingApiV1BoardsBoardIdOnboardingConfirmPost
>
>
>;
export type ConfirmOnboardingApiV1BoardsBoardIdOnboardingConfirmPostMutationBody =
BoardOnboardingConfirm;
export type ConfirmOnboardingApiV1BoardsBoardIdOnboardingConfirmPostMutationError =
HTTPValidationError;
/**
* @summary Confirm Onboarding
*/
export const useConfirmOnboardingApiV1BoardsBoardIdOnboardingConfirmPost = <
TError = HTTPValidationError,
TContext = unknown,
>(
options?: {
mutation?: UseMutationOptions<
Awaited<
ReturnType<
typeof confirmOnboardingApiV1BoardsBoardIdOnboardingConfirmPost
>
>,
TError,
{ boardId: string; data: BoardOnboardingConfirm },
TContext
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseMutationResult<
Awaited<
ReturnType<typeof confirmOnboardingApiV1BoardsBoardIdOnboardingConfirmPost>
>,
TError,
{ boardId: string; data: BoardOnboardingConfirm },
TContext
> => {
return useMutation(
getConfirmOnboardingApiV1BoardsBoardIdOnboardingConfirmPostMutationOptions(
options,
),
queryClient,
);
};

View File

@@ -24,8 +24,8 @@ import type {
BoardCreate, BoardCreate,
BoardRead, BoardRead,
BoardUpdate, BoardUpdate,
DeleteBoardApiV1BoardsBoardIdDelete200,
HTTPValidationError, HTTPValidationError,
OkResponse,
} from ".././model"; } from ".././model";
import { customFetch } from "../../mutator"; import { customFetch } from "../../mutator";
@@ -653,7 +653,7 @@ export const useUpdateBoardApiV1BoardsBoardIdPatch = <
* @summary Delete Board * @summary Delete Board
*/ */
export type deleteBoardApiV1BoardsBoardIdDeleteResponse200 = { export type deleteBoardApiV1BoardsBoardIdDeleteResponse200 = {
data: DeleteBoardApiV1BoardsBoardIdDelete200; data: OkResponse;
status: 200; status: 200;
}; };

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,243 @@
/**
* Generated by orval v8.2.0 🍺
* Do not edit manually.
* Mission Control API
* OpenAPI spec version: 0.1.0
*/
import { useQuery } from "@tanstack/react-query";
import type {
DataTag,
DefinedInitialDataOptions,
DefinedUseQueryResult,
QueryClient,
QueryFunction,
QueryKey,
UndefinedInitialDataOptions,
UseQueryOptions,
UseQueryResult,
} from "@tanstack/react-query";
import type {
DashboardMetrics,
DashboardMetricsApiV1MetricsDashboardGetParams,
HTTPValidationError,
} from ".././model";
import { customFetch } from "../../mutator";
type SecondParameter<T extends (...args: never) => unknown> = Parameters<T>[1];
/**
* @summary Dashboard Metrics
*/
export type dashboardMetricsApiV1MetricsDashboardGetResponse200 = {
data: DashboardMetrics;
status: 200;
};
export type dashboardMetricsApiV1MetricsDashboardGetResponse422 = {
data: HTTPValidationError;
status: 422;
};
export type dashboardMetricsApiV1MetricsDashboardGetResponseSuccess =
dashboardMetricsApiV1MetricsDashboardGetResponse200 & {
headers: Headers;
};
export type dashboardMetricsApiV1MetricsDashboardGetResponseError =
dashboardMetricsApiV1MetricsDashboardGetResponse422 & {
headers: Headers;
};
export type dashboardMetricsApiV1MetricsDashboardGetResponse =
| dashboardMetricsApiV1MetricsDashboardGetResponseSuccess
| dashboardMetricsApiV1MetricsDashboardGetResponseError;
export const getDashboardMetricsApiV1MetricsDashboardGetUrl = (
params?: DashboardMetricsApiV1MetricsDashboardGetParams,
) => {
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/metrics/dashboard?${stringifiedParams}`
: `/api/v1/metrics/dashboard`;
};
export const dashboardMetricsApiV1MetricsDashboardGet = async (
params?: DashboardMetricsApiV1MetricsDashboardGetParams,
options?: RequestInit,
): Promise<dashboardMetricsApiV1MetricsDashboardGetResponse> => {
return customFetch<dashboardMetricsApiV1MetricsDashboardGetResponse>(
getDashboardMetricsApiV1MetricsDashboardGetUrl(params),
{
...options,
method: "GET",
},
);
};
export const getDashboardMetricsApiV1MetricsDashboardGetQueryKey = (
params?: DashboardMetricsApiV1MetricsDashboardGetParams,
) => {
return [`/api/v1/metrics/dashboard`, ...(params ? [params] : [])] as const;
};
export const getDashboardMetricsApiV1MetricsDashboardGetQueryOptions = <
TData = Awaited<ReturnType<typeof dashboardMetricsApiV1MetricsDashboardGet>>,
TError = HTTPValidationError,
>(
params?: DashboardMetricsApiV1MetricsDashboardGetParams,
options?: {
query?: Partial<
UseQueryOptions<
Awaited<ReturnType<typeof dashboardMetricsApiV1MetricsDashboardGet>>,
TError,
TData
>
>;
request?: SecondParameter<typeof customFetch>;
},
) => {
const { query: queryOptions, request: requestOptions } = options ?? {};
const queryKey =
queryOptions?.queryKey ??
getDashboardMetricsApiV1MetricsDashboardGetQueryKey(params);
const queryFn: QueryFunction<
Awaited<ReturnType<typeof dashboardMetricsApiV1MetricsDashboardGet>>
> = ({ signal }) =>
dashboardMetricsApiV1MetricsDashboardGet(params, {
signal,
...requestOptions,
});
return { queryKey, queryFn, ...queryOptions } as UseQueryOptions<
Awaited<ReturnType<typeof dashboardMetricsApiV1MetricsDashboardGet>>,
TError,
TData
> & { queryKey: DataTag<QueryKey, TData, TError> };
};
export type DashboardMetricsApiV1MetricsDashboardGetQueryResult = NonNullable<
Awaited<ReturnType<typeof dashboardMetricsApiV1MetricsDashboardGet>>
>;
export type DashboardMetricsApiV1MetricsDashboardGetQueryError =
HTTPValidationError;
export function useDashboardMetricsApiV1MetricsDashboardGet<
TData = Awaited<ReturnType<typeof dashboardMetricsApiV1MetricsDashboardGet>>,
TError = HTTPValidationError,
>(
params: undefined | DashboardMetricsApiV1MetricsDashboardGetParams,
options: {
query: Partial<
UseQueryOptions<
Awaited<ReturnType<typeof dashboardMetricsApiV1MetricsDashboardGet>>,
TError,
TData
>
> &
Pick<
DefinedInitialDataOptions<
Awaited<ReturnType<typeof dashboardMetricsApiV1MetricsDashboardGet>>,
TError,
Awaited<ReturnType<typeof dashboardMetricsApiV1MetricsDashboardGet>>
>,
"initialData"
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): DefinedUseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
export function useDashboardMetricsApiV1MetricsDashboardGet<
TData = Awaited<ReturnType<typeof dashboardMetricsApiV1MetricsDashboardGet>>,
TError = HTTPValidationError,
>(
params?: DashboardMetricsApiV1MetricsDashboardGetParams,
options?: {
query?: Partial<
UseQueryOptions<
Awaited<ReturnType<typeof dashboardMetricsApiV1MetricsDashboardGet>>,
TError,
TData
>
> &
Pick<
UndefinedInitialDataOptions<
Awaited<ReturnType<typeof dashboardMetricsApiV1MetricsDashboardGet>>,
TError,
Awaited<ReturnType<typeof dashboardMetricsApiV1MetricsDashboardGet>>
>,
"initialData"
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
export function useDashboardMetricsApiV1MetricsDashboardGet<
TData = Awaited<ReturnType<typeof dashboardMetricsApiV1MetricsDashboardGet>>,
TError = HTTPValidationError,
>(
params?: DashboardMetricsApiV1MetricsDashboardGetParams,
options?: {
query?: Partial<
UseQueryOptions<
Awaited<ReturnType<typeof dashboardMetricsApiV1MetricsDashboardGet>>,
TError,
TData
>
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
};
/**
* @summary Dashboard Metrics
*/
export function useDashboardMetricsApiV1MetricsDashboardGet<
TData = Awaited<ReturnType<typeof dashboardMetricsApiV1MetricsDashboardGet>>,
TError = HTTPValidationError,
>(
params?: DashboardMetricsApiV1MetricsDashboardGetParams,
options?: {
query?: Partial<
UseQueryOptions<
Awaited<ReturnType<typeof dashboardMetricsApiV1MetricsDashboardGet>>,
TError,
TData
>
>;
request?: SecondParameter<typeof customFetch>;
},
queryClient?: QueryClient,
): UseQueryResult<TData, TError> & {
queryKey: DataTag<QueryKey, TData, TError>;
} {
const queryOptions = getDashboardMetricsApiV1MetricsDashboardGetQueryOptions(
params,
options,
);
const query = useQuery(queryOptions, queryClient) as UseQueryResult<
TData,
TError
> & { queryKey: DataTag<QueryKey, TData, TError> };
return { ...query, queryKey: queryOptions.queryKey };
}

View File

@@ -5,10 +5,15 @@
* OpenAPI spec version: 0.1.0 * OpenAPI spec version: 0.1.0
*/ */
import type { AgentCreateHeartbeatConfig } from "./agentCreateHeartbeatConfig"; import type { AgentCreateHeartbeatConfig } from "./agentCreateHeartbeatConfig";
import type { AgentCreateIdentityProfile } from "./agentCreateIdentityProfile";
export interface AgentCreate { export interface AgentCreate {
board_id?: string | null; board_id?: string | null;
/** @minLength 1 */
name: string; name: string;
status?: string; status?: string;
heartbeat_config?: AgentCreateHeartbeatConfig; heartbeat_config?: AgentCreateHeartbeatConfig;
identity_profile?: AgentCreateIdentityProfile;
identity_template?: string | null;
soul_template?: string | null;
} }

View File

@@ -0,0 +1,8 @@
/**
* Generated by orval v8.2.0 🍺
* Do not edit manually.
* Mission Control API
* OpenAPI spec version: 0.1.0
*/
export type AgentCreateIdentityProfile = { [key: string]: unknown } | null;

View File

@@ -7,6 +7,7 @@
export interface AgentHeartbeatCreate { export interface AgentHeartbeatCreate {
status?: string | null; status?: string | null;
/** @minLength 1 */
name: string; name: string;
board_id?: string | null; board_id?: string | null;
} }

View File

@@ -0,0 +1,11 @@
/**
* Generated by orval v8.2.0 🍺
* Do not edit manually.
* Mission Control API
* OpenAPI spec version: 0.1.0
*/
export interface AgentNudge {
/** @minLength 1 */
message: string;
}

View File

@@ -5,13 +5,20 @@
* OpenAPI spec version: 0.1.0 * OpenAPI spec version: 0.1.0
*/ */
import type { AgentReadHeartbeatConfig } from "./agentReadHeartbeatConfig"; import type { AgentReadHeartbeatConfig } from "./agentReadHeartbeatConfig";
import type { AgentReadIdentityProfile } from "./agentReadIdentityProfile";
export interface AgentRead { export interface AgentRead {
board_id?: string | null; board_id?: string | null;
/** @minLength 1 */
name: string; name: string;
status?: string; status?: string;
heartbeat_config?: AgentReadHeartbeatConfig; heartbeat_config?: AgentReadHeartbeatConfig;
identity_profile?: AgentReadIdentityProfile;
identity_template?: string | null;
soul_template?: string | null;
id: string; id: string;
is_board_lead?: boolean;
is_gateway_main?: boolean;
openclaw_session_id?: string | null; openclaw_session_id?: string | null;
last_seen_at: string | null; last_seen_at: string | null;
created_at: string; created_at: string;

View File

@@ -0,0 +1,8 @@
/**
* Generated by orval v8.2.0 🍺
* Do not edit manually.
* Mission Control API
* OpenAPI spec version: 0.1.0
*/
export type AgentReadIdentityProfile = { [key: string]: unknown } | null;

View File

@@ -5,10 +5,15 @@
* OpenAPI spec version: 0.1.0 * OpenAPI spec version: 0.1.0
*/ */
import type { AgentUpdateHeartbeatConfig } from "./agentUpdateHeartbeatConfig"; import type { AgentUpdateHeartbeatConfig } from "./agentUpdateHeartbeatConfig";
import type { AgentUpdateIdentityProfile } from "./agentUpdateIdentityProfile";
export interface AgentUpdate { export interface AgentUpdate {
board_id?: string | null; board_id?: string | null;
is_gateway_main?: boolean | null;
name?: string | null; name?: string | null;
status?: string | null; status?: string | null;
heartbeat_config?: AgentUpdateHeartbeatConfig; heartbeat_config?: AgentUpdateHeartbeatConfig;
identity_profile?: AgentUpdateIdentityProfile;
identity_template?: string | null;
soul_template?: string | null;
} }

View File

@@ -0,0 +1,8 @@
/**
* Generated by orval v8.2.0 🍺
* Do not edit manually.
* Mission Control API
* OpenAPI spec version: 0.1.0
*/
export type AgentUpdateIdentityProfile = { [key: string]: unknown } | null;

View File

@@ -0,0 +1,18 @@
/**
* Generated by orval v8.2.0 🍺
* Do not edit manually.
* Mission Control API
* OpenAPI spec version: 0.1.0
*/
import type { ApprovalCreatePayload } from "./approvalCreatePayload";
import type { ApprovalCreateRubricScores } from "./approvalCreateRubricScores";
import type { ApprovalCreateStatus } from "./approvalCreateStatus";
export interface ApprovalCreate {
action_type: string;
payload?: ApprovalCreatePayload;
confidence: number;
rubric_scores?: ApprovalCreateRubricScores;
status?: ApprovalCreateStatus;
agent_id?: string | null;
}

View File

@@ -0,0 +1,8 @@
/**
* Generated by orval v8.2.0 🍺
* Do not edit manually.
* Mission Control API
* OpenAPI spec version: 0.1.0
*/
export type ApprovalCreatePayload = { [key: string]: unknown } | null;

View File

@@ -0,0 +1,8 @@
/**
* Generated by orval v8.2.0 🍺
* Do not edit manually.
* Mission Control API
* OpenAPI spec version: 0.1.0
*/
export type ApprovalCreateRubricScores = { [key: string]: number } | null;

View File

@@ -0,0 +1,15 @@
/**
* Generated by orval v8.2.0 🍺
* Do not edit manually.
* Mission Control API
* OpenAPI spec version: 0.1.0
*/
export type ApprovalCreateStatus =
(typeof ApprovalCreateStatus)[keyof typeof ApprovalCreateStatus];
export const ApprovalCreateStatus = {
pending: "pending",
approved: "approved",
rejected: "rejected",
} as const;

View File

@@ -0,0 +1,22 @@
/**
* Generated by orval v8.2.0 🍺
* Do not edit manually.
* Mission Control API
* OpenAPI spec version: 0.1.0
*/
import type { ApprovalReadPayload } from "./approvalReadPayload";
import type { ApprovalReadRubricScores } from "./approvalReadRubricScores";
import type { ApprovalReadStatus } from "./approvalReadStatus";
export interface ApprovalRead {
action_type: string;
payload?: ApprovalReadPayload;
confidence: number;
rubric_scores?: ApprovalReadRubricScores;
status?: ApprovalReadStatus;
id: string;
board_id: string;
agent_id?: string | null;
created_at: string;
resolved_at?: string | null;
}

View File

@@ -0,0 +1,8 @@
/**
* Generated by orval v8.2.0 🍺
* Do not edit manually.
* Mission Control API
* OpenAPI spec version: 0.1.0
*/
export type ApprovalReadPayload = { [key: string]: unknown } | null;

View File

@@ -0,0 +1,8 @@
/**
* Generated by orval v8.2.0 🍺
* Do not edit manually.
* Mission Control API
* OpenAPI spec version: 0.1.0
*/
export type ApprovalReadRubricScores = { [key: string]: number } | null;

View File

@@ -0,0 +1,15 @@
/**
* Generated by orval v8.2.0 🍺
* Do not edit manually.
* Mission Control API
* OpenAPI spec version: 0.1.0
*/
export type ApprovalReadStatus =
(typeof ApprovalReadStatus)[keyof typeof ApprovalReadStatus];
export const ApprovalReadStatus = {
pending: "pending",
approved: "approved",
rejected: "rejected",
} as const;

View File

@@ -0,0 +1,10 @@
/**
* Generated by orval v8.2.0 🍺
* Do not edit manually.
* Mission Control API
* OpenAPI spec version: 0.1.0
*/
export interface ApprovalUpdate {
status?: "pending" | "approved" | "rejected" | null;
}

View File

@@ -4,9 +4,16 @@
* Mission Control API * Mission Control API
* OpenAPI spec version: 0.1.0 * OpenAPI spec version: 0.1.0
*/ */
import type { BoardCreateSuccessMetrics } from "./boardCreateSuccessMetrics";
export interface BoardCreate { export interface BoardCreate {
name: string; name: string;
slug: string; slug: string;
gateway_id?: string | null; gateway_id: string;
board_type?: string;
objective?: string | null;
success_metrics?: BoardCreateSuccessMetrics;
target_date?: string | null;
goal_confirmed?: boolean;
goal_source?: string | null;
} }

View File

@@ -0,0 +1,8 @@
/**
* Generated by orval v8.2.0 🍺
* Do not edit manually.
* Mission Control API
* OpenAPI spec version: 0.1.0
*/
export type BoardCreateSuccessMetrics = { [key: string]: unknown } | null;

View File

@@ -0,0 +1,13 @@
/**
* Generated by orval v8.2.0 🍺
* Do not edit manually.
* Mission Control API
* OpenAPI spec version: 0.1.0
*/
export interface BoardMemoryCreate {
/** @minLength 1 */
content: string;
tags?: string[] | null;
source?: string | null;
}

View File

@@ -0,0 +1,16 @@
/**
* Generated by orval v8.2.0 🍺
* Do not edit manually.
* Mission Control API
* OpenAPI spec version: 0.1.0
*/
export interface BoardMemoryRead {
/** @minLength 1 */
content: string;
tags?: string[] | null;
source?: string | null;
id: string;
board_id: string;
created_at: string;
}

View File

@@ -0,0 +1,15 @@
/**
* Generated by orval v8.2.0 🍺
* Do not edit manually.
* Mission Control API
* OpenAPI spec version: 0.1.0
*/
import type { BoardOnboardingAgentCompleteSuccessMetrics } from "./boardOnboardingAgentCompleteSuccessMetrics";
export interface BoardOnboardingAgentComplete {
board_type: string;
objective?: string | null;
success_metrics?: BoardOnboardingAgentCompleteSuccessMetrics;
target_date?: string | null;
status: "complete";
}

View File

@@ -0,0 +1,10 @@
/**
* Generated by orval v8.2.0 🍺
* Do not edit manually.
* Mission Control API
* OpenAPI spec version: 0.1.0
*/
export type BoardOnboardingAgentCompleteSuccessMetrics = {
[key: string]: unknown;
} | null;

View File

@@ -0,0 +1,14 @@
/**
* Generated by orval v8.2.0 🍺
* Do not edit manually.
* Mission Control API
* OpenAPI spec version: 0.1.0
*/
import type { BoardOnboardingQuestionOption } from "./boardOnboardingQuestionOption";
export interface BoardOnboardingAgentQuestion {
/** @minLength 1 */
question: string;
/** @minItems 1 */
options: BoardOnboardingQuestionOption[];
}

View File

@@ -0,0 +1,12 @@
/**
* Generated by orval v8.2.0 🍺
* Do not edit manually.
* Mission Control API
* OpenAPI spec version: 0.1.0
*/
export interface BoardOnboardingAnswer {
/** @minLength 1 */
answer: string;
other_text?: string | null;
}

View File

@@ -0,0 +1,14 @@
/**
* Generated by orval v8.2.0 🍺
* Do not edit manually.
* Mission Control API
* OpenAPI spec version: 0.1.0
*/
import type { BoardOnboardingConfirmSuccessMetrics } from "./boardOnboardingConfirmSuccessMetrics";
export interface BoardOnboardingConfirm {
board_type: string;
objective?: string | null;
success_metrics?: BoardOnboardingConfirmSuccessMetrics;
target_date?: string | null;
}

View File

@@ -0,0 +1,10 @@
/**
* Generated by orval v8.2.0 🍺
* Do not edit manually.
* Mission Control API
* OpenAPI spec version: 0.1.0
*/
export type BoardOnboardingConfirmSuccessMetrics = {
[key: string]: unknown;
} | null;

View File

@@ -0,0 +1,13 @@
/**
* Generated by orval v8.2.0 🍺
* Do not edit manually.
* Mission Control API
* OpenAPI spec version: 0.1.0
*/
export interface BoardOnboardingQuestionOption {
/** @minLength 1 */
id: string;
/** @minLength 1 */
label: string;
}

View File

@@ -0,0 +1,19 @@
/**
* Generated by orval v8.2.0 🍺
* Do not edit manually.
* Mission Control API
* OpenAPI spec version: 0.1.0
*/
import type { BoardOnboardingReadDraftGoal } from "./boardOnboardingReadDraftGoal";
import type { BoardOnboardingReadMessages } from "./boardOnboardingReadMessages";
export interface BoardOnboardingRead {
id: string;
board_id: string;
session_key: string;
status: string;
messages?: BoardOnboardingReadMessages;
draft_goal?: BoardOnboardingReadDraftGoal;
created_at: string;
updated_at: string;
}

View File

@@ -0,0 +1,8 @@
/**
* Generated by orval v8.2.0 🍺
* Do not edit manually.
* Mission Control API
* OpenAPI spec version: 0.1.0
*/
export type BoardOnboardingReadDraftGoal = { [key: string]: unknown } | null;

View File

@@ -0,0 +1,8 @@
/**
* Generated by orval v8.2.0 🍺
* Do not edit manually.
* Mission Control API
* OpenAPI spec version: 0.1.0
*/
export type BoardOnboardingReadMessages = { [key: string]: unknown }[] | null;

View File

@@ -0,0 +1,10 @@
/**
* Generated by orval v8.2.0 🍺
* Do not edit manually.
* Mission Control API
* OpenAPI spec version: 0.1.0
*/
export interface BoardOnboardingStart {
[key: string]: unknown;
}

View File

@@ -4,11 +4,18 @@
* Mission Control API * Mission Control API
* OpenAPI spec version: 0.1.0 * OpenAPI spec version: 0.1.0
*/ */
import type { BoardReadSuccessMetrics } from "./boardReadSuccessMetrics";
export interface BoardRead { export interface BoardRead {
name: string; name: string;
slug: string; slug: string;
gateway_id?: string | null; gateway_id?: string | null;
board_type?: string;
objective?: string | null;
success_metrics?: BoardReadSuccessMetrics;
target_date?: string | null;
goal_confirmed?: boolean;
goal_source?: string | null;
id: string; id: string;
created_at: string; created_at: string;
updated_at: string; updated_at: string;

View File

@@ -0,0 +1,8 @@
/**
* Generated by orval v8.2.0 🍺
* Do not edit manually.
* Mission Control API
* OpenAPI spec version: 0.1.0
*/
export type BoardReadSuccessMetrics = { [key: string]: unknown } | null;

View File

@@ -4,9 +4,16 @@
* Mission Control API * Mission Control API
* OpenAPI spec version: 0.1.0 * OpenAPI spec version: 0.1.0
*/ */
import type { BoardUpdateSuccessMetrics } from "./boardUpdateSuccessMetrics";
export interface BoardUpdate { export interface BoardUpdate {
name?: string | null; name?: string | null;
slug?: string | null; slug?: string | null;
gateway_id?: string | null; gateway_id?: string | null;
board_type?: string | null;
objective?: string | null;
success_metrics?: BoardUpdateSuccessMetrics;
target_date?: string | null;
goal_confirmed?: boolean | null;
goal_source?: string | null;
} }

View File

@@ -0,0 +1,8 @@
/**
* Generated by orval v8.2.0 🍺
* Do not edit manually.
* Mission Control API
* OpenAPI spec version: 0.1.0
*/
export type BoardUpdateSuccessMetrics = { [key: string]: unknown } | null;

View File

@@ -0,0 +1,13 @@
/**
* Generated by orval v8.2.0 🍺
* Do not edit manually.
* Mission Control API
* OpenAPI spec version: 0.1.0
*/
export interface DashboardKpis {
active_agents: number;
tasks_in_progress: number;
error_rate_pct: number;
median_cycle_time_hours_7d: number | null;
}

View File

@@ -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 { DashboardKpis } from "./dashboardKpis";
import type { DashboardMetricsRange } from "./dashboardMetricsRange";
import type { DashboardSeriesSet } from "./dashboardSeriesSet";
import type { DashboardWipSeriesSet } from "./dashboardWipSeriesSet";
export interface DashboardMetrics {
range: DashboardMetricsRange;
generated_at: string;
kpis: DashboardKpis;
throughput: DashboardSeriesSet;
cycle_time: DashboardSeriesSet;
error_rate: DashboardSeriesSet;
wip: DashboardWipSeriesSet;
}

View File

@@ -0,0 +1,11 @@
/**
* Generated by orval v8.2.0 🍺
* Do not edit manually.
* Mission Control API
* OpenAPI spec version: 0.1.0
*/
import type { DashboardMetricsApiV1MetricsDashboardGetRange } from "./dashboardMetricsApiV1MetricsDashboardGetRange";
export type DashboardMetricsApiV1MetricsDashboardGetParams = {
range?: DashboardMetricsApiV1MetricsDashboardGetRange;
};

View File

@@ -0,0 +1,14 @@
/**
* Generated by orval v8.2.0 🍺
* Do not edit manually.
* Mission Control API
* OpenAPI spec version: 0.1.0
*/
export type DashboardMetricsApiV1MetricsDashboardGetRange =
(typeof DashboardMetricsApiV1MetricsDashboardGetRange)[keyof typeof DashboardMetricsApiV1MetricsDashboardGetRange];
export const DashboardMetricsApiV1MetricsDashboardGetRange = {
"24h": "24h",
"7d": "7d",
} as const;

View File

@@ -0,0 +1,14 @@
/**
* Generated by orval v8.2.0 🍺
* Do not edit manually.
* Mission Control API
* OpenAPI spec version: 0.1.0
*/
export type DashboardMetricsRange =
(typeof DashboardMetricsRange)[keyof typeof DashboardMetricsRange];
export const DashboardMetricsRange = {
"24h": "24h",
"7d": "7d",
} as const;

View File

@@ -0,0 +1,15 @@
/**
* Generated by orval v8.2.0 🍺
* Do not edit manually.
* Mission Control API
* OpenAPI spec version: 0.1.0
*/
import type { DashboardRangeSeriesBucket } from "./dashboardRangeSeriesBucket";
import type { DashboardRangeSeriesRange } from "./dashboardRangeSeriesRange";
import type { DashboardSeriesPoint } from "./dashboardSeriesPoint";
export interface DashboardRangeSeries {
range: DashboardRangeSeriesRange;
bucket: DashboardRangeSeriesBucket;
points: DashboardSeriesPoint[];
}

View File

@@ -0,0 +1,14 @@
/**
* Generated by orval v8.2.0 🍺
* Do not edit manually.
* Mission Control API
* OpenAPI spec version: 0.1.0
*/
export type DashboardRangeSeriesBucket =
(typeof DashboardRangeSeriesBucket)[keyof typeof DashboardRangeSeriesBucket];
export const DashboardRangeSeriesBucket = {
hour: "hour",
day: "day",
} as const;

View File

@@ -0,0 +1,14 @@
/**
* Generated by orval v8.2.0 🍺
* Do not edit manually.
* Mission Control API
* OpenAPI spec version: 0.1.0
*/
export type DashboardRangeSeriesRange =
(typeof DashboardRangeSeriesRange)[keyof typeof DashboardRangeSeriesRange];
export const DashboardRangeSeriesRange = {
"24h": "24h",
"7d": "7d",
} as const;

View File

@@ -0,0 +1,11 @@
/**
* Generated by orval v8.2.0 🍺
* Do not edit manually.
* Mission Control API
* OpenAPI spec version: 0.1.0
*/
export interface DashboardSeriesPoint {
period: string;
value: number;
}

View File

@@ -0,0 +1,12 @@
/**
* Generated by orval v8.2.0 🍺
* Do not edit manually.
* Mission Control API
* OpenAPI spec version: 0.1.0
*/
import type { DashboardRangeSeries } from "./dashboardRangeSeries";
export interface DashboardSeriesSet {
primary: DashboardRangeSeries;
comparison: DashboardRangeSeries;
}

View File

@@ -0,0 +1,13 @@
/**
* Generated by orval v8.2.0 🍺
* Do not edit manually.
* Mission Control API
* OpenAPI spec version: 0.1.0
*/
export interface DashboardWipPoint {
period: string;
inbox: number;
in_progress: number;
review: number;
}

View File

@@ -0,0 +1,15 @@
/**
* Generated by orval v8.2.0 🍺
* Do not edit manually.
* Mission Control API
* OpenAPI spec version: 0.1.0
*/
import type { DashboardWipPoint } from "./dashboardWipPoint";
import type { DashboardWipRangeSeriesBucket } from "./dashboardWipRangeSeriesBucket";
import type { DashboardWipRangeSeriesRange } from "./dashboardWipRangeSeriesRange";
export interface DashboardWipRangeSeries {
range: DashboardWipRangeSeriesRange;
bucket: DashboardWipRangeSeriesBucket;
points: DashboardWipPoint[];
}

Some files were not shown because too many files have changed in this diff Show More