2026-05-13 01:32:38 +02:00
|
|
|
import { request, getApiKey, getBaseUrlValue } from '../client'
|
2026-05-08 11:32:47 +08:00
|
|
|
|
|
|
|
|
// ─── Types ──────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
export type KanbanTaskStatus = 'triage' | 'todo' | 'ready' | 'running' | 'blocked' | 'done' | 'archived'
|
|
|
|
|
|
|
|
|
|
export interface KanbanTask {
|
|
|
|
|
id: string
|
|
|
|
|
title: string
|
|
|
|
|
body: string | null
|
|
|
|
|
assignee: string | null
|
|
|
|
|
status: KanbanTaskStatus
|
|
|
|
|
priority: number
|
|
|
|
|
created_by: string | null
|
|
|
|
|
created_at: number
|
|
|
|
|
started_at: number | null
|
|
|
|
|
completed_at: number | null
|
|
|
|
|
workspace_kind: string
|
|
|
|
|
workspace_path: string | null
|
|
|
|
|
tenant: string | null
|
|
|
|
|
result: string | null
|
|
|
|
|
skills: string[] | null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface KanbanRun {
|
|
|
|
|
id: number
|
|
|
|
|
task_id: string
|
|
|
|
|
profile: string | null
|
|
|
|
|
status: string
|
|
|
|
|
outcome: string | null
|
|
|
|
|
summary: string | null
|
|
|
|
|
error: string | null
|
|
|
|
|
metadata: Record<string, unknown> | null
|
|
|
|
|
worker_pid: number | null
|
|
|
|
|
started_at: number
|
|
|
|
|
ended_at: number | null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface KanbanComment {
|
|
|
|
|
id: number
|
|
|
|
|
task_id: string
|
|
|
|
|
author: string
|
|
|
|
|
body: string
|
|
|
|
|
created_at: number
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface KanbanEvent {
|
|
|
|
|
id: number
|
|
|
|
|
task_id: string
|
|
|
|
|
kind: string
|
|
|
|
|
payload: Record<string, unknown> | null
|
|
|
|
|
created_at: number
|
|
|
|
|
run_id: number | null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface KanbanTaskMessage {
|
|
|
|
|
id: number | string
|
|
|
|
|
session_id: string
|
|
|
|
|
role: string
|
|
|
|
|
content: string
|
|
|
|
|
tool_call_id: string | null
|
|
|
|
|
tool_calls: any[] | null
|
|
|
|
|
tool_name: string | null
|
|
|
|
|
timestamp: number
|
|
|
|
|
token_count: number | null
|
|
|
|
|
finish_reason: string | null
|
|
|
|
|
reasoning: string | null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface KanbanTaskSession {
|
|
|
|
|
id: string
|
|
|
|
|
title: string | null
|
|
|
|
|
source: string
|
|
|
|
|
model: string
|
|
|
|
|
started_at: number
|
|
|
|
|
ended_at: number | null
|
|
|
|
|
messages: KanbanTaskMessage[]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface KanbanTaskDetail {
|
|
|
|
|
task: KanbanTask
|
|
|
|
|
latest_summary: string | null
|
|
|
|
|
session?: KanbanTaskSession
|
|
|
|
|
comments: KanbanComment[]
|
|
|
|
|
events: KanbanEvent[]
|
|
|
|
|
runs: KanbanRun[]
|
2026-05-13 01:32:38 +02:00
|
|
|
parents?: string[]
|
|
|
|
|
children?: string[]
|
2026-05-08 11:32:47 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface KanbanStats {
|
|
|
|
|
by_status: Record<string, number>
|
|
|
|
|
by_assignee: Record<string, number>
|
|
|
|
|
total: number
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface KanbanAssignee {
|
|
|
|
|
name: string
|
|
|
|
|
on_disk: boolean
|
|
|
|
|
counts: Record<string, number> | null
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 13:58:44 +02:00
|
|
|
export interface KanbanBoard {
|
|
|
|
|
slug: string
|
|
|
|
|
name: string
|
|
|
|
|
description: string
|
|
|
|
|
icon: string
|
|
|
|
|
color: string
|
|
|
|
|
created_at: number | null
|
|
|
|
|
archived: boolean
|
|
|
|
|
db_path?: string
|
|
|
|
|
is_current?: boolean
|
|
|
|
|
counts: Record<string, number>
|
|
|
|
|
total: number
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface KanbanBoardCreateRequest {
|
|
|
|
|
slug: string
|
|
|
|
|
name?: string
|
|
|
|
|
description?: string
|
|
|
|
|
icon?: string
|
|
|
|
|
color?: string
|
|
|
|
|
switchCurrent?: boolean
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-11 15:26:24 +02:00
|
|
|
export interface KanbanCapabilityStatus {
|
|
|
|
|
key: string
|
|
|
|
|
status: 'supported' | 'partial' | 'missing'
|
|
|
|
|
reason?: string
|
|
|
|
|
canonicalRoute?: string
|
|
|
|
|
canonicalCommand?: string
|
|
|
|
|
requiresBoard: boolean
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 13:58:44 +02:00
|
|
|
export interface KanbanCapabilities {
|
|
|
|
|
source: 'hermes-cli'
|
|
|
|
|
supports: Record<string, boolean>
|
|
|
|
|
missing: string[]
|
2026-05-11 15:26:24 +02:00
|
|
|
capabilities?: KanbanCapabilityStatus[]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface KanbanTaskLog {
|
|
|
|
|
task_id: string
|
|
|
|
|
path: string | null
|
|
|
|
|
exists: boolean
|
|
|
|
|
size_bytes: number
|
|
|
|
|
content: string
|
|
|
|
|
truncated: boolean
|
2026-05-10 13:58:44 +02:00
|
|
|
}
|
|
|
|
|
|
2026-05-08 11:32:47 +08:00
|
|
|
export interface KanbanCreateRequest {
|
|
|
|
|
title: string
|
|
|
|
|
body?: string
|
|
|
|
|
assignee?: string
|
|
|
|
|
priority?: number
|
|
|
|
|
tenant?: string
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 13:58:44 +02:00
|
|
|
export interface KanbanBoardOptions {
|
|
|
|
|
board?: string
|
|
|
|
|
}
|
2026-05-08 11:32:47 +08:00
|
|
|
|
2026-05-10 13:58:44 +02:00
|
|
|
export interface KanbanListOptions extends KanbanBoardOptions {
|
2026-05-08 11:32:47 +08:00
|
|
|
status?: string
|
|
|
|
|
assignee?: string
|
|
|
|
|
tenant?: string
|
2026-05-11 15:09:58 +02:00
|
|
|
includeArchived?: boolean
|
2026-05-10 13:58:44 +02:00
|
|
|
}
|
|
|
|
|
|
2026-05-11 15:26:24 +02:00
|
|
|
export interface KanbanCommentCreateRequest {
|
|
|
|
|
body: string
|
|
|
|
|
author?: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface KanbanTaskLogOptions extends KanbanBoardOptions {
|
|
|
|
|
tail?: number
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface KanbanDiagnosticsOptions extends KanbanBoardOptions {
|
|
|
|
|
task?: string
|
|
|
|
|
severity?: 'warning' | 'error' | 'critical'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface KanbanReclaimOptions extends KanbanBoardOptions {
|
|
|
|
|
reason?: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface KanbanReassignOptions extends KanbanBoardOptions {
|
|
|
|
|
reclaim?: boolean
|
|
|
|
|
reason?: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface KanbanSpecifyOptions extends KanbanBoardOptions {
|
|
|
|
|
author?: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface KanbanDispatchOptions extends KanbanBoardOptions {
|
|
|
|
|
dryRun?: boolean
|
|
|
|
|
max?: number
|
|
|
|
|
failureLimit?: number
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-13 01:32:38 +02:00
|
|
|
export interface KanbanLinkRequest {
|
|
|
|
|
parent_id: string
|
|
|
|
|
child_id: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface KanbanBulkUpdateRequest {
|
|
|
|
|
ids: string[]
|
|
|
|
|
status?: KanbanTaskStatus
|
|
|
|
|
assignee?: string | null
|
|
|
|
|
archive?: boolean
|
|
|
|
|
summary?: string
|
|
|
|
|
reason?: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface KanbanBulkTaskResult {
|
|
|
|
|
id: string
|
|
|
|
|
ok: boolean
|
|
|
|
|
error?: string
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 13:58:44 +02:00
|
|
|
function normalizedBoard(board?: string): string {
|
|
|
|
|
const trimmed = board?.trim()
|
|
|
|
|
return trimmed || 'default'
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 18:44:53 +08:00
|
|
|
function activeProfileName(): string | null {
|
|
|
|
|
try {
|
|
|
|
|
return localStorage.getItem('hermes_active_profile_name')
|
|
|
|
|
} catch {
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 13:58:44 +02:00
|
|
|
function appendQuery(path: string, params: URLSearchParams): string {
|
|
|
|
|
const qs = params.toString()
|
|
|
|
|
return qs ? `${path}?${qs}` : path
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function boardParams(board?: string): URLSearchParams {
|
2026-05-08 11:32:47 +08:00
|
|
|
const params = new URLSearchParams()
|
2026-05-10 13:58:44 +02:00
|
|
|
params.set('board', normalizedBoard(board))
|
|
|
|
|
return params
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-13 01:32:38 +02:00
|
|
|
function websocketProtocol(base?: string): string {
|
|
|
|
|
if (base) return base.startsWith('https') ? 'wss:' : 'ws:'
|
|
|
|
|
return location.protocol === 'https:' ? 'wss:' : 'ws:'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatHostForPort(hostname: string, port: number): string {
|
|
|
|
|
if (hostname.startsWith('[') && hostname.endsWith(']')) return `${hostname}:${port}`
|
|
|
|
|
return hostname.includes(':') ? `[${hostname}]:${port}` : `${hostname}:${port}`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function buildKanbanEventsWebSocketUrl(opts?: KanbanBoardOptions): string {
|
|
|
|
|
const base = getBaseUrlValue()
|
|
|
|
|
const params = boardParams(opts?.board)
|
|
|
|
|
const token = getApiKey()
|
|
|
|
|
if (token) params.set('token', token)
|
2026-05-23 18:44:53 +08:00
|
|
|
const profile = activeProfileName()
|
|
|
|
|
if (profile) params.set('profile', profile)
|
2026-05-13 01:32:38 +02:00
|
|
|
const path = `/api/hermes/kanban/events?${params.toString()}`
|
|
|
|
|
|
|
|
|
|
if (base) {
|
|
|
|
|
return `${websocketProtocol(base)}//${new URL(base).host}${path}`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const host = import.meta.env.DEV
|
|
|
|
|
? formatHostForPort(location.hostname, 8648)
|
|
|
|
|
: location.host
|
|
|
|
|
return `${websocketProtocol()}//${host}${path}`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function openKanbanEventStream(opts?: KanbanBoardOptions): WebSocket {
|
|
|
|
|
return new WebSocket(buildKanbanEventsWebSocketUrl(opts))
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 13:58:44 +02:00
|
|
|
// ─── API functions ───────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
export async function listBoards(opts?: { includeArchived?: boolean }): Promise<KanbanBoard[]> {
|
|
|
|
|
const params = new URLSearchParams()
|
|
|
|
|
if (opts?.includeArchived) params.set('includeArchived', 'true')
|
|
|
|
|
const res = await request<{ boards: KanbanBoard[] }>(appendQuery('/api/hermes/kanban/boards', params))
|
|
|
|
|
return res.boards
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function createBoard(data: KanbanBoardCreateRequest): Promise<KanbanBoard> {
|
|
|
|
|
const res = await request<{ board: KanbanBoard }>('/api/hermes/kanban/boards', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
body: JSON.stringify(data),
|
|
|
|
|
})
|
|
|
|
|
return res.board
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function archiveBoard(slug: string): Promise<{ ok: boolean }> {
|
|
|
|
|
return request<{ ok: boolean }>(`/api/hermes/kanban/boards/${encodeURIComponent(slug)}`, {
|
|
|
|
|
method: 'DELETE',
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function getCapabilities(): Promise<KanbanCapabilities> {
|
|
|
|
|
const res = await request<{ capabilities: KanbanCapabilities }>('/api/hermes/kanban/capabilities')
|
|
|
|
|
return res.capabilities
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function listTasks(opts?: KanbanListOptions): Promise<KanbanTask[]> {
|
|
|
|
|
const params = boardParams(opts?.board)
|
2026-05-08 11:32:47 +08:00
|
|
|
if (opts?.status) params.set('status', opts.status)
|
|
|
|
|
if (opts?.assignee) params.set('assignee', opts.assignee)
|
|
|
|
|
if (opts?.tenant) params.set('tenant', opts.tenant)
|
2026-05-11 15:09:58 +02:00
|
|
|
if (opts?.includeArchived) params.set('includeArchived', 'true')
|
2026-05-10 13:58:44 +02:00
|
|
|
const res = await request<{ tasks: KanbanTask[] }>(appendQuery('/api/hermes/kanban', params))
|
2026-05-08 11:32:47 +08:00
|
|
|
return res.tasks
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 13:58:44 +02:00
|
|
|
export async function getTask(id: string, opts?: KanbanBoardOptions): Promise<KanbanTaskDetail> {
|
|
|
|
|
return request<KanbanTaskDetail>(appendQuery(`/api/hermes/kanban/${encodeURIComponent(id)}`, boardParams(opts?.board)))
|
2026-05-08 11:32:47 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-10 13:58:44 +02:00
|
|
|
export async function createTask(data: KanbanCreateRequest, opts?: KanbanBoardOptions): Promise<KanbanTask> {
|
|
|
|
|
const res = await request<{ task: KanbanTask }>(appendQuery('/api/hermes/kanban', boardParams(opts?.board)), {
|
2026-05-08 11:32:47 +08:00
|
|
|
method: 'POST',
|
|
|
|
|
body: JSON.stringify(data),
|
|
|
|
|
})
|
|
|
|
|
return res.task
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 13:58:44 +02:00
|
|
|
export async function completeTasks(taskIds: string[], summary?: string, opts?: KanbanBoardOptions): Promise<{ ok: boolean }> {
|
|
|
|
|
return request<{ ok: boolean }>(appendQuery('/api/hermes/kanban/complete', boardParams(opts?.board)), {
|
2026-05-08 11:32:47 +08:00
|
|
|
method: 'POST',
|
|
|
|
|
body: JSON.stringify({ task_ids: taskIds, summary }),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 13:58:44 +02:00
|
|
|
export async function blockTask(taskId: string, reason: string, opts?: KanbanBoardOptions): Promise<{ ok: boolean }> {
|
|
|
|
|
return request<{ ok: boolean }>(appendQuery(`/api/hermes/kanban/${encodeURIComponent(taskId)}/block`, boardParams(opts?.board)), {
|
2026-05-08 11:32:47 +08:00
|
|
|
method: 'POST',
|
|
|
|
|
body: JSON.stringify({ reason }),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 13:58:44 +02:00
|
|
|
export async function unblockTasks(taskIds: string[], opts?: KanbanBoardOptions): Promise<{ ok: boolean }> {
|
|
|
|
|
return request<{ ok: boolean }>(appendQuery('/api/hermes/kanban/unblock', boardParams(opts?.board)), {
|
2026-05-08 11:32:47 +08:00
|
|
|
method: 'POST',
|
|
|
|
|
body: JSON.stringify({ task_ids: taskIds }),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 13:58:44 +02:00
|
|
|
export async function assignTask(taskId: string, profile: string, opts?: KanbanBoardOptions): Promise<{ ok: boolean }> {
|
|
|
|
|
return request<{ ok: boolean }>(appendQuery(`/api/hermes/kanban/${encodeURIComponent(taskId)}/assign`, boardParams(opts?.board)), {
|
2026-05-08 11:32:47 +08:00
|
|
|
method: 'POST',
|
|
|
|
|
body: JSON.stringify({ profile }),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-11 15:26:24 +02:00
|
|
|
export async function addComment(taskId: string, data: KanbanCommentCreateRequest, opts?: KanbanBoardOptions): Promise<{ ok: boolean; output?: string }> {
|
|
|
|
|
return request<{ ok: boolean; output?: string }>(appendQuery(`/api/hermes/kanban/${encodeURIComponent(taskId)}/comments`, boardParams(opts?.board)), {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
body: JSON.stringify(data),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-13 01:32:38 +02:00
|
|
|
export async function linkTasks(data: KanbanLinkRequest, opts?: KanbanBoardOptions): Promise<{ ok: boolean; output?: string }> {
|
|
|
|
|
return request<{ ok: boolean; output?: string }>(appendQuery('/api/hermes/kanban/links', boardParams(opts?.board)), {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
body: JSON.stringify(data),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function unlinkTasks(data: KanbanLinkRequest, opts?: KanbanBoardOptions): Promise<{ ok: boolean; output?: string }> {
|
|
|
|
|
const params = boardParams(opts?.board)
|
|
|
|
|
params.set('parent_id', data.parent_id)
|
|
|
|
|
params.set('child_id', data.child_id)
|
|
|
|
|
return request<{ ok: boolean; output?: string }>(appendQuery('/api/hermes/kanban/links', params), {
|
|
|
|
|
method: 'DELETE',
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function bulkUpdateTasks(data: KanbanBulkUpdateRequest, opts?: KanbanBoardOptions): Promise<{ results: KanbanBulkTaskResult[] }> {
|
|
|
|
|
return request<{ results: KanbanBulkTaskResult[] }>(appendQuery('/api/hermes/kanban/tasks/bulk', boardParams(opts?.board)), {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
body: JSON.stringify(data),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-11 15:26:24 +02:00
|
|
|
export async function getTaskLog(taskId: string, opts?: KanbanTaskLogOptions): Promise<KanbanTaskLog> {
|
|
|
|
|
const params = boardParams(opts?.board)
|
|
|
|
|
if (opts?.tail !== undefined) params.set('tail', String(opts.tail))
|
|
|
|
|
return request<KanbanTaskLog>(appendQuery(`/api/hermes/kanban/${encodeURIComponent(taskId)}/log`, params))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function getDiagnostics(opts?: KanbanDiagnosticsOptions): Promise<unknown[]> {
|
|
|
|
|
const params = boardParams(opts?.board)
|
|
|
|
|
if (opts?.task) params.set('task', opts.task)
|
|
|
|
|
if (opts?.severity) params.set('severity', opts.severity)
|
|
|
|
|
const res = await request<{ diagnostics: unknown[] }>(appendQuery('/api/hermes/kanban/diagnostics', params))
|
|
|
|
|
return res.diagnostics
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function reclaimTask(taskId: string, opts?: KanbanReclaimOptions): Promise<{ ok: boolean; output?: string }> {
|
|
|
|
|
return request<{ ok: boolean; output?: string }>(appendQuery(`/api/hermes/kanban/${encodeURIComponent(taskId)}/reclaim`, boardParams(opts?.board)), {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
body: JSON.stringify({ reason: opts?.reason }),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function reassignTask(taskId: string, profile: string, opts?: KanbanReassignOptions): Promise<{ ok: boolean; output?: string }> {
|
|
|
|
|
return request<{ ok: boolean; output?: string }>(appendQuery(`/api/hermes/kanban/${encodeURIComponent(taskId)}/reassign`, boardParams(opts?.board)), {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
body: JSON.stringify({ profile, reclaim: opts?.reclaim, reason: opts?.reason }),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function specifyTask(taskId: string, opts?: KanbanSpecifyOptions): Promise<unknown[]> {
|
|
|
|
|
const res = await request<{ results: unknown[] }>(appendQuery(`/api/hermes/kanban/${encodeURIComponent(taskId)}/specify`, boardParams(opts?.board)), {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
body: JSON.stringify({ author: opts?.author }),
|
|
|
|
|
})
|
|
|
|
|
return res.results
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function dispatch(opts?: KanbanDispatchOptions): Promise<unknown> {
|
|
|
|
|
const params = boardParams(opts?.board)
|
|
|
|
|
const res = await request<{ result: unknown }>(appendQuery('/api/hermes/kanban/dispatch', params), {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
body: JSON.stringify({ dryRun: opts?.dryRun, max: opts?.max, failureLimit: opts?.failureLimit }),
|
|
|
|
|
})
|
|
|
|
|
return res.result
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 13:58:44 +02:00
|
|
|
export async function getStats(opts?: KanbanBoardOptions): Promise<KanbanStats> {
|
|
|
|
|
const res = await request<{ stats: KanbanStats }>(appendQuery('/api/hermes/kanban/stats', boardParams(opts?.board)))
|
2026-05-08 11:32:47 +08:00
|
|
|
return res.stats
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 13:58:44 +02:00
|
|
|
export async function getAssignees(opts?: KanbanBoardOptions): Promise<KanbanAssignee[]> {
|
|
|
|
|
const res = await request<{ assignees: KanbanAssignee[] }>(appendQuery('/api/hermes/kanban/assignees', boardParams(opts?.board)))
|
2026-05-08 11:32:47 +08:00
|
|
|
return res.assignees
|
|
|
|
|
}
|