diff --git a/backend/app/api/board_webhooks.py b/backend/app/api/board_webhooks.py new file mode 100644 index 00000000..f9ab37ca --- /dev/null +++ b/backend/app/api/board_webhooks.py @@ -0,0 +1,451 @@ +"""Board webhook configuration and inbound payload ingestion endpoints.""" + +from __future__ import annotations + +import json +from typing import TYPE_CHECKING +from uuid import UUID + +from fastapi import APIRouter, Depends, HTTPException, Request, status +from sqlmodel import col, select + +from app.api.deps import get_board_for_user_read, get_board_for_user_write, get_board_or_404 +from app.core.config import settings +from app.core.time import utcnow +from app.db import crud +from app.db.pagination import paginate +from app.db.session import get_session +from app.models.agents import Agent +from app.models.board_memory import BoardMemory +from app.models.board_webhook_payloads import BoardWebhookPayload +from app.models.board_webhooks import BoardWebhook +from app.schemas.board_webhooks import ( + BoardWebhookCreate, + BoardWebhookIngestResponse, + BoardWebhookPayloadRead, + BoardWebhookRead, + BoardWebhookUpdate, +) +from app.schemas.common import OkResponse +from app.schemas.pagination import DefaultLimitOffsetPage +from app.services.openclaw.gateway_dispatch import GatewayDispatchService + +if TYPE_CHECKING: + from collections.abc import Sequence + + from fastapi_pagination.limit_offset import LimitOffsetPage + from sqlmodel.ext.asyncio.session import AsyncSession + + from app.models.boards import Board + +router = APIRouter(prefix="/boards/{board_id}/webhooks", tags=["board-webhooks"]) +SESSION_DEP = Depends(get_session) +BOARD_USER_READ_DEP = Depends(get_board_for_user_read) +BOARD_USER_WRITE_DEP = Depends(get_board_for_user_write) +BOARD_OR_404_DEP = Depends(get_board_or_404) +PAYLOAD_PREVIEW_MAX_CHARS = 1600 + + +def _webhook_endpoint_path(board_id: UUID, webhook_id: UUID) -> str: + return f"/api/v1/boards/{board_id}/webhooks/{webhook_id}" + + +def _webhook_endpoint_url(endpoint_path: str) -> str | None: + base_url = settings.base_url.rstrip("/") + if not base_url: + return None + return f"{base_url}{endpoint_path}" + + +def _to_webhook_read(webhook: BoardWebhook) -> BoardWebhookRead: + endpoint_path = _webhook_endpoint_path(webhook.board_id, webhook.id) + return BoardWebhookRead( + id=webhook.id, + board_id=webhook.board_id, + description=webhook.description, + enabled=webhook.enabled, + endpoint_path=endpoint_path, + endpoint_url=_webhook_endpoint_url(endpoint_path), + created_at=webhook.created_at, + updated_at=webhook.updated_at, + ) + + +def _to_payload_read(payload: BoardWebhookPayload) -> BoardWebhookPayloadRead: + return BoardWebhookPayloadRead.model_validate(payload, from_attributes=True) + + +def _coerce_webhook_items(items: Sequence[object]) -> list[BoardWebhook]: + values: list[BoardWebhook] = [] + for item in items: + if not isinstance(item, BoardWebhook): + msg = "Expected BoardWebhook items from paginated query" + raise TypeError(msg) + values.append(item) + return values + + +def _coerce_payload_items(items: Sequence[object]) -> list[BoardWebhookPayload]: + values: list[BoardWebhookPayload] = [] + for item in items: + if not isinstance(item, BoardWebhookPayload): + msg = "Expected BoardWebhookPayload items from paginated query" + raise TypeError(msg) + values.append(item) + return values + + +async def _require_board_webhook( + session: AsyncSession, + *, + board_id: UUID, + webhook_id: UUID, +) -> BoardWebhook: + webhook = ( + await session.exec( + select(BoardWebhook) + .where(col(BoardWebhook.id) == webhook_id) + .where(col(BoardWebhook.board_id) == board_id), + ) + ).first() + if webhook is None: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) + return webhook + + +async def _require_board_webhook_payload( + session: AsyncSession, + *, + board_id: UUID, + webhook_id: UUID, + payload_id: UUID, +) -> BoardWebhookPayload: + payload = ( + await session.exec( + select(BoardWebhookPayload) + .where(col(BoardWebhookPayload.id) == payload_id) + .where(col(BoardWebhookPayload.board_id) == board_id) + .where(col(BoardWebhookPayload.webhook_id) == webhook_id), + ) + ).first() + if payload is None: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) + return payload + + +def _decode_payload( + raw_body: bytes, + *, + content_type: str | None, +) -> dict[str, object] | list[object] | str | int | float | bool | None: + if not raw_body: + return {} + + body_text = raw_body.decode("utf-8", errors="replace") + normalized_content_type = (content_type or "").lower() + should_parse_json = "application/json" in normalized_content_type + if not should_parse_json: + should_parse_json = body_text.startswith(("{", "[", '"')) or body_text in {"true", "false"} + + if should_parse_json: + try: + parsed = json.loads(body_text) + except json.JSONDecodeError: + return body_text + if isinstance(parsed, (dict, list, str, int, float, bool)) or parsed is None: + return parsed + return body_text + + +def _captured_headers(request: Request) -> dict[str, str] | None: + captured: dict[str, str] = {} + for header, value in request.headers.items(): + normalized = header.lower() + if normalized in {"content-type", "user-agent"} or normalized.startswith("x-"): + captured[normalized] = value + return captured or None + + +def _payload_preview( + value: dict[str, object] | list[object] | str | int | float | bool | None, +) -> str: + if isinstance(value, str): + preview = value + else: + try: + preview = json.dumps(value, indent=2, ensure_ascii=True) + except TypeError: + preview = str(value) + if len(preview) <= PAYLOAD_PREVIEW_MAX_CHARS: + return preview + return f"{preview[: PAYLOAD_PREVIEW_MAX_CHARS - 3]}..." + + +def _webhook_memory_content( + *, + webhook: BoardWebhook, + payload: BoardWebhookPayload, +) -> str: + preview = _payload_preview(payload.payload) + inspect_path = f"/api/v1/boards/{webhook.board_id}/webhooks/{webhook.id}/payloads/{payload.id}" + return ( + "WEBHOOK PAYLOAD RECEIVED\n" + f"Webhook ID: {webhook.id}\n" + f"Payload ID: {payload.id}\n" + f"Instruction: {webhook.description}\n" + f"Inspect (admin API): {inspect_path}\n\n" + "Payload preview:\n" + f"{preview}" + ) + + +async def _notify_lead_on_webhook_payload( + *, + session: AsyncSession, + board: Board, + webhook: BoardWebhook, + payload: BoardWebhookPayload, +) -> None: + lead = ( + await Agent.objects.filter_by(board_id=board.id) + .filter(col(Agent.is_board_lead).is_(True)) + .first(session) + ) + if lead is None or not lead.openclaw_session_id: + return + + dispatch = GatewayDispatchService(session) + config = await dispatch.optional_gateway_config_for_board(board) + if config is None: + return + + payload_preview = _payload_preview(payload.payload) + message = ( + "WEBHOOK EVENT RECEIVED\n" + f"Board: {board.name}\n" + f"Webhook ID: {webhook.id}\n" + f"Payload ID: {payload.id}\n" + f"Instruction: {webhook.description}\n\n" + "Take action:\n" + "1) Triage this payload against the webhook instruction.\n" + "2) Create/update tasks as needed.\n" + f"3) Reference payload ID {payload.id} in task descriptions.\n\n" + "Payload preview:\n" + f"{payload_preview}\n\n" + "To inspect board memory entries:\n" + f"GET /api/v1/agent/boards/{board.id}/memory?is_chat=false" + ) + await dispatch.try_send_agent_message( + session_key=lead.openclaw_session_id, + config=config, + agent_name=lead.name, + message=message, + deliver=False, + ) + + +@router.get("", response_model=DefaultLimitOffsetPage[BoardWebhookRead]) +async def list_board_webhooks( + board: Board = BOARD_USER_READ_DEP, + session: AsyncSession = SESSION_DEP, +) -> LimitOffsetPage[BoardWebhookRead]: + """List configured webhooks for a board.""" + statement = ( + select(BoardWebhook) + .where(col(BoardWebhook.board_id) == board.id) + .order_by(col(BoardWebhook.created_at).desc()) + ) + + def _transform(items: Sequence[object]) -> Sequence[object]: + webhooks = _coerce_webhook_items(items) + return [_to_webhook_read(value) for value in webhooks] + + return await paginate(session, statement, transformer=_transform) + + +@router.post("", response_model=BoardWebhookRead) +async def create_board_webhook( + payload: BoardWebhookCreate, + board: Board = BOARD_USER_WRITE_DEP, + session: AsyncSession = SESSION_DEP, +) -> BoardWebhookRead: + """Create a new board webhook with a generated UUID endpoint.""" + webhook = BoardWebhook( + board_id=board.id, + description=payload.description, + enabled=payload.enabled, + ) + await crud.save(session, webhook) + return _to_webhook_read(webhook) + + +@router.get("/{webhook_id}", response_model=BoardWebhookRead) +async def get_board_webhook( + webhook_id: UUID, + board: Board = BOARD_USER_READ_DEP, + session: AsyncSession = SESSION_DEP, +) -> BoardWebhookRead: + """Get one board webhook configuration.""" + webhook = await _require_board_webhook( + session, + board_id=board.id, + webhook_id=webhook_id, + ) + return _to_webhook_read(webhook) + + +@router.patch("/{webhook_id}", response_model=BoardWebhookRead) +async def update_board_webhook( + webhook_id: UUID, + payload: BoardWebhookUpdate, + board: Board = BOARD_USER_WRITE_DEP, + session: AsyncSession = SESSION_DEP, +) -> BoardWebhookRead: + """Update board webhook description or enabled state.""" + webhook = await _require_board_webhook( + session, + board_id=board.id, + webhook_id=webhook_id, + ) + updates = payload.model_dump(exclude_unset=True) + if updates: + crud.apply_updates(webhook, updates) + webhook.updated_at = utcnow() + await crud.save(session, webhook) + return _to_webhook_read(webhook) + + +@router.delete("/{webhook_id}", response_model=OkResponse) +async def delete_board_webhook( + webhook_id: UUID, + board: Board = BOARD_USER_WRITE_DEP, + session: AsyncSession = SESSION_DEP, +) -> OkResponse: + """Delete a webhook and its stored payload rows.""" + webhook = await _require_board_webhook( + session, + board_id=board.id, + webhook_id=webhook_id, + ) + await crud.delete_where( + session, + BoardWebhookPayload, + col(BoardWebhookPayload.webhook_id) == webhook.id, + commit=False, + ) + await session.delete(webhook) + await session.commit() + return OkResponse() + + +@router.get( + "/{webhook_id}/payloads", response_model=DefaultLimitOffsetPage[BoardWebhookPayloadRead] +) +async def list_board_webhook_payloads( + webhook_id: UUID, + board: Board = BOARD_USER_READ_DEP, + session: AsyncSession = SESSION_DEP, +) -> LimitOffsetPage[BoardWebhookPayloadRead]: + """List stored payloads for one board webhook.""" + await _require_board_webhook( + session, + board_id=board.id, + webhook_id=webhook_id, + ) + statement = ( + select(BoardWebhookPayload) + .where(col(BoardWebhookPayload.board_id) == board.id) + .where(col(BoardWebhookPayload.webhook_id) == webhook_id) + .order_by(col(BoardWebhookPayload.received_at).desc()) + ) + + def _transform(items: Sequence[object]) -> Sequence[object]: + payloads = _coerce_payload_items(items) + return [_to_payload_read(value) for value in payloads] + + return await paginate(session, statement, transformer=_transform) + + +@router.get("/{webhook_id}/payloads/{payload_id}", response_model=BoardWebhookPayloadRead) +async def get_board_webhook_payload( + webhook_id: UUID, + payload_id: UUID, + board: Board = BOARD_USER_READ_DEP, + session: AsyncSession = SESSION_DEP, +) -> BoardWebhookPayloadRead: + """Get a single stored payload for one board webhook.""" + await _require_board_webhook( + session, + board_id=board.id, + webhook_id=webhook_id, + ) + payload = await _require_board_webhook_payload( + session, + board_id=board.id, + webhook_id=webhook_id, + payload_id=payload_id, + ) + return _to_payload_read(payload) + + +@router.post( + "/{webhook_id}", + response_model=BoardWebhookIngestResponse, + status_code=status.HTTP_202_ACCEPTED, +) +async def ingest_board_webhook( + request: Request, + webhook_id: UUID, + board: Board = BOARD_OR_404_DEP, + session: AsyncSession = SESSION_DEP, +) -> BoardWebhookIngestResponse: + """Open inbound webhook endpoint that stores payloads and nudges the board lead.""" + webhook = await _require_board_webhook( + session, + board_id=board.id, + webhook_id=webhook_id, + ) + if not webhook.enabled: + raise HTTPException( + status_code=status.HTTP_410_GONE, + detail="Webhook is disabled.", + ) + + content_type = request.headers.get("content-type") + payload_value = _decode_payload( + await request.body(), + content_type=content_type, + ) + payload = BoardWebhookPayload( + board_id=board.id, + webhook_id=webhook.id, + payload=payload_value, + headers=_captured_headers(request), + source_ip=request.client.host if request.client else None, + content_type=content_type, + ) + session.add(payload) + memory = BoardMemory( + board_id=board.id, + content=_webhook_memory_content(webhook=webhook, payload=payload), + tags=[ + "webhook", + f"webhook:{webhook.id}", + f"payload:{payload.id}", + ], + source="webhook", + is_chat=False, + ) + session.add(memory) + await session.commit() + await _notify_lead_on_webhook_payload( + session=session, + board=board, + webhook=webhook, + payload=payload, + ) + return BoardWebhookIngestResponse( + board_id=board.id, + webhook_id=webhook.id, + payload_id=payload.id, + ) diff --git a/backend/app/api/organizations.py b/backend/app/api/organizations.py index 4a5ae642..683e7996 100644 --- a/backend/app/api/organizations.py +++ b/backend/app/api/organizations.py @@ -24,6 +24,8 @@ from app.models.board_group_memory import BoardGroupMemory from app.models.board_groups import BoardGroup from app.models.board_memory import BoardMemory from app.models.board_onboarding import BoardOnboardingSession +from app.models.board_webhook_payloads import BoardWebhookPayload +from app.models.board_webhooks import BoardWebhook from app.models.boards import Board from app.models.gateways import Gateway from app.models.organization_board_access import OrganizationBoardAccess @@ -290,6 +292,18 @@ async def delete_my_org( col(BoardMemory.board_id).in_(board_ids), commit=False, ) + await crud.delete_where( + session, + BoardWebhookPayload, + col(BoardWebhookPayload.board_id).in_(board_ids), + commit=False, + ) + await crud.delete_where( + session, + BoardWebhook, + col(BoardWebhook.board_id).in_(board_ids), + commit=False, + ) await crud.delete_where( session, BoardOnboardingSession, diff --git a/backend/app/api/tasks.py b/backend/app/api/tasks.py index 5a86d69b..be86fed6 100644 --- a/backend/app/api/tasks.py +++ b/backend/app/api/tasks.py @@ -120,9 +120,7 @@ def _approval_required_for_done_error() -> HTTPException: return HTTPException( status_code=status.HTTP_409_CONFLICT, detail={ - "message": ( - "Task can only be marked done when a linked approval has been approved." - ), + "message": ("Task can only be marked done when a linked approval has been approved."), "blocked_by_task_ids": [], }, ) @@ -132,9 +130,7 @@ def _review_required_for_done_error() -> HTTPException: return HTTPException( status_code=status.HTTP_409_CONFLICT, detail={ - "message": ( - "Task can only be marked done from review when the board rule is enabled." - ), + "message": ("Task can only be marked done from review when the board rule is enabled."), "blocked_by_task_ids": [], }, ) @@ -144,9 +140,7 @@ def _pending_approval_blocks_status_change_error() -> HTTPException: return HTTPException( status_code=status.HTTP_409_CONFLICT, detail={ - "message": ( - "Task status cannot be changed while a linked approval is pending." - ), + "message": ("Task status cannot be changed while a linked approval is pending."), "blocked_by_task_ids": [], }, ) diff --git a/backend/app/main.py b/backend/app/main.py index 39bbe536..2ad07000 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -18,6 +18,7 @@ from app.api.board_group_memory import router as board_group_memory_router from app.api.board_groups import router as board_groups_router from app.api.board_memory import router as board_memory_router from app.api.board_onboarding import router as board_onboarding_router +from app.api.board_webhooks import router as board_webhooks_router from app.api.boards import router as boards_router from app.api.gateway import router as gateway_router from app.api.gateways import router as gateways_router @@ -105,6 +106,7 @@ api_v1.include_router(board_groups_router) api_v1.include_router(board_group_memory_router) api_v1.include_router(boards_router) api_v1.include_router(board_memory_router) +api_v1.include_router(board_webhooks_router) api_v1.include_router(board_onboarding_router) api_v1.include_router(approvals_router) api_v1.include_router(tasks_router) diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index 94b3b8cc..0b90e249 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -8,6 +8,8 @@ from app.models.board_group_memory import BoardGroupMemory from app.models.board_groups import BoardGroup from app.models.board_memory import BoardMemory from app.models.board_onboarding import BoardOnboardingSession +from app.models.board_webhook_payloads import BoardWebhookPayload +from app.models.board_webhooks import BoardWebhook from app.models.boards import Board from app.models.gateways import Gateway from app.models.organization_board_access import OrganizationBoardAccess @@ -28,6 +30,8 @@ __all__ = [ "ApprovalTaskLink", "Approval", "BoardGroupMemory", + "BoardWebhook", + "BoardWebhookPayload", "BoardMemory", "BoardOnboardingSession", "BoardGroup", diff --git a/backend/app/models/board_webhook_payloads.py b/backend/app/models/board_webhook_payloads.py new file mode 100644 index 00000000..606596a1 --- /dev/null +++ b/backend/app/models/board_webhook_payloads.py @@ -0,0 +1,32 @@ +"""Persisted webhook payloads received for board webhooks.""" + +from __future__ import annotations + +from datetime import datetime +from uuid import UUID, uuid4 + +from sqlalchemy import JSON, Column +from sqlmodel import Field + +from app.core.time import utcnow +from app.models.base import QueryModel + +RUNTIME_ANNOTATION_TYPES = (datetime,) + + +class BoardWebhookPayload(QueryModel, table=True): + """Captured inbound webhook payload with request metadata.""" + + __tablename__ = "board_webhook_payloads" # pyright: ignore[reportAssignmentType] + + id: UUID = Field(default_factory=uuid4, primary_key=True) + board_id: UUID = Field(foreign_key="boards.id", index=True) + webhook_id: UUID = Field(foreign_key="board_webhooks.id", index=True) + payload: dict[str, object] | list[object] | str | int | float | bool | None = Field( + default=None, + sa_column=Column(JSON), + ) + headers: dict[str, str] | None = Field(default=None, sa_column=Column(JSON)) + source_ip: str | None = None + content_type: str | None = None + received_at: datetime = Field(default_factory=utcnow, index=True) diff --git a/backend/app/models/board_webhooks.py b/backend/app/models/board_webhooks.py new file mode 100644 index 00000000..43e524f0 --- /dev/null +++ b/backend/app/models/board_webhooks.py @@ -0,0 +1,26 @@ +"""Board webhook configuration model.""" + +from __future__ import annotations + +from datetime import datetime +from uuid import UUID, uuid4 + +from sqlmodel import Field + +from app.core.time import utcnow +from app.models.base import QueryModel + +RUNTIME_ANNOTATION_TYPES = (datetime,) + + +class BoardWebhook(QueryModel, table=True): + """Inbound webhook endpoint configuration for a board.""" + + __tablename__ = "board_webhooks" # pyright: ignore[reportAssignmentType] + + id: UUID = Field(default_factory=uuid4, primary_key=True) + board_id: UUID = Field(foreign_key="boards.id", index=True) + description: str + enabled: bool = Field(default=True, index=True) + created_at: datetime = Field(default_factory=utcnow) + updated_at: datetime = Field(default_factory=utcnow) diff --git a/backend/app/schemas/__init__.py b/backend/app/schemas/__init__.py index aaa8ac81..0843a445 100644 --- a/backend/app/schemas/__init__.py +++ b/backend/app/schemas/__init__.py @@ -11,6 +11,13 @@ from app.schemas.board_onboarding import ( BoardOnboardingRead, BoardOnboardingStart, ) +from app.schemas.board_webhooks import ( + BoardWebhookCreate, + BoardWebhookIngestResponse, + BoardWebhookPayloadRead, + BoardWebhookRead, + BoardWebhookUpdate, +) from app.schemas.boards import BoardCreate, BoardRead, BoardUpdate from app.schemas.gateways import GatewayCreate, GatewayRead, GatewayUpdate from app.schemas.metrics import DashboardMetrics @@ -47,6 +54,11 @@ __all__ = [ "BoardGroupMemoryRead", "BoardMemoryCreate", "BoardMemoryRead", + "BoardWebhookCreate", + "BoardWebhookIngestResponse", + "BoardWebhookPayloadRead", + "BoardWebhookRead", + "BoardWebhookUpdate", "BoardOnboardingAnswer", "BoardOnboardingConfirm", "BoardOnboardingRead", diff --git a/backend/app/schemas/board_webhooks.py b/backend/app/schemas/board_webhooks.py new file mode 100644 index 00000000..7a5348c8 --- /dev/null +++ b/backend/app/schemas/board_webhooks.py @@ -0,0 +1,61 @@ +"""Schemas for board webhook configuration and payload capture endpoints.""" + +from __future__ import annotations + +from datetime import datetime +from uuid import UUID + +from sqlmodel import SQLModel + +from app.schemas.common import NonEmptyStr + +RUNTIME_ANNOTATION_TYPES = (datetime, UUID, NonEmptyStr) + + +class BoardWebhookCreate(SQLModel): + """Payload for creating a board webhook.""" + + description: NonEmptyStr + enabled: bool = True + + +class BoardWebhookUpdate(SQLModel): + """Payload for updating a board webhook.""" + + description: NonEmptyStr | None = None + enabled: bool | None = None + + +class BoardWebhookRead(SQLModel): + """Serialized board webhook configuration.""" + + id: UUID + board_id: UUID + description: str + enabled: bool + endpoint_path: str + endpoint_url: str | None = None + created_at: datetime + updated_at: datetime + + +class BoardWebhookPayloadRead(SQLModel): + """Serialized stored webhook payload.""" + + id: UUID + board_id: UUID + webhook_id: UUID + payload: dict[str, object] | list[object] | str | int | float | bool | None = None + headers: dict[str, str] | None = None + source_ip: str | None = None + content_type: str | None = None + received_at: datetime + + +class BoardWebhookIngestResponse(SQLModel): + """Response payload for inbound webhook ingestion.""" + + ok: bool = True + board_id: UUID + webhook_id: UUID + payload_id: UUID diff --git a/backend/app/services/board_lifecycle.py b/backend/app/services/board_lifecycle.py index 8527044a..ea59e14e 100644 --- a/backend/app/services/board_lifecycle.py +++ b/backend/app/services/board_lifecycle.py @@ -18,6 +18,8 @@ from app.models.approval_task_links import ApprovalTaskLink from app.models.approvals import Approval from app.models.board_memory import BoardMemory from app.models.board_onboarding import BoardOnboardingSession +from app.models.board_webhook_payloads import BoardWebhookPayload +from app.models.board_webhooks import BoardWebhook from app.models.organization_board_access import OrganizationBoardAccess from app.models.organization_invite_board_access import OrganizationInviteBoardAccess from app.models.task_dependencies import TaskDependency @@ -84,6 +86,12 @@ async def delete_board(session: AsyncSession, *, board: Board) -> OkResponse: await crud.delete_where(session, Approval, col(Approval.board_id) == board.id) await crud.delete_where(session, BoardMemory, col(BoardMemory.board_id) == board.id) + await crud.delete_where( + session, + BoardWebhookPayload, + col(BoardWebhookPayload.board_id) == board.id, + ) + await crud.delete_where(session, BoardWebhook, col(BoardWebhook.board_id) == board.id) await crud.delete_where( session, BoardOnboardingSession, diff --git a/backend/migrations/versions/fa6e83f8d9a1_add_board_webhooks_and_payloads.py b/backend/migrations/versions/fa6e83f8d9a1_add_board_webhooks_and_payloads.py new file mode 100644 index 00000000..7bf93256 --- /dev/null +++ b/backend/migrations/versions/fa6e83f8d9a1_add_board_webhooks_and_payloads.py @@ -0,0 +1,130 @@ +"""Add board webhook configuration and payload storage tables. + +Revision ID: fa6e83f8d9a1 +Revises: c2e9f1a6d4b8 +Create Date: 2026-02-13 00:10:00.000000 + +""" + +from __future__ import annotations + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "fa6e83f8d9a1" +down_revision = "c2e9f1a6d4b8" +branch_labels = None +depends_on = None + + +def _index_names(inspector: sa.Inspector, table_name: str) -> set[str]: + return {item["name"] for item in inspector.get_indexes(table_name)} + + +def upgrade() -> None: + """Create board webhook and payload capture tables.""" + bind = op.get_bind() + inspector = sa.inspect(bind) + + if not inspector.has_table("board_webhooks"): + op.create_table( + "board_webhooks", + sa.Column("id", sa.Uuid(), nullable=False), + sa.Column("board_id", sa.Uuid(), nullable=False), + sa.Column("description", sa.String(), nullable=False), + sa.Column("enabled", sa.Boolean(), nullable=False), + sa.Column("created_at", sa.DateTime(), nullable=False), + sa.Column("updated_at", sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(["board_id"], ["boards.id"]), + sa.PrimaryKeyConstraint("id"), + ) + + inspector = sa.inspect(bind) + webhook_indexes = _index_names(inspector, "board_webhooks") + if "ix_board_webhooks_board_id" not in webhook_indexes: + op.create_index("ix_board_webhooks_board_id", "board_webhooks", ["board_id"]) + if "ix_board_webhooks_enabled" not in webhook_indexes: + op.create_index("ix_board_webhooks_enabled", "board_webhooks", ["enabled"]) + + if not inspector.has_table("board_webhook_payloads"): + op.create_table( + "board_webhook_payloads", + sa.Column("id", sa.Uuid(), nullable=False), + sa.Column("board_id", sa.Uuid(), nullable=False), + sa.Column("webhook_id", sa.Uuid(), nullable=False), + sa.Column("payload", sa.JSON(), nullable=True), + sa.Column("headers", sa.JSON(), nullable=True), + sa.Column("source_ip", sa.String(), nullable=True), + sa.Column("content_type", sa.String(), nullable=True), + sa.Column("received_at", sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(["board_id"], ["boards.id"]), + sa.ForeignKeyConstraint(["webhook_id"], ["board_webhooks.id"]), + sa.PrimaryKeyConstraint("id"), + ) + + inspector = sa.inspect(bind) + payload_indexes = _index_names(inspector, "board_webhook_payloads") + if "ix_board_webhook_payloads_board_id" not in payload_indexes: + op.create_index( + "ix_board_webhook_payloads_board_id", + "board_webhook_payloads", + ["board_id"], + ) + if "ix_board_webhook_payloads_webhook_id" not in payload_indexes: + op.create_index( + "ix_board_webhook_payloads_webhook_id", + "board_webhook_payloads", + ["webhook_id"], + ) + if "ix_board_webhook_payloads_received_at" not in payload_indexes: + op.create_index( + "ix_board_webhook_payloads_received_at", + "board_webhook_payloads", + ["received_at"], + ) + if "ix_board_webhook_payloads_board_webhook_received_at" not in payload_indexes: + op.create_index( + "ix_board_webhook_payloads_board_webhook_received_at", + "board_webhook_payloads", + ["board_id", "webhook_id", "received_at"], + ) + + +def downgrade() -> None: + """Drop board webhook and payload capture tables.""" + bind = op.get_bind() + inspector = sa.inspect(bind) + + if inspector.has_table("board_webhook_payloads"): + payload_indexes = _index_names(inspector, "board_webhook_payloads") + if "ix_board_webhook_payloads_board_webhook_received_at" in payload_indexes: + op.drop_index( + "ix_board_webhook_payloads_board_webhook_received_at", + table_name="board_webhook_payloads", + ) + if "ix_board_webhook_payloads_received_at" in payload_indexes: + op.drop_index( + "ix_board_webhook_payloads_received_at", + table_name="board_webhook_payloads", + ) + if "ix_board_webhook_payloads_webhook_id" in payload_indexes: + op.drop_index( + "ix_board_webhook_payloads_webhook_id", + table_name="board_webhook_payloads", + ) + if "ix_board_webhook_payloads_board_id" in payload_indexes: + op.drop_index( + "ix_board_webhook_payloads_board_id", + table_name="board_webhook_payloads", + ) + op.drop_table("board_webhook_payloads") + + inspector = sa.inspect(bind) + if inspector.has_table("board_webhooks"): + webhook_indexes = _index_names(inspector, "board_webhooks") + if "ix_board_webhooks_enabled" in webhook_indexes: + op.drop_index("ix_board_webhooks_enabled", table_name="board_webhooks") + if "ix_board_webhooks_board_id" in webhook_indexes: + op.drop_index("ix_board_webhooks_board_id", table_name="board_webhooks") + op.drop_table("board_webhooks") diff --git a/backend/tests/test_board_webhooks_api.py b/backend/tests/test_board_webhooks_api.py new file mode 100644 index 00000000..77128c87 --- /dev/null +++ b/backend/tests/test_board_webhooks_api.py @@ -0,0 +1,282 @@ +# ruff: noqa: INP001 +"""Integration tests for board webhook ingestion behavior.""" + +from __future__ import annotations + +from uuid import UUID, uuid4 + +import pytest +from fastapi import APIRouter, Depends, FastAPI +from httpx import ASGITransport, AsyncClient +from sqlalchemy.ext.asyncio import AsyncEngine, async_sessionmaker, create_async_engine +from sqlmodel import SQLModel, col, select +from sqlmodel.ext.asyncio.session import AsyncSession + +from app.api import board_webhooks +from app.api.board_webhooks import router as board_webhooks_router +from app.api.deps import get_board_or_404 +from app.db.session import get_session +from app.models.agents import Agent +from app.models.board_memory import BoardMemory +from app.models.board_webhook_payloads import BoardWebhookPayload +from app.models.board_webhooks import BoardWebhook +from app.models.boards import Board +from app.models.gateways import Gateway +from app.models.organizations import Organization + + +async def _make_engine() -> AsyncEngine: + engine = create_async_engine("sqlite+aiosqlite:///:memory:") + async with engine.connect() as conn, conn.begin(): + await conn.run_sync(SQLModel.metadata.create_all) + return engine + + +def _build_test_app( + session_maker: async_sessionmaker[AsyncSession], +) -> FastAPI: + app = FastAPI() + api_v1 = APIRouter(prefix="/api/v1") + api_v1.include_router(board_webhooks_router) + app.include_router(api_v1) + + async def _override_get_session() -> AsyncSession: + async with session_maker() as session: + yield session + + async def _override_get_board_or_404( + board_id: str, + session: AsyncSession = Depends(get_session), + ) -> Board: + board = await Board.objects.by_id(UUID(board_id)).first(session) + if board is None: + from fastapi import HTTPException, status + + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND) + return board + + app.dependency_overrides[get_session] = _override_get_session + app.dependency_overrides[get_board_or_404] = _override_get_board_or_404 + return app + + +async def _seed_webhook( + session: AsyncSession, + *, + enabled: bool, +) -> tuple[Board, BoardWebhook]: + organization_id = uuid4() + gateway_id = uuid4() + board_id = uuid4() + webhook_id = uuid4() + + session.add(Organization(id=organization_id, name=f"org-{organization_id}")) + session.add( + Gateway( + id=gateway_id, + organization_id=organization_id, + name="gateway", + url="https://gateway.example.local", + workspace_root="/tmp/workspace", + ), + ) + board = Board( + id=board_id, + organization_id=organization_id, + gateway_id=gateway_id, + name="Launch board", + slug="launch-board", + description="Board for launch automation.", + ) + session.add(board) + session.add( + Agent( + id=uuid4(), + board_id=board_id, + gateway_id=gateway_id, + name="Lead Agent", + status="online", + openclaw_session_id="lead:session:key", + is_board_lead=True, + ), + ) + webhook = BoardWebhook( + id=webhook_id, + board_id=board_id, + description="Triage payload and create tasks for impacted services.", + enabled=enabled, + ) + session.add(webhook) + await session.commit() + return board, webhook + + +@pytest.mark.asyncio +async def test_ingest_board_webhook_stores_payload_and_notifies_lead( + monkeypatch: pytest.MonkeyPatch, +) -> None: + engine = await _make_engine() + session_maker = async_sessionmaker( + engine, + class_=AsyncSession, + expire_on_commit=False, + ) + app = _build_test_app(session_maker) + sent_messages: list[dict[str, str]] = [] + + async with session_maker() as session: + board, webhook = await _seed_webhook(session, enabled=True) + + async def _fake_optional_gateway_config_for_board( + self: board_webhooks.GatewayDispatchService, + _board: Board, + ) -> object: + return object() + + async def _fake_try_send_agent_message( + self: board_webhooks.GatewayDispatchService, + *, + session_key: str, + config: object, + agent_name: str, + message: str, + deliver: bool = False, + ) -> None: + del self, config, deliver + sent_messages.append( + { + "session_key": session_key, + "agent_name": agent_name, + "message": message, + }, + ) + return None + + monkeypatch.setattr( + board_webhooks.GatewayDispatchService, + "optional_gateway_config_for_board", + _fake_optional_gateway_config_for_board, + ) + monkeypatch.setattr( + board_webhooks.GatewayDispatchService, + "try_send_agent_message", + _fake_try_send_agent_message, + ) + + try: + async with AsyncClient( + transport=ASGITransport(app=app), + base_url="http://testserver", + ) as client: + response = await client.post( + f"/api/v1/boards/{board.id}/webhooks/{webhook.id}", + json={"event": "deploy", "service": "api"}, + headers={"X-Signature": "sha256=abc123"}, + ) + + assert response.status_code == 202 + body = response.json() + payload_id = UUID(body["payload_id"]) + assert body["board_id"] == str(board.id) + assert body["webhook_id"] == str(webhook.id) + + async with session_maker() as session: + payloads = ( + await session.exec( + select(BoardWebhookPayload).where(col(BoardWebhookPayload.id) == payload_id), + ) + ).all() + assert len(payloads) == 1 + assert payloads[0].payload == {"event": "deploy", "service": "api"} + assert payloads[0].headers is not None + assert payloads[0].headers.get("x-signature") == "sha256=abc123" + assert payloads[0].headers.get("content-type") == "application/json" + + memory_items = ( + await session.exec( + select(BoardMemory).where(col(BoardMemory.board_id) == board.id), + ) + ).all() + assert len(memory_items) == 1 + assert memory_items[0].source == "webhook" + assert memory_items[0].tags is not None + assert f"webhook:{webhook.id}" in memory_items[0].tags + assert f"payload:{payload_id}" in memory_items[0].tags + assert f"Payload ID: {payload_id}" in memory_items[0].content + + assert len(sent_messages) == 1 + assert sent_messages[0]["session_key"] == "lead:session:key" + assert "WEBHOOK EVENT RECEIVED" in sent_messages[0]["message"] + assert str(payload_id) in sent_messages[0]["message"] + assert webhook.description in sent_messages[0]["message"] + finally: + await engine.dispose() + + +@pytest.mark.asyncio +async def test_ingest_board_webhook_rejects_disabled_endpoint( + monkeypatch: pytest.MonkeyPatch, +) -> None: + engine = await _make_engine() + session_maker = async_sessionmaker( + engine, + class_=AsyncSession, + expire_on_commit=False, + ) + app = _build_test_app(session_maker) + sent_messages: list[str] = [] + + async with session_maker() as session: + board, webhook = await _seed_webhook(session, enabled=False) + + async def _fake_try_send_agent_message( + self: board_webhooks.GatewayDispatchService, + *, + session_key: str, + config: object, + agent_name: str, + message: str, + deliver: bool = False, + ) -> None: + del self, session_key, config, agent_name, deliver + sent_messages.append(message) + return None + + monkeypatch.setattr( + board_webhooks.GatewayDispatchService, + "try_send_agent_message", + _fake_try_send_agent_message, + ) + + try: + async with AsyncClient( + transport=ASGITransport(app=app), + base_url="http://testserver", + ) as client: + response = await client.post( + f"/api/v1/boards/{board.id}/webhooks/{webhook.id}", + json={"event": "deploy"}, + ) + + assert response.status_code == 410 + assert response.json() == {"detail": "Webhook is disabled."} + + async with session_maker() as session: + stored_payloads = ( + await session.exec( + select(BoardWebhookPayload).where( + col(BoardWebhookPayload.board_id) == board.id + ), + ) + ).all() + assert stored_payloads == [] + stored_memory = ( + await session.exec( + select(BoardMemory).where(col(BoardMemory.board_id) == board.id), + ) + ).all() + assert stored_memory == [] + + assert sent_messages == [] + finally: + await engine.dispose() diff --git a/backend/tests/test_organizations_delete_api.py b/backend/tests/test_organizations_delete_api.py index 77a47f75..6527ef5a 100644 --- a/backend/tests/test_organizations_delete_api.py +++ b/backend/tests/test_organizations_delete_api.py @@ -59,6 +59,8 @@ async def test_delete_my_org_cleans_dependents_before_organization_delete() -> N "approval_task_links", "approvals", "board_memory", + "board_webhook_payloads", + "board_webhooks", "board_onboarding_sessions", "organization_board_access", "organization_invite_board_access", diff --git a/backend/tests/test_tasks_done_approval_gate.py b/backend/tests/test_tasks_done_approval_gate.py index ea042c63..a3d6deea 100644 --- a/backend/tests/test_tasks_done_approval_gate.py +++ b/backend/tests/test_tasks_done_approval_gate.py @@ -277,7 +277,9 @@ async def test_update_task_allows_done_from_review_when_review_toggle_enabled() @pytest.mark.asyncio -async def test_update_task_rejects_status_change_with_pending_approval_when_toggle_enabled() -> None: +async def test_update_task_rejects_status_change_with_pending_approval_when_toggle_enabled() -> ( + None +): engine = await _make_engine() try: async with await _make_session(engine) as session: @@ -318,7 +320,9 @@ async def test_update_task_rejects_status_change_with_pending_approval_when_togg @pytest.mark.asyncio -async def test_update_task_allows_status_change_with_pending_approval_when_toggle_disabled() -> None: +async def test_update_task_allows_status_change_with_pending_approval_when_toggle_disabled() -> ( + None +): engine = await _make_engine() try: async with await _make_session(engine) as session: @@ -353,7 +357,9 @@ async def test_update_task_allows_status_change_with_pending_approval_when_toggl @pytest.mark.asyncio -async def test_update_task_rejects_status_change_for_pending_multi_task_link_when_toggle_enabled() -> None: +async def test_update_task_rejects_status_change_for_pending_multi_task_link_when_toggle_enabled() -> ( + None +): engine = await _make_engine() try: async with await _make_session(engine) as session: diff --git a/frontend/README.md b/frontend/README.md index 7ca1e33c..07d4daf2 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -113,6 +113,7 @@ It will: When changing UI intended to be mobile-ready, validate in Chrome (or similar) using the device toolbar at common widths (e.g. **320px**, **375px**, **768px**). Quick checklist: + - No horizontal scroll - Primary actions reachable without precision taps - Focus rings visible when tabbing diff --git a/frontend/src/api/generated/board-webhooks/board-webhooks.ts b/frontend/src/api/generated/board-webhooks/board-webhooks.ts new file mode 100644 index 00000000..3170f958 --- /dev/null +++ b/frontend/src/api/generated/board-webhooks/board-webhooks.ts @@ -0,0 +1,1829 @@ +/** + * Generated by orval v8.3.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 { + BoardWebhookCreate, + BoardWebhookIngestResponse, + BoardWebhookPayloadRead, + BoardWebhookRead, + BoardWebhookUpdate, + HTTPValidationError, + LimitOffsetPageTypeVarCustomizedBoardWebhookPayloadRead, + LimitOffsetPageTypeVarCustomizedBoardWebhookRead, + ListBoardWebhookPayloadsApiV1BoardsBoardIdWebhooksWebhookIdPayloadsGetParams, + ListBoardWebhooksApiV1BoardsBoardIdWebhooksGetParams, + OkResponse, +} from ".././model"; + +import { customFetch } from "../../mutator"; + +type SecondParameter unknown> = Parameters[1]; + +/** + * List configured webhooks for a board. + * @summary List Board Webhooks + */ +export type listBoardWebhooksApiV1BoardsBoardIdWebhooksGetResponse200 = { + data: LimitOffsetPageTypeVarCustomizedBoardWebhookRead; + status: 200; +}; + +export type listBoardWebhooksApiV1BoardsBoardIdWebhooksGetResponse422 = { + data: HTTPValidationError; + status: 422; +}; + +export type listBoardWebhooksApiV1BoardsBoardIdWebhooksGetResponseSuccess = + listBoardWebhooksApiV1BoardsBoardIdWebhooksGetResponse200 & { + headers: Headers; + }; +export type listBoardWebhooksApiV1BoardsBoardIdWebhooksGetResponseError = + listBoardWebhooksApiV1BoardsBoardIdWebhooksGetResponse422 & { + headers: Headers; + }; + +export type listBoardWebhooksApiV1BoardsBoardIdWebhooksGetResponse = + | listBoardWebhooksApiV1BoardsBoardIdWebhooksGetResponseSuccess + | listBoardWebhooksApiV1BoardsBoardIdWebhooksGetResponseError; + +export const getListBoardWebhooksApiV1BoardsBoardIdWebhooksGetUrl = ( + boardId: string, + params?: ListBoardWebhooksApiV1BoardsBoardIdWebhooksGetParams, +) => { + 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}/webhooks?${stringifiedParams}` + : `/api/v1/boards/${boardId}/webhooks`; +}; + +export const listBoardWebhooksApiV1BoardsBoardIdWebhooksGet = async ( + boardId: string, + params?: ListBoardWebhooksApiV1BoardsBoardIdWebhooksGetParams, + options?: RequestInit, +): Promise => { + return customFetch( + getListBoardWebhooksApiV1BoardsBoardIdWebhooksGetUrl(boardId, params), + { + ...options, + method: "GET", + }, + ); +}; + +export const getListBoardWebhooksApiV1BoardsBoardIdWebhooksGetQueryKey = ( + boardId: string, + params?: ListBoardWebhooksApiV1BoardsBoardIdWebhooksGetParams, +) => { + return [ + `/api/v1/boards/${boardId}/webhooks`, + ...(params ? [params] : []), + ] as const; +}; + +export const getListBoardWebhooksApiV1BoardsBoardIdWebhooksGetQueryOptions = < + TData = Awaited< + ReturnType + >, + TError = HTTPValidationError, +>( + boardId: string, + params?: ListBoardWebhooksApiV1BoardsBoardIdWebhooksGetParams, + options?: { + query?: Partial< + UseQueryOptions< + Awaited< + ReturnType + >, + TError, + TData + > + >; + request?: SecondParameter; + }, +) => { + const { query: queryOptions, request: requestOptions } = options ?? {}; + + const queryKey = + queryOptions?.queryKey ?? + getListBoardWebhooksApiV1BoardsBoardIdWebhooksGetQueryKey(boardId, params); + + const queryFn: QueryFunction< + Awaited> + > = ({ signal }) => + listBoardWebhooksApiV1BoardsBoardIdWebhooksGet(boardId, params, { + signal, + ...requestOptions, + }); + + return { + queryKey, + queryFn, + enabled: !!boardId, + ...queryOptions, + } as UseQueryOptions< + Awaited>, + TError, + TData + > & { queryKey: DataTag }; +}; + +export type ListBoardWebhooksApiV1BoardsBoardIdWebhooksGetQueryResult = + NonNullable< + Awaited> + >; +export type ListBoardWebhooksApiV1BoardsBoardIdWebhooksGetQueryError = + HTTPValidationError; + +export function useListBoardWebhooksApiV1BoardsBoardIdWebhooksGet< + TData = Awaited< + ReturnType + >, + TError = HTTPValidationError, +>( + boardId: string, + params: undefined | ListBoardWebhooksApiV1BoardsBoardIdWebhooksGetParams, + options: { + query: Partial< + UseQueryOptions< + Awaited< + ReturnType + >, + TError, + TData + > + > & + Pick< + DefinedInitialDataOptions< + Awaited< + ReturnType + >, + TError, + Awaited< + ReturnType + > + >, + "initialData" + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): DefinedUseQueryResult & { + queryKey: DataTag; +}; +export function useListBoardWebhooksApiV1BoardsBoardIdWebhooksGet< + TData = Awaited< + ReturnType + >, + TError = HTTPValidationError, +>( + boardId: string, + params?: ListBoardWebhooksApiV1BoardsBoardIdWebhooksGetParams, + options?: { + query?: Partial< + UseQueryOptions< + Awaited< + ReturnType + >, + TError, + TData + > + > & + Pick< + UndefinedInitialDataOptions< + Awaited< + ReturnType + >, + TError, + Awaited< + ReturnType + > + >, + "initialData" + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { + queryKey: DataTag; +}; +export function useListBoardWebhooksApiV1BoardsBoardIdWebhooksGet< + TData = Awaited< + ReturnType + >, + TError = HTTPValidationError, +>( + boardId: string, + params?: ListBoardWebhooksApiV1BoardsBoardIdWebhooksGetParams, + options?: { + query?: Partial< + UseQueryOptions< + Awaited< + ReturnType + >, + TError, + TData + > + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { + queryKey: DataTag; +}; +/** + * @summary List Board Webhooks + */ + +export function useListBoardWebhooksApiV1BoardsBoardIdWebhooksGet< + TData = Awaited< + ReturnType + >, + TError = HTTPValidationError, +>( + boardId: string, + params?: ListBoardWebhooksApiV1BoardsBoardIdWebhooksGetParams, + options?: { + query?: Partial< + UseQueryOptions< + Awaited< + ReturnType + >, + TError, + TData + > + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { + queryKey: DataTag; +} { + const queryOptions = + getListBoardWebhooksApiV1BoardsBoardIdWebhooksGetQueryOptions( + boardId, + params, + options, + ); + + const query = useQuery(queryOptions, queryClient) as UseQueryResult< + TData, + TError + > & { queryKey: DataTag }; + + return { ...query, queryKey: queryOptions.queryKey }; +} + +/** + * Create a new board webhook with a generated UUID endpoint. + * @summary Create Board Webhook + */ +export type createBoardWebhookApiV1BoardsBoardIdWebhooksPostResponse200 = { + data: BoardWebhookRead; + status: 200; +}; + +export type createBoardWebhookApiV1BoardsBoardIdWebhooksPostResponse422 = { + data: HTTPValidationError; + status: 422; +}; + +export type createBoardWebhookApiV1BoardsBoardIdWebhooksPostResponseSuccess = + createBoardWebhookApiV1BoardsBoardIdWebhooksPostResponse200 & { + headers: Headers; + }; +export type createBoardWebhookApiV1BoardsBoardIdWebhooksPostResponseError = + createBoardWebhookApiV1BoardsBoardIdWebhooksPostResponse422 & { + headers: Headers; + }; + +export type createBoardWebhookApiV1BoardsBoardIdWebhooksPostResponse = + | createBoardWebhookApiV1BoardsBoardIdWebhooksPostResponseSuccess + | createBoardWebhookApiV1BoardsBoardIdWebhooksPostResponseError; + +export const getCreateBoardWebhookApiV1BoardsBoardIdWebhooksPostUrl = ( + boardId: string, +) => { + return `/api/v1/boards/${boardId}/webhooks`; +}; + +export const createBoardWebhookApiV1BoardsBoardIdWebhooksPost = async ( + boardId: string, + boardWebhookCreate: BoardWebhookCreate, + options?: RequestInit, +): Promise => { + return customFetch( + getCreateBoardWebhookApiV1BoardsBoardIdWebhooksPostUrl(boardId), + { + ...options, + method: "POST", + headers: { "Content-Type": "application/json", ...options?.headers }, + body: JSON.stringify(boardWebhookCreate), + }, + ); +}; + +export const getCreateBoardWebhookApiV1BoardsBoardIdWebhooksPostMutationOptions = + (options?: { + mutation?: UseMutationOptions< + Awaited< + ReturnType + >, + TError, + { boardId: string; data: BoardWebhookCreate }, + TContext + >; + request?: SecondParameter; + }): UseMutationOptions< + Awaited< + ReturnType + >, + TError, + { boardId: string; data: BoardWebhookCreate }, + TContext + > => { + const mutationKey = ["createBoardWebhookApiV1BoardsBoardIdWebhooksPost"]; + 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 + >, + { boardId: string; data: BoardWebhookCreate } + > = (props) => { + const { boardId, data } = props ?? {}; + + return createBoardWebhookApiV1BoardsBoardIdWebhooksPost( + boardId, + data, + requestOptions, + ); + }; + + return { mutationFn, ...mutationOptions }; + }; + +export type CreateBoardWebhookApiV1BoardsBoardIdWebhooksPostMutationResult = + NonNullable< + Awaited> + >; +export type CreateBoardWebhookApiV1BoardsBoardIdWebhooksPostMutationBody = + BoardWebhookCreate; +export type CreateBoardWebhookApiV1BoardsBoardIdWebhooksPostMutationError = + HTTPValidationError; + +/** + * @summary Create Board Webhook + */ +export const useCreateBoardWebhookApiV1BoardsBoardIdWebhooksPost = < + TError = HTTPValidationError, + TContext = unknown, +>( + options?: { + mutation?: UseMutationOptions< + Awaited< + ReturnType + >, + TError, + { boardId: string; data: BoardWebhookCreate }, + TContext + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseMutationResult< + Awaited>, + TError, + { boardId: string; data: BoardWebhookCreate }, + TContext +> => { + return useMutation( + getCreateBoardWebhookApiV1BoardsBoardIdWebhooksPostMutationOptions(options), + queryClient, + ); +}; +/** + * Delete a webhook and its stored payload rows. + * @summary Delete Board Webhook + */ +export type deleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDeleteResponse200 = + { + data: OkResponse; + status: 200; + }; + +export type deleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDeleteResponse422 = + { + data: HTTPValidationError; + status: 422; + }; + +export type deleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDeleteResponseSuccess = + deleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDeleteResponse200 & { + headers: Headers; + }; +export type deleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDeleteResponseError = + deleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDeleteResponse422 & { + headers: Headers; + }; + +export type deleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDeleteResponse = + + | deleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDeleteResponseSuccess + | deleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDeleteResponseError; + +export const getDeleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDeleteUrl = + (boardId: string, webhookId: string) => { + return `/api/v1/boards/${boardId}/webhooks/${webhookId}`; + }; + +export const deleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDelete = + async ( + boardId: string, + webhookId: string, + options?: RequestInit, + ): Promise => { + return customFetch( + getDeleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDeleteUrl( + boardId, + webhookId, + ), + { + ...options, + method: "DELETE", + }, + ); + }; + +export const getDeleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDeleteMutationOptions = + (options?: { + mutation?: UseMutationOptions< + Awaited< + ReturnType< + typeof deleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDelete + > + >, + TError, + { boardId: string; webhookId: string }, + TContext + >; + request?: SecondParameter; + }): UseMutationOptions< + Awaited< + ReturnType< + typeof deleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDelete + > + >, + TError, + { boardId: string; webhookId: string }, + TContext + > => { + const mutationKey = [ + "deleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDelete", + ]; + 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 deleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDelete + > + >, + { boardId: string; webhookId: string } + > = (props) => { + const { boardId, webhookId } = props ?? {}; + + return deleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDelete( + boardId, + webhookId, + requestOptions, + ); + }; + + return { mutationFn, ...mutationOptions }; + }; + +export type DeleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDeleteMutationResult = + NonNullable< + Awaited< + ReturnType< + typeof deleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDelete + > + > + >; + +export type DeleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDeleteMutationError = + HTTPValidationError; + +/** + * @summary Delete Board Webhook + */ +export const useDeleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDelete = < + TError = HTTPValidationError, + TContext = unknown, +>( + options?: { + mutation?: UseMutationOptions< + Awaited< + ReturnType< + typeof deleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDelete + > + >, + TError, + { boardId: string; webhookId: string }, + TContext + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseMutationResult< + Awaited< + ReturnType< + typeof deleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDelete + > + >, + TError, + { boardId: string; webhookId: string }, + TContext +> => { + return useMutation( + getDeleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDeleteMutationOptions( + options, + ), + queryClient, + ); +}; +/** + * Get one board webhook configuration. + * @summary Get Board Webhook + */ +export type getBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdGetResponse200 = { + data: BoardWebhookRead; + status: 200; +}; + +export type getBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdGetResponse422 = { + data: HTTPValidationError; + status: 422; +}; + +export type getBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdGetResponseSuccess = + getBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdGetResponse200 & { + headers: Headers; + }; +export type getBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdGetResponseError = + getBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdGetResponse422 & { + headers: Headers; + }; + +export type getBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdGetResponse = + | getBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdGetResponseSuccess + | getBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdGetResponseError; + +export const getGetBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdGetUrl = ( + boardId: string, + webhookId: string, +) => { + return `/api/v1/boards/${boardId}/webhooks/${webhookId}`; +}; + +export const getBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdGet = async ( + boardId: string, + webhookId: string, + options?: RequestInit, +): Promise => { + return customFetch( + getGetBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdGetUrl( + boardId, + webhookId, + ), + { + ...options, + method: "GET", + }, + ); +}; + +export const getGetBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdGetQueryKey = + (boardId: string, webhookId: string) => { + return [`/api/v1/boards/${boardId}/webhooks/${webhookId}`] as const; + }; + +export const getGetBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdGetQueryOptions = + < + TData = Awaited< + ReturnType + >, + TError = HTTPValidationError, + >( + boardId: string, + webhookId: string, + options?: { + query?: Partial< + UseQueryOptions< + Awaited< + ReturnType< + typeof getBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdGet + > + >, + TError, + TData + > + >; + request?: SecondParameter; + }, + ) => { + const { query: queryOptions, request: requestOptions } = options ?? {}; + + const queryKey = + queryOptions?.queryKey ?? + getGetBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdGetQueryKey( + boardId, + webhookId, + ); + + const queryFn: QueryFunction< + Awaited< + ReturnType + > + > = ({ signal }) => + getBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdGet( + boardId, + webhookId, + { signal, ...requestOptions }, + ); + + return { + queryKey, + queryFn, + enabled: !!(boardId && webhookId), + ...queryOptions, + } as UseQueryOptions< + Awaited< + ReturnType + >, + TError, + TData + > & { queryKey: DataTag }; + }; + +export type GetBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdGetQueryResult = + NonNullable< + Awaited< + ReturnType + > + >; +export type GetBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdGetQueryError = + HTTPValidationError; + +export function useGetBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdGet< + TData = Awaited< + ReturnType + >, + TError = HTTPValidationError, +>( + boardId: string, + webhookId: string, + options: { + query: Partial< + UseQueryOptions< + Awaited< + ReturnType< + typeof getBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdGet + > + >, + TError, + TData + > + > & + Pick< + DefinedInitialDataOptions< + Awaited< + ReturnType< + typeof getBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdGet + > + >, + TError, + Awaited< + ReturnType< + typeof getBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdGet + > + > + >, + "initialData" + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): DefinedUseQueryResult & { + queryKey: DataTag; +}; +export function useGetBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdGet< + TData = Awaited< + ReturnType + >, + TError = HTTPValidationError, +>( + boardId: string, + webhookId: string, + options?: { + query?: Partial< + UseQueryOptions< + Awaited< + ReturnType< + typeof getBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdGet + > + >, + TError, + TData + > + > & + Pick< + UndefinedInitialDataOptions< + Awaited< + ReturnType< + typeof getBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdGet + > + >, + TError, + Awaited< + ReturnType< + typeof getBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdGet + > + > + >, + "initialData" + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { + queryKey: DataTag; +}; +export function useGetBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdGet< + TData = Awaited< + ReturnType + >, + TError = HTTPValidationError, +>( + boardId: string, + webhookId: string, + options?: { + query?: Partial< + UseQueryOptions< + Awaited< + ReturnType< + typeof getBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdGet + > + >, + TError, + TData + > + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { + queryKey: DataTag; +}; +/** + * @summary Get Board Webhook + */ + +export function useGetBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdGet< + TData = Awaited< + ReturnType + >, + TError = HTTPValidationError, +>( + boardId: string, + webhookId: string, + options?: { + query?: Partial< + UseQueryOptions< + Awaited< + ReturnType< + typeof getBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdGet + > + >, + TError, + TData + > + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { + queryKey: DataTag; +} { + const queryOptions = + getGetBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdGetQueryOptions( + boardId, + webhookId, + options, + ); + + const query = useQuery(queryOptions, queryClient) as UseQueryResult< + TData, + TError + > & { queryKey: DataTag }; + + return { ...query, queryKey: queryOptions.queryKey }; +} + +/** + * Update board webhook description or enabled state. + * @summary Update Board Webhook + */ +export type updateBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdPatchResponse200 = + { + data: BoardWebhookRead; + status: 200; + }; + +export type updateBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdPatchResponse422 = + { + data: HTTPValidationError; + status: 422; + }; + +export type updateBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdPatchResponseSuccess = + updateBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdPatchResponse200 & { + headers: Headers; + }; +export type updateBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdPatchResponseError = + updateBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdPatchResponse422 & { + headers: Headers; + }; + +export type updateBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdPatchResponse = + | updateBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdPatchResponseSuccess + | updateBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdPatchResponseError; + +export const getUpdateBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdPatchUrl = + (boardId: string, webhookId: string) => { + return `/api/v1/boards/${boardId}/webhooks/${webhookId}`; + }; + +export const updateBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdPatch = + async ( + boardId: string, + webhookId: string, + boardWebhookUpdate: BoardWebhookUpdate, + options?: RequestInit, + ): Promise => { + return customFetch( + getUpdateBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdPatchUrl( + boardId, + webhookId, + ), + { + ...options, + method: "PATCH", + headers: { "Content-Type": "application/json", ...options?.headers }, + body: JSON.stringify(boardWebhookUpdate), + }, + ); + }; + +export const getUpdateBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdPatchMutationOptions = + (options?: { + mutation?: UseMutationOptions< + Awaited< + ReturnType< + typeof updateBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdPatch + > + >, + TError, + { boardId: string; webhookId: string; data: BoardWebhookUpdate }, + TContext + >; + request?: SecondParameter; + }): UseMutationOptions< + Awaited< + ReturnType< + typeof updateBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdPatch + > + >, + TError, + { boardId: string; webhookId: string; data: BoardWebhookUpdate }, + TContext + > => { + const mutationKey = [ + "updateBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdPatch", + ]; + 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 updateBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdPatch + > + >, + { boardId: string; webhookId: string; data: BoardWebhookUpdate } + > = (props) => { + const { boardId, webhookId, data } = props ?? {}; + + return updateBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdPatch( + boardId, + webhookId, + data, + requestOptions, + ); + }; + + return { mutationFn, ...mutationOptions }; + }; + +export type UpdateBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdPatchMutationResult = + NonNullable< + Awaited< + ReturnType< + typeof updateBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdPatch + > + > + >; +export type UpdateBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdPatchMutationBody = + BoardWebhookUpdate; +export type UpdateBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdPatchMutationError = + HTTPValidationError; + +/** + * @summary Update Board Webhook + */ +export const useUpdateBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdPatch = < + TError = HTTPValidationError, + TContext = unknown, +>( + options?: { + mutation?: UseMutationOptions< + Awaited< + ReturnType< + typeof updateBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdPatch + > + >, + TError, + { boardId: string; webhookId: string; data: BoardWebhookUpdate }, + TContext + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseMutationResult< + Awaited< + ReturnType< + typeof updateBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdPatch + > + >, + TError, + { boardId: string; webhookId: string; data: BoardWebhookUpdate }, + TContext +> => { + return useMutation( + getUpdateBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdPatchMutationOptions( + options, + ), + queryClient, + ); +}; +/** + * Open inbound webhook endpoint that stores payloads and nudges the board lead. + * @summary Ingest Board Webhook + */ +export type ingestBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdPostResponse202 = + { + data: BoardWebhookIngestResponse; + status: 202; + }; + +export type ingestBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdPostResponse422 = + { + data: HTTPValidationError; + status: 422; + }; + +export type ingestBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdPostResponseSuccess = + ingestBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdPostResponse202 & { + headers: Headers; + }; +export type ingestBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdPostResponseError = + ingestBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdPostResponse422 & { + headers: Headers; + }; + +export type ingestBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdPostResponse = + | ingestBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdPostResponseSuccess + | ingestBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdPostResponseError; + +export const getIngestBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdPostUrl = ( + boardId: string, + webhookId: string, +) => { + return `/api/v1/boards/${boardId}/webhooks/${webhookId}`; +}; + +export const ingestBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdPost = async ( + boardId: string, + webhookId: string, + options?: RequestInit, +): Promise => { + return customFetch( + getIngestBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdPostUrl( + boardId, + webhookId, + ), + { + ...options, + method: "POST", + }, + ); +}; + +export const getIngestBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdPostMutationOptions = + (options?: { + mutation?: UseMutationOptions< + Awaited< + ReturnType< + typeof ingestBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdPost + > + >, + TError, + { boardId: string; webhookId: string }, + TContext + >; + request?: SecondParameter; + }): UseMutationOptions< + Awaited< + ReturnType< + typeof ingestBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdPost + > + >, + TError, + { boardId: string; webhookId: string }, + TContext + > => { + const mutationKey = [ + "ingestBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdPost", + ]; + 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 ingestBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdPost + > + >, + { boardId: string; webhookId: string } + > = (props) => { + const { boardId, webhookId } = props ?? {}; + + return ingestBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdPost( + boardId, + webhookId, + requestOptions, + ); + }; + + return { mutationFn, ...mutationOptions }; + }; + +export type IngestBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdPostMutationResult = + NonNullable< + Awaited< + ReturnType< + typeof ingestBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdPost + > + > + >; + +export type IngestBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdPostMutationError = + HTTPValidationError; + +/** + * @summary Ingest Board Webhook + */ +export const useIngestBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdPost = < + TError = HTTPValidationError, + TContext = unknown, +>( + options?: { + mutation?: UseMutationOptions< + Awaited< + ReturnType< + typeof ingestBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdPost + > + >, + TError, + { boardId: string; webhookId: string }, + TContext + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseMutationResult< + Awaited< + ReturnType + >, + TError, + { boardId: string; webhookId: string }, + TContext +> => { + return useMutation( + getIngestBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdPostMutationOptions( + options, + ), + queryClient, + ); +}; +/** + * List stored payloads for one board webhook. + * @summary List Board Webhook Payloads + */ +export type listBoardWebhookPayloadsApiV1BoardsBoardIdWebhooksWebhookIdPayloadsGetResponse200 = + { + data: LimitOffsetPageTypeVarCustomizedBoardWebhookPayloadRead; + status: 200; + }; + +export type listBoardWebhookPayloadsApiV1BoardsBoardIdWebhooksWebhookIdPayloadsGetResponse422 = + { + data: HTTPValidationError; + status: 422; + }; + +export type listBoardWebhookPayloadsApiV1BoardsBoardIdWebhooksWebhookIdPayloadsGetResponseSuccess = + listBoardWebhookPayloadsApiV1BoardsBoardIdWebhooksWebhookIdPayloadsGetResponse200 & { + headers: Headers; + }; +export type listBoardWebhookPayloadsApiV1BoardsBoardIdWebhooksWebhookIdPayloadsGetResponseError = + listBoardWebhookPayloadsApiV1BoardsBoardIdWebhooksWebhookIdPayloadsGetResponse422 & { + headers: Headers; + }; + +export type listBoardWebhookPayloadsApiV1BoardsBoardIdWebhooksWebhookIdPayloadsGetResponse = + + | listBoardWebhookPayloadsApiV1BoardsBoardIdWebhooksWebhookIdPayloadsGetResponseSuccess + | listBoardWebhookPayloadsApiV1BoardsBoardIdWebhooksWebhookIdPayloadsGetResponseError; + +export const getListBoardWebhookPayloadsApiV1BoardsBoardIdWebhooksWebhookIdPayloadsGetUrl = + ( + boardId: string, + webhookId: string, + params?: ListBoardWebhookPayloadsApiV1BoardsBoardIdWebhooksWebhookIdPayloadsGetParams, + ) => { + 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}/webhooks/${webhookId}/payloads?${stringifiedParams}` + : `/api/v1/boards/${boardId}/webhooks/${webhookId}/payloads`; + }; + +export const listBoardWebhookPayloadsApiV1BoardsBoardIdWebhooksWebhookIdPayloadsGet = + async ( + boardId: string, + webhookId: string, + params?: ListBoardWebhookPayloadsApiV1BoardsBoardIdWebhooksWebhookIdPayloadsGetParams, + options?: RequestInit, + ): Promise => { + return customFetch( + getListBoardWebhookPayloadsApiV1BoardsBoardIdWebhooksWebhookIdPayloadsGetUrl( + boardId, + webhookId, + params, + ), + { + ...options, + method: "GET", + }, + ); + }; + +export const getListBoardWebhookPayloadsApiV1BoardsBoardIdWebhooksWebhookIdPayloadsGetQueryKey = + ( + boardId: string, + webhookId: string, + params?: ListBoardWebhookPayloadsApiV1BoardsBoardIdWebhooksWebhookIdPayloadsGetParams, + ) => { + return [ + `/api/v1/boards/${boardId}/webhooks/${webhookId}/payloads`, + ...(params ? [params] : []), + ] as const; + }; + +export const getListBoardWebhookPayloadsApiV1BoardsBoardIdWebhooksWebhookIdPayloadsGetQueryOptions = + < + TData = Awaited< + ReturnType< + typeof listBoardWebhookPayloadsApiV1BoardsBoardIdWebhooksWebhookIdPayloadsGet + > + >, + TError = HTTPValidationError, + >( + boardId: string, + webhookId: string, + params?: ListBoardWebhookPayloadsApiV1BoardsBoardIdWebhooksWebhookIdPayloadsGetParams, + options?: { + query?: Partial< + UseQueryOptions< + Awaited< + ReturnType< + typeof listBoardWebhookPayloadsApiV1BoardsBoardIdWebhooksWebhookIdPayloadsGet + > + >, + TError, + TData + > + >; + request?: SecondParameter; + }, + ) => { + const { query: queryOptions, request: requestOptions } = options ?? {}; + + const queryKey = + queryOptions?.queryKey ?? + getListBoardWebhookPayloadsApiV1BoardsBoardIdWebhooksWebhookIdPayloadsGetQueryKey( + boardId, + webhookId, + params, + ); + + const queryFn: QueryFunction< + Awaited< + ReturnType< + typeof listBoardWebhookPayloadsApiV1BoardsBoardIdWebhooksWebhookIdPayloadsGet + > + > + > = ({ signal }) => + listBoardWebhookPayloadsApiV1BoardsBoardIdWebhooksWebhookIdPayloadsGet( + boardId, + webhookId, + params, + { signal, ...requestOptions }, + ); + + return { + queryKey, + queryFn, + enabled: !!(boardId && webhookId), + ...queryOptions, + } as UseQueryOptions< + Awaited< + ReturnType< + typeof listBoardWebhookPayloadsApiV1BoardsBoardIdWebhooksWebhookIdPayloadsGet + > + >, + TError, + TData + > & { queryKey: DataTag }; + }; + +export type ListBoardWebhookPayloadsApiV1BoardsBoardIdWebhooksWebhookIdPayloadsGetQueryResult = + NonNullable< + Awaited< + ReturnType< + typeof listBoardWebhookPayloadsApiV1BoardsBoardIdWebhooksWebhookIdPayloadsGet + > + > + >; +export type ListBoardWebhookPayloadsApiV1BoardsBoardIdWebhooksWebhookIdPayloadsGetQueryError = + HTTPValidationError; + +export function useListBoardWebhookPayloadsApiV1BoardsBoardIdWebhooksWebhookIdPayloadsGet< + TData = Awaited< + ReturnType< + typeof listBoardWebhookPayloadsApiV1BoardsBoardIdWebhooksWebhookIdPayloadsGet + > + >, + TError = HTTPValidationError, +>( + boardId: string, + webhookId: string, + params: + | undefined + | ListBoardWebhookPayloadsApiV1BoardsBoardIdWebhooksWebhookIdPayloadsGetParams, + options: { + query: Partial< + UseQueryOptions< + Awaited< + ReturnType< + typeof listBoardWebhookPayloadsApiV1BoardsBoardIdWebhooksWebhookIdPayloadsGet + > + >, + TError, + TData + > + > & + Pick< + DefinedInitialDataOptions< + Awaited< + ReturnType< + typeof listBoardWebhookPayloadsApiV1BoardsBoardIdWebhooksWebhookIdPayloadsGet + > + >, + TError, + Awaited< + ReturnType< + typeof listBoardWebhookPayloadsApiV1BoardsBoardIdWebhooksWebhookIdPayloadsGet + > + > + >, + "initialData" + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): DefinedUseQueryResult & { + queryKey: DataTag; +}; +export function useListBoardWebhookPayloadsApiV1BoardsBoardIdWebhooksWebhookIdPayloadsGet< + TData = Awaited< + ReturnType< + typeof listBoardWebhookPayloadsApiV1BoardsBoardIdWebhooksWebhookIdPayloadsGet + > + >, + TError = HTTPValidationError, +>( + boardId: string, + webhookId: string, + params?: ListBoardWebhookPayloadsApiV1BoardsBoardIdWebhooksWebhookIdPayloadsGetParams, + options?: { + query?: Partial< + UseQueryOptions< + Awaited< + ReturnType< + typeof listBoardWebhookPayloadsApiV1BoardsBoardIdWebhooksWebhookIdPayloadsGet + > + >, + TError, + TData + > + > & + Pick< + UndefinedInitialDataOptions< + Awaited< + ReturnType< + typeof listBoardWebhookPayloadsApiV1BoardsBoardIdWebhooksWebhookIdPayloadsGet + > + >, + TError, + Awaited< + ReturnType< + typeof listBoardWebhookPayloadsApiV1BoardsBoardIdWebhooksWebhookIdPayloadsGet + > + > + >, + "initialData" + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { + queryKey: DataTag; +}; +export function useListBoardWebhookPayloadsApiV1BoardsBoardIdWebhooksWebhookIdPayloadsGet< + TData = Awaited< + ReturnType< + typeof listBoardWebhookPayloadsApiV1BoardsBoardIdWebhooksWebhookIdPayloadsGet + > + >, + TError = HTTPValidationError, +>( + boardId: string, + webhookId: string, + params?: ListBoardWebhookPayloadsApiV1BoardsBoardIdWebhooksWebhookIdPayloadsGetParams, + options?: { + query?: Partial< + UseQueryOptions< + Awaited< + ReturnType< + typeof listBoardWebhookPayloadsApiV1BoardsBoardIdWebhooksWebhookIdPayloadsGet + > + >, + TError, + TData + > + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { + queryKey: DataTag; +}; +/** + * @summary List Board Webhook Payloads + */ + +export function useListBoardWebhookPayloadsApiV1BoardsBoardIdWebhooksWebhookIdPayloadsGet< + TData = Awaited< + ReturnType< + typeof listBoardWebhookPayloadsApiV1BoardsBoardIdWebhooksWebhookIdPayloadsGet + > + >, + TError = HTTPValidationError, +>( + boardId: string, + webhookId: string, + params?: ListBoardWebhookPayloadsApiV1BoardsBoardIdWebhooksWebhookIdPayloadsGetParams, + options?: { + query?: Partial< + UseQueryOptions< + Awaited< + ReturnType< + typeof listBoardWebhookPayloadsApiV1BoardsBoardIdWebhooksWebhookIdPayloadsGet + > + >, + TError, + TData + > + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { + queryKey: DataTag; +} { + const queryOptions = + getListBoardWebhookPayloadsApiV1BoardsBoardIdWebhooksWebhookIdPayloadsGetQueryOptions( + boardId, + webhookId, + params, + options, + ); + + const query = useQuery(queryOptions, queryClient) as UseQueryResult< + TData, + TError + > & { queryKey: DataTag }; + + return { ...query, queryKey: queryOptions.queryKey }; +} + +/** + * Get a single stored payload for one board webhook. + * @summary Get Board Webhook Payload + */ +export type getBoardWebhookPayloadApiV1BoardsBoardIdWebhooksWebhookIdPayloadsPayloadIdGetResponse200 = + { + data: BoardWebhookPayloadRead; + status: 200; + }; + +export type getBoardWebhookPayloadApiV1BoardsBoardIdWebhooksWebhookIdPayloadsPayloadIdGetResponse422 = + { + data: HTTPValidationError; + status: 422; + }; + +export type getBoardWebhookPayloadApiV1BoardsBoardIdWebhooksWebhookIdPayloadsPayloadIdGetResponseSuccess = + getBoardWebhookPayloadApiV1BoardsBoardIdWebhooksWebhookIdPayloadsPayloadIdGetResponse200 & { + headers: Headers; + }; +export type getBoardWebhookPayloadApiV1BoardsBoardIdWebhooksWebhookIdPayloadsPayloadIdGetResponseError = + getBoardWebhookPayloadApiV1BoardsBoardIdWebhooksWebhookIdPayloadsPayloadIdGetResponse422 & { + headers: Headers; + }; + +export type getBoardWebhookPayloadApiV1BoardsBoardIdWebhooksWebhookIdPayloadsPayloadIdGetResponse = + + | getBoardWebhookPayloadApiV1BoardsBoardIdWebhooksWebhookIdPayloadsPayloadIdGetResponseSuccess + | getBoardWebhookPayloadApiV1BoardsBoardIdWebhooksWebhookIdPayloadsPayloadIdGetResponseError; + +export const getGetBoardWebhookPayloadApiV1BoardsBoardIdWebhooksWebhookIdPayloadsPayloadIdGetUrl = + (boardId: string, webhookId: string, payloadId: string) => { + return `/api/v1/boards/${boardId}/webhooks/${webhookId}/payloads/${payloadId}`; + }; + +export const getBoardWebhookPayloadApiV1BoardsBoardIdWebhooksWebhookIdPayloadsPayloadIdGet = + async ( + boardId: string, + webhookId: string, + payloadId: string, + options?: RequestInit, + ): Promise => { + return customFetch( + getGetBoardWebhookPayloadApiV1BoardsBoardIdWebhooksWebhookIdPayloadsPayloadIdGetUrl( + boardId, + webhookId, + payloadId, + ), + { + ...options, + method: "GET", + }, + ); + }; + +export const getGetBoardWebhookPayloadApiV1BoardsBoardIdWebhooksWebhookIdPayloadsPayloadIdGetQueryKey = + (boardId: string, webhookId: string, payloadId: string) => { + return [ + `/api/v1/boards/${boardId}/webhooks/${webhookId}/payloads/${payloadId}`, + ] as const; + }; + +export const getGetBoardWebhookPayloadApiV1BoardsBoardIdWebhooksWebhookIdPayloadsPayloadIdGetQueryOptions = + < + TData = Awaited< + ReturnType< + typeof getBoardWebhookPayloadApiV1BoardsBoardIdWebhooksWebhookIdPayloadsPayloadIdGet + > + >, + TError = HTTPValidationError, + >( + boardId: string, + webhookId: string, + payloadId: string, + options?: { + query?: Partial< + UseQueryOptions< + Awaited< + ReturnType< + typeof getBoardWebhookPayloadApiV1BoardsBoardIdWebhooksWebhookIdPayloadsPayloadIdGet + > + >, + TError, + TData + > + >; + request?: SecondParameter; + }, + ) => { + const { query: queryOptions, request: requestOptions } = options ?? {}; + + const queryKey = + queryOptions?.queryKey ?? + getGetBoardWebhookPayloadApiV1BoardsBoardIdWebhooksWebhookIdPayloadsPayloadIdGetQueryKey( + boardId, + webhookId, + payloadId, + ); + + const queryFn: QueryFunction< + Awaited< + ReturnType< + typeof getBoardWebhookPayloadApiV1BoardsBoardIdWebhooksWebhookIdPayloadsPayloadIdGet + > + > + > = ({ signal }) => + getBoardWebhookPayloadApiV1BoardsBoardIdWebhooksWebhookIdPayloadsPayloadIdGet( + boardId, + webhookId, + payloadId, + { signal, ...requestOptions }, + ); + + return { + queryKey, + queryFn, + enabled: !!(boardId && webhookId && payloadId), + ...queryOptions, + } as UseQueryOptions< + Awaited< + ReturnType< + typeof getBoardWebhookPayloadApiV1BoardsBoardIdWebhooksWebhookIdPayloadsPayloadIdGet + > + >, + TError, + TData + > & { queryKey: DataTag }; + }; + +export type GetBoardWebhookPayloadApiV1BoardsBoardIdWebhooksWebhookIdPayloadsPayloadIdGetQueryResult = + NonNullable< + Awaited< + ReturnType< + typeof getBoardWebhookPayloadApiV1BoardsBoardIdWebhooksWebhookIdPayloadsPayloadIdGet + > + > + >; +export type GetBoardWebhookPayloadApiV1BoardsBoardIdWebhooksWebhookIdPayloadsPayloadIdGetQueryError = + HTTPValidationError; + +export function useGetBoardWebhookPayloadApiV1BoardsBoardIdWebhooksWebhookIdPayloadsPayloadIdGet< + TData = Awaited< + ReturnType< + typeof getBoardWebhookPayloadApiV1BoardsBoardIdWebhooksWebhookIdPayloadsPayloadIdGet + > + >, + TError = HTTPValidationError, +>( + boardId: string, + webhookId: string, + payloadId: string, + options: { + query: Partial< + UseQueryOptions< + Awaited< + ReturnType< + typeof getBoardWebhookPayloadApiV1BoardsBoardIdWebhooksWebhookIdPayloadsPayloadIdGet + > + >, + TError, + TData + > + > & + Pick< + DefinedInitialDataOptions< + Awaited< + ReturnType< + typeof getBoardWebhookPayloadApiV1BoardsBoardIdWebhooksWebhookIdPayloadsPayloadIdGet + > + >, + TError, + Awaited< + ReturnType< + typeof getBoardWebhookPayloadApiV1BoardsBoardIdWebhooksWebhookIdPayloadsPayloadIdGet + > + > + >, + "initialData" + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): DefinedUseQueryResult & { + queryKey: DataTag; +}; +export function useGetBoardWebhookPayloadApiV1BoardsBoardIdWebhooksWebhookIdPayloadsPayloadIdGet< + TData = Awaited< + ReturnType< + typeof getBoardWebhookPayloadApiV1BoardsBoardIdWebhooksWebhookIdPayloadsPayloadIdGet + > + >, + TError = HTTPValidationError, +>( + boardId: string, + webhookId: string, + payloadId: string, + options?: { + query?: Partial< + UseQueryOptions< + Awaited< + ReturnType< + typeof getBoardWebhookPayloadApiV1BoardsBoardIdWebhooksWebhookIdPayloadsPayloadIdGet + > + >, + TError, + TData + > + > & + Pick< + UndefinedInitialDataOptions< + Awaited< + ReturnType< + typeof getBoardWebhookPayloadApiV1BoardsBoardIdWebhooksWebhookIdPayloadsPayloadIdGet + > + >, + TError, + Awaited< + ReturnType< + typeof getBoardWebhookPayloadApiV1BoardsBoardIdWebhooksWebhookIdPayloadsPayloadIdGet + > + > + >, + "initialData" + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { + queryKey: DataTag; +}; +export function useGetBoardWebhookPayloadApiV1BoardsBoardIdWebhooksWebhookIdPayloadsPayloadIdGet< + TData = Awaited< + ReturnType< + typeof getBoardWebhookPayloadApiV1BoardsBoardIdWebhooksWebhookIdPayloadsPayloadIdGet + > + >, + TError = HTTPValidationError, +>( + boardId: string, + webhookId: string, + payloadId: string, + options?: { + query?: Partial< + UseQueryOptions< + Awaited< + ReturnType< + typeof getBoardWebhookPayloadApiV1BoardsBoardIdWebhooksWebhookIdPayloadsPayloadIdGet + > + >, + TError, + TData + > + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { + queryKey: DataTag; +}; +/** + * @summary Get Board Webhook Payload + */ + +export function useGetBoardWebhookPayloadApiV1BoardsBoardIdWebhooksWebhookIdPayloadsPayloadIdGet< + TData = Awaited< + ReturnType< + typeof getBoardWebhookPayloadApiV1BoardsBoardIdWebhooksWebhookIdPayloadsPayloadIdGet + > + >, + TError = HTTPValidationError, +>( + boardId: string, + webhookId: string, + payloadId: string, + options?: { + query?: Partial< + UseQueryOptions< + Awaited< + ReturnType< + typeof getBoardWebhookPayloadApiV1BoardsBoardIdWebhooksWebhookIdPayloadsPayloadIdGet + > + >, + TError, + TData + > + >; + request?: SecondParameter; + }, + queryClient?: QueryClient, +): UseQueryResult & { + queryKey: DataTag; +} { + const queryOptions = + getGetBoardWebhookPayloadApiV1BoardsBoardIdWebhooksWebhookIdPayloadsPayloadIdGetQueryOptions( + boardId, + webhookId, + payloadId, + options, + ); + + const query = useQuery(queryOptions, queryClient) as UseQueryResult< + TData, + TError + > & { queryKey: DataTag }; + + return { ...query, queryKey: queryOptions.queryKey }; +} diff --git a/frontend/src/api/generated/model/boardWebhookCreate.ts b/frontend/src/api/generated/model/boardWebhookCreate.ts new file mode 100644 index 00000000..9514bc17 --- /dev/null +++ b/frontend/src/api/generated/model/boardWebhookCreate.ts @@ -0,0 +1,15 @@ +/** + * Generated by orval v8.3.0 🍺 + * Do not edit manually. + * Mission Control API + * OpenAPI spec version: 0.1.0 + */ + +/** + * Payload for creating a board webhook. + */ +export interface BoardWebhookCreate { + /** @minLength 1 */ + description: string; + enabled?: boolean; +} diff --git a/frontend/src/api/generated/model/boardWebhookIngestResponse.ts b/frontend/src/api/generated/model/boardWebhookIngestResponse.ts new file mode 100644 index 00000000..806dcb4c --- /dev/null +++ b/frontend/src/api/generated/model/boardWebhookIngestResponse.ts @@ -0,0 +1,16 @@ +/** + * Generated by orval v8.3.0 🍺 + * Do not edit manually. + * Mission Control API + * OpenAPI spec version: 0.1.0 + */ + +/** + * Response payload for inbound webhook ingestion. + */ +export interface BoardWebhookIngestResponse { + board_id: string; + ok?: boolean; + payload_id: string; + webhook_id: string; +} diff --git a/frontend/src/api/generated/model/boardWebhookPayloadRead.ts b/frontend/src/api/generated/model/boardWebhookPayloadRead.ts new file mode 100644 index 00000000..846b388c --- /dev/null +++ b/frontend/src/api/generated/model/boardWebhookPayloadRead.ts @@ -0,0 +1,22 @@ +/** + * Generated by orval v8.3.0 🍺 + * Do not edit manually. + * Mission Control API + * OpenAPI spec version: 0.1.0 + */ +import type { BoardWebhookPayloadReadHeaders } from "./boardWebhookPayloadReadHeaders"; +import type { BoardWebhookPayloadReadPayload } from "./boardWebhookPayloadReadPayload"; + +/** + * Serialized stored webhook payload. + */ +export interface BoardWebhookPayloadRead { + board_id: string; + content_type?: string | null; + headers?: BoardWebhookPayloadReadHeaders; + id: string; + payload?: BoardWebhookPayloadReadPayload; + received_at: string; + source_ip?: string | null; + webhook_id: string; +} diff --git a/frontend/src/api/generated/model/boardWebhookPayloadReadHeaders.ts b/frontend/src/api/generated/model/boardWebhookPayloadReadHeaders.ts new file mode 100644 index 00000000..5d2d2f62 --- /dev/null +++ b/frontend/src/api/generated/model/boardWebhookPayloadReadHeaders.ts @@ -0,0 +1,8 @@ +/** + * Generated by orval v8.3.0 🍺 + * Do not edit manually. + * Mission Control API + * OpenAPI spec version: 0.1.0 + */ + +export type BoardWebhookPayloadReadHeaders = { [key: string]: string } | null; diff --git a/frontend/src/api/generated/model/boardWebhookPayloadReadPayload.ts b/frontend/src/api/generated/model/boardWebhookPayloadReadPayload.ts new file mode 100644 index 00000000..d4bea611 --- /dev/null +++ b/frontend/src/api/generated/model/boardWebhookPayloadReadPayload.ts @@ -0,0 +1,14 @@ +/** + * Generated by orval v8.3.0 🍺 + * Do not edit manually. + * Mission Control API + * OpenAPI spec version: 0.1.0 + */ + +export type BoardWebhookPayloadReadPayload = + | { [key: string]: unknown } + | unknown[] + | string + | number + | boolean + | null; diff --git a/frontend/src/api/generated/model/boardWebhookRead.ts b/frontend/src/api/generated/model/boardWebhookRead.ts new file mode 100644 index 00000000..f07ce768 --- /dev/null +++ b/frontend/src/api/generated/model/boardWebhookRead.ts @@ -0,0 +1,20 @@ +/** + * Generated by orval v8.3.0 🍺 + * Do not edit manually. + * Mission Control API + * OpenAPI spec version: 0.1.0 + */ + +/** + * Serialized board webhook configuration. + */ +export interface BoardWebhookRead { + board_id: string; + created_at: string; + description: string; + enabled: boolean; + endpoint_path: string; + endpoint_url?: string | null; + id: string; + updated_at: string; +} diff --git a/frontend/src/api/generated/model/boardWebhookUpdate.ts b/frontend/src/api/generated/model/boardWebhookUpdate.ts new file mode 100644 index 00000000..46e27c5c --- /dev/null +++ b/frontend/src/api/generated/model/boardWebhookUpdate.ts @@ -0,0 +1,14 @@ +/** + * Generated by orval v8.3.0 🍺 + * Do not edit manually. + * Mission Control API + * OpenAPI spec version: 0.1.0 + */ + +/** + * Payload for updating a board webhook. + */ +export interface BoardWebhookUpdate { + description?: string | null; + enabled?: boolean | null; +} diff --git a/frontend/src/api/generated/model/index.ts b/frontend/src/api/generated/model/index.ts index 72807aba..2ec29e20 100644 --- a/frontend/src/api/generated/model/index.ts +++ b/frontend/src/api/generated/model/index.ts @@ -64,6 +64,13 @@ export * from "./boardReadSuccessMetrics"; export * from "./boardSnapshot"; export * from "./boardUpdate"; export * from "./boardUpdateSuccessMetrics"; +export * from "./boardWebhookCreate"; +export * from "./boardWebhookIngestResponse"; +export * from "./boardWebhookPayloadRead"; +export * from "./boardWebhookPayloadReadHeaders"; +export * from "./boardWebhookPayloadReadPayload"; +export * from "./boardWebhookRead"; +export * from "./boardWebhookUpdate"; export * from "./dashboardKpis"; export * from "./dashboardMetrics"; export * from "./dashboardMetricsApiV1MetricsDashboardGetParams"; @@ -115,6 +122,8 @@ export * from "./limitOffsetPageTypeVarCustomizedBoardGroupMemoryRead"; export * from "./limitOffsetPageTypeVarCustomizedBoardGroupRead"; export * from "./limitOffsetPageTypeVarCustomizedBoardMemoryRead"; export * from "./limitOffsetPageTypeVarCustomizedBoardRead"; +export * from "./limitOffsetPageTypeVarCustomizedBoardWebhookPayloadRead"; +export * from "./limitOffsetPageTypeVarCustomizedBoardWebhookRead"; export * from "./limitOffsetPageTypeVarCustomizedGatewayRead"; export * from "./limitOffsetPageTypeVarCustomizedOrganizationInviteRead"; export * from "./limitOffsetPageTypeVarCustomizedOrganizationMemberRead"; @@ -133,6 +142,8 @@ export * from "./listBoardMemoryApiV1AgentBoardsBoardIdMemoryGetParams"; export * from "./listBoardMemoryApiV1BoardsBoardIdMemoryGetParams"; export * from "./listBoardsApiV1AgentBoardsGetParams"; export * from "./listBoardsApiV1BoardsGetParams"; +export * from "./listBoardWebhookPayloadsApiV1BoardsBoardIdWebhooksWebhookIdPayloadsGetParams"; +export * from "./listBoardWebhooksApiV1BoardsBoardIdWebhooksGetParams"; export * from "./listGatewaysApiV1GatewaysGetParams"; export * from "./listGatewaySessionsApiV1GatewaysSessionsGetParams"; export * from "./listOrgInvitesApiV1OrganizationsMeInvitesGetParams"; diff --git a/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedBoardWebhookPayloadRead.ts b/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedBoardWebhookPayloadRead.ts new file mode 100644 index 00000000..e9fe9f71 --- /dev/null +++ b/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedBoardWebhookPayloadRead.ts @@ -0,0 +1,17 @@ +/** + * Generated by orval v8.3.0 🍺 + * Do not edit manually. + * Mission Control API + * OpenAPI spec version: 0.1.0 + */ +import type { BoardWebhookPayloadRead } from "./boardWebhookPayloadRead"; + +export interface LimitOffsetPageTypeVarCustomizedBoardWebhookPayloadRead { + items: BoardWebhookPayloadRead[]; + /** @minimum 1 */ + limit: number; + /** @minimum 0 */ + offset: number; + /** @minimum 0 */ + total: number; +} diff --git a/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedBoardWebhookRead.ts b/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedBoardWebhookRead.ts new file mode 100644 index 00000000..6bde11ec --- /dev/null +++ b/frontend/src/api/generated/model/limitOffsetPageTypeVarCustomizedBoardWebhookRead.ts @@ -0,0 +1,17 @@ +/** + * Generated by orval v8.3.0 🍺 + * Do not edit manually. + * Mission Control API + * OpenAPI spec version: 0.1.0 + */ +import type { BoardWebhookRead } from "./boardWebhookRead"; + +export interface LimitOffsetPageTypeVarCustomizedBoardWebhookRead { + items: BoardWebhookRead[]; + /** @minimum 1 */ + limit: number; + /** @minimum 0 */ + offset: number; + /** @minimum 0 */ + total: number; +} diff --git a/frontend/src/api/generated/model/listBoardWebhookPayloadsApiV1BoardsBoardIdWebhooksWebhookIdPayloadsGetParams.ts b/frontend/src/api/generated/model/listBoardWebhookPayloadsApiV1BoardsBoardIdWebhooksWebhookIdPayloadsGetParams.ts new file mode 100644 index 00000000..430eac1b --- /dev/null +++ b/frontend/src/api/generated/model/listBoardWebhookPayloadsApiV1BoardsBoardIdWebhooksWebhookIdPayloadsGetParams.ts @@ -0,0 +1,19 @@ +/** + * Generated by orval v8.3.0 🍺 + * Do not edit manually. + * Mission Control API + * OpenAPI spec version: 0.1.0 + */ + +export type ListBoardWebhookPayloadsApiV1BoardsBoardIdWebhooksWebhookIdPayloadsGetParams = + { + /** + * @minimum 1 + * @maximum 200 + */ + limit?: number; + /** + * @minimum 0 + */ + offset?: number; + }; diff --git a/frontend/src/api/generated/model/listBoardWebhooksApiV1BoardsBoardIdWebhooksGetParams.ts b/frontend/src/api/generated/model/listBoardWebhooksApiV1BoardsBoardIdWebhooksGetParams.ts new file mode 100644 index 00000000..e260e498 --- /dev/null +++ b/frontend/src/api/generated/model/listBoardWebhooksApiV1BoardsBoardIdWebhooksGetParams.ts @@ -0,0 +1,18 @@ +/** + * Generated by orval v8.3.0 🍺 + * Do not edit manually. + * Mission Control API + * OpenAPI spec version: 0.1.0 + */ + +export type ListBoardWebhooksApiV1BoardsBoardIdWebhooksGetParams = { + /** + * @minimum 1 + * @maximum 200 + */ + limit?: number; + /** + * @minimum 0 + */ + offset?: number; +}; diff --git a/frontend/src/app/boards/[boardId]/edit/page.tsx b/frontend/src/app/boards/[boardId]/edit/page.tsx index 2320c24b..97112cb3 100644 --- a/frontend/src/app/boards/[boardId]/edit/page.tsx +++ b/frontend/src/app/boards/[boardId]/edit/page.tsx @@ -7,6 +7,7 @@ import { useParams, useRouter, useSearchParams } from "next/navigation"; import { useAuth } from "@/auth/clerk"; import { X } from "lucide-react"; +import { useQueryClient } from "@tanstack/react-query"; import { ApiError } from "@/api/mutator"; import { @@ -14,6 +15,14 @@ import { useGetBoardApiV1BoardsBoardIdGet, useUpdateBoardApiV1BoardsBoardIdPatch, } from "@/api/generated/boards/boards"; +import { + getListBoardWebhooksApiV1BoardsBoardIdWebhooksGetQueryKey, + type listBoardWebhooksApiV1BoardsBoardIdWebhooksGetResponse, + useCreateBoardWebhookApiV1BoardsBoardIdWebhooksPost, + useDeleteBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdDelete, + useListBoardWebhooksApiV1BoardsBoardIdWebhooksGet, + useUpdateBoardWebhookApiV1BoardsBoardIdWebhooksWebhookIdPatch, +} from "@/api/generated/board-webhooks/board-webhooks"; import { type listBoardGroupsApiV1BoardGroupsGetResponse, useListBoardGroupsApiV1BoardGroupsGet, @@ -25,6 +34,7 @@ import { import { useOrganizationMembership } from "@/lib/use-organization-membership"; import type { BoardGroupRead, + BoardWebhookRead, BoardRead, BoardUpdate, } from "@/api/generated/model"; @@ -51,8 +61,147 @@ const slugify = (value: string) => .replace(/[^a-z0-9]+/g, "-") .replace(/(^-|-$)/g, "") || "board"; +type WebhookCardProps = { + webhook: BoardWebhookRead; + isLoading: boolean; + isWebhookCreating: boolean; + isDeletingWebhook: boolean; + isUpdatingWebhook: boolean; + copiedWebhookId: string | null; + onCopy: (webhook: BoardWebhookRead) => void; + onDelete: (webhookId: string) => void; + onViewPayloads: (webhookId: string) => void; + onUpdate: (webhookId: string, description: string) => Promise; +}; + +function WebhookCard({ + webhook, + isLoading, + isWebhookCreating, + isDeletingWebhook, + isUpdatingWebhook, + copiedWebhookId, + onCopy, + onDelete, + onViewPayloads, + onUpdate, +}: WebhookCardProps) { + const [isEditing, setIsEditing] = useState(false); + const [draftDescription, setDraftDescription] = useState(webhook.description); + + const isBusy = + isLoading || isWebhookCreating || isDeletingWebhook || isUpdatingWebhook; + const trimmedDescription = draftDescription.trim(); + const isDescriptionChanged = + trimmedDescription !== webhook.description.trim(); + + const handleSave = async () => { + if (!trimmedDescription) return; + if (!isDescriptionChanged) { + setIsEditing(false); + return; + } + const saved = await onUpdate(webhook.id, trimmedDescription); + if (saved) { + setIsEditing(false); + } + }; + + return ( +
+
+ + Webhook {webhook.id.slice(0, 8)} + +
+ + + {isEditing ? ( + <> + + + + ) : ( + <> + + + + )} +
+
+ {isEditing ? ( +