kin/web/frontend/src/api.ts

343 lines
11 KiB
TypeScript
Raw Normal View History

const BASE = '/api'
export class ApiError extends Error {
code: string
constructor(code: string, message: string) {
super(message)
this.name = 'ApiError'
this.code = code
}
}
async function throwApiError(res: Response): Promise<never> {
let code = ''
let msg = `${res.status} ${res.statusText}`
try {
const data = await res.json()
if (data.error) code = data.error
if (data.message) msg = data.message
} catch {}
throw new ApiError(code, msg)
}
async function get<T>(path: string): Promise<T> {
const res = await fetch(`${BASE}${path}`)
if (!res.ok) await throwApiError(res)
return res.json()
}
async function patch<T>(path: string, body: unknown): Promise<T> {
const res = await fetch(`${BASE}${path}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
if (!res.ok) await throwApiError(res)
return res.json()
}
async function post<T>(path: string, body: unknown): Promise<T> {
const res = await fetch(`${BASE}${path}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
if (!res.ok) await throwApiError(res)
return res.json()
}
async function del<T>(path: string): Promise<T> {
const res = await fetch(`${BASE}${path}`, { method: 'DELETE' })
if (!res.ok) await throwApiError(res)
if (res.status === 204) return undefined as T
return res.json()
}
export interface Project {
id: string
name: string
path: string
status: string
priority: number
tech_stack: string[] | null
execution_mode: string | null
autocommit_enabled: number | null
obsidian_vault_path: string | null
deploy_command: string | null
created_at: string
total_tasks: number
done_tasks: number
active_tasks: number
blocked_tasks: number
review_tasks: number
project_type: string | null
ssh_host: string | null
ssh_user: string | null
ssh_key_path: string | null
ssh_proxy_jump: string | null
description: string | null
}
export interface ObsidianSyncResult {
exported_decisions: number
tasks_updated: number
errors: string[]
vault_path: string
}
export interface ProjectDetail extends Project {
tasks: Task[]
modules: Module[]
decisions: Decision[]
}
export interface Task {
id: string
project_id: string
title: string
status: string
priority: number
assigned_role: string | null
parent_task_id: string | null
brief: Record<string, unknown> | null
spec: Record<string, unknown> | null
execution_mode: string | null
blocked_reason: string | null
dangerously_skipped: number | null
category: string | null
acceptance_criteria: string | null
created_at: string
updated_at: string
}
export interface Decision {
id: number
project_id: string
task_id: string | null
type: string
category: string | null
title: string
description: string
tags: string[] | null
created_at: string
}
export interface Module {
id: number
project_id: string
name: string
type: string
path: string
description: string | null
owner_role: string | null
dependencies: string[] | null
}
export interface PipelineStep {
id: number
agent_role: string
action: string
output_summary: string | null
success: boolean | number
duration_seconds: number | null
tokens_used: number | null
model: string | null
cost_usd: number | null
created_at: string
}
export interface DeployResult {
success: boolean
exit_code: number
stdout: string
stderr: string
duration_seconds: number
}
export interface TaskFull extends Task {
pipeline_steps: PipelineStep[]
related_decisions: Decision[]
project_deploy_command: string | null
}
export interface PendingAction {
type: string
description: string
original_item: Record<string, unknown>
options: string[]
}
export interface CostEntry {
project_id: string
project_name: string
runs: number
total_tokens: number
total_cost_usd: number
total_duration_seconds: number
}
export interface Phase {
id: number
project_id: string
role: string
phase_order: number
status: string
task_id: string | null
revise_count: number
kin: KIN-059 Workflow new_project с выбором команды. При создании нового проекта через GUI или CLI директор описывает проект свободным текстом и выбирает галочками какие этапы research нужны: ☐ Business analyst (бизнес-модель, аудитория, монетизация) ☐ Market researcher (конкуренты, ниша, отзывы, сильные/слабые стороны) ☐ Legal researcher (юрисдикция, лицензии, KYC/AML, GDPR) ☐ Tech researcher (API, ограничения, стоимость, альтернативы) ☐ UX designer (анализ UX конкурентов, user journey, wireframes) ☐ Marketer (стратегия продвижения, SEO, conversion-паттерны) ☐ Architect (blueprint на основе одобренных research'ей) — всегда последний Architect включается автоматически если выбран хотя бы один researcher. Каждый выбранный этап — отдельная задача на review. Директор одобряет, отклоняет, или просит доисследовать (Revise). Следующий этап только после approve предыдущего. GUI: форма 'New Project' с описанием + чекбоксы ролей + кнопка 'Start Research'. CLI: kin new-project 'описание' --roles 'business,market,tech,architect'
2026-03-16 09:30:00 +02:00
revise_comment: string | null
created_at: string
updated_at: string
task?: Task | null
}
export interface NewProjectPayload {
id: string
name: string
path: string
description: string
roles: string[]
tech_stack?: string[]
priority?: number
language?: string
project_type?: string
ssh_host?: string
ssh_user?: string
ssh_key_path?: string
ssh_proxy_jump?: string
}
export interface NewProjectResult {
project: Project
phases: Phase[]
}
export interface AuditItem {
id: string
reason: string
}
export interface AuditResult {
success: boolean
already_done: AuditItem[]
still_pending: AuditItem[]
unclear: AuditItem[]
duration_seconds?: number
cost_usd?: number
error?: string
}
export interface ProjectEnvironment {
id: number
project_id: string
name: string
host: string
port: number
username: string
auth_type: string
is_installed: number
created_at: string
updated_at: string
}
export interface EscalationNotification {
task_id: string
project_id: string
agent_role: string
reason: string
pipeline_step: string | null
blocked_at: string
telegram_sent: boolean
}
export interface ChatMessage {
id: number
project_id: string
role: 'user' | 'assistant'
content: string
message_type: string
task_id: string | null
created_at: string
task_stub?: {
id: string
title: string
status: string
} | null
}
export interface ChatSendResult {
user_message: ChatMessage
assistant_message: ChatMessage
task?: Task | null
}
export const api = {
projects: () => get<Project[]>('/projects'),
project: (id: string) => get<ProjectDetail>(`/projects/${id}`),
task: (id: string) => get<Task>(`/tasks/${id}`),
taskFull: (id: string) => get<TaskFull>(`/tasks/${id}/full`),
taskPipeline: (id: string) => get<PipelineStep[]>(`/tasks/${id}/pipeline`),
cost: (days = 7) => get<CostEntry[]>(`/cost?days=${days}`),
createProject: (data: { id: string; name: string; path?: string; tech_stack?: string[]; priority?: number; project_type?: string; ssh_host?: string; ssh_user?: string; ssh_key_path?: string; ssh_proxy_jump?: string }) =>
post<Project>('/projects', data),
createTask: (data: { project_id: string; title: string; priority?: number; route_type?: string; category?: string; acceptance_criteria?: string }) =>
post<Task>('/tasks', data),
approveTask: (id: string, data?: { decision_title?: string; decision_description?: string; decision_type?: string; create_followups?: boolean }) =>
post<{ status: string; followup_tasks: Task[]; needs_decision: boolean; pending_actions: PendingAction[] }>(`/tasks/${id}/approve`, data || {}),
resolveAction: (id: string, action: PendingAction, choice: string) =>
post<{ choice: string; result: unknown }>(`/tasks/${id}/resolve`, { action, choice }),
rejectTask: (id: string, reason: string) =>
post<{ status: string }>(`/tasks/${id}/reject`, { reason }),
reviseTask: (id: string, comment: string) =>
post<{ status: string; comment: string }>(`/tasks/${id}/revise`, { comment }),
runTask: (id: string) =>
post<{ status: string }>(`/tasks/${id}/run`, {}),
bootstrap: (data: { path: string; id: string; name: string }) =>
post<{ project: Project }>('/bootstrap', data),
auditProject: (projectId: string) =>
post<AuditResult>(`/projects/${projectId}/audit`, {}),
auditApply: (projectId: string, taskIds: string[]) =>
post<{ updated: string[]; count: number }>(`/projects/${projectId}/audit/apply`, { task_ids: taskIds }),
patchTask: (id: string, data: { status?: string; execution_mode?: string; priority?: number; route_type?: string; title?: string; brief_text?: string; acceptance_criteria?: string }) =>
patch<Task>(`/tasks/${id}`, data),
patchProject: (id: string, data: { execution_mode?: string; autocommit_enabled?: boolean; obsidian_vault_path?: string; deploy_command?: string; project_type?: string; ssh_host?: string; ssh_user?: string; ssh_key_path?: string; ssh_proxy_jump?: string }) =>
patch<Project>(`/projects/${id}`, data),
deployProject: (projectId: string) =>
post<DeployResult>(`/projects/${projectId}/deploy`, {}),
syncObsidian: (projectId: string) =>
post<ObsidianSyncResult>(`/projects/${projectId}/sync/obsidian`, {}),
deleteProject: (id: string) =>
del<void>(`/projects/${id}`),
deleteDecision: (projectId: string, decisionId: number) =>
del<{ deleted: number }>(`/projects/${projectId}/decisions/${decisionId}`),
createDecision: (data: { project_id: string; type: string; title: string; description: string; category?: string; tags?: string[] }) =>
post<Decision>('/decisions', data),
newProject: (data: NewProjectPayload) =>
post<NewProjectResult>('/projects/new', data),
getPhases: (projectId: string) =>
get<Phase[]>(`/projects/${projectId}/phases`),
notifications: (projectId?: string) =>
get<EscalationNotification[]>(`/notifications${projectId ? `?project_id=${projectId}` : ''}`),
approvePhase: (phaseId: number, comment?: string) =>
post<{ phase: Phase; next_phase: Phase | null }>(`/phases/${phaseId}/approve`, { comment }),
rejectPhase: (phaseId: number, reason: string) =>
post<Phase>(`/phases/${phaseId}/reject`, { reason }),
revisePhase: (phaseId: number, comment: string) =>
post<{ phase: Phase; new_task: Task }>(`/phases/${phaseId}/revise`, { comment }),
kin: KIN-059 Workflow new_project с выбором команды. При создании нового проекта через GUI или CLI директор описывает проект свободным текстом и выбирает галочками какие этапы research нужны: ☐ Business analyst (бизнес-модель, аудитория, монетизация) ☐ Market researcher (конкуренты, ниша, отзывы, сильные/слабые стороны) ☐ Legal researcher (юрисдикция, лицензии, KYC/AML, GDPR) ☐ Tech researcher (API, ограничения, стоимость, альтернативы) ☐ UX designer (анализ UX конкурентов, user journey, wireframes) ☐ Marketer (стратегия продвижения, SEO, conversion-паттерны) ☐ Architect (blueprint на основе одобренных research'ей) — всегда последний Architect включается автоматически если выбран хотя бы один researcher. Каждый выбранный этап — отдельная задача на review. Директор одобряет, отклоняет, или просит доисследовать (Revise). Следующий этап только после approve предыдущего. GUI: форма 'New Project' с описанием + чекбоксы ролей + кнопка 'Start Research'. CLI: kin new-project 'описание' --roles 'business,market,tech,architect'
2026-03-16 09:30:00 +02:00
startPhase: (projectId: string) =>
post<{ status: string; phase_id: number; task_id: string }>(`/projects/${projectId}/phases/start`, {}),
environments: (projectId: string) =>
get<ProjectEnvironment[]>(`/projects/${projectId}/environments`),
createEnvironment: (projectId: string, data: { name: string; host: string; port?: number; username: string; auth_type?: string; auth_value?: string; is_installed?: boolean }) =>
post<ProjectEnvironment & { scan_task_id?: string }>(`/projects/${projectId}/environments`, data),
updateEnvironment: (projectId: string, envId: number, data: { name?: string; host?: string; port?: number; username?: string; auth_type?: string; auth_value?: string; is_installed?: boolean }) =>
patch<ProjectEnvironment & { scan_task_id?: string }>(`/projects/${projectId}/environments/${envId}`, data),
deleteEnvironment: (projectId: string, envId: number) =>
del<void>(`/projects/${projectId}/environments/${envId}`),
scanEnvironment: (projectId: string, envId: number) =>
post<{ status: string; task_id: string }>(`/projects/${projectId}/environments/${envId}/scan`, {}),
chatHistory: (projectId: string, limit = 50) =>
get<ChatMessage[]>(`/projects/${projectId}/chat?limit=${limit}`),
sendChatMessage: (projectId: string, content: string) =>
post<ChatSendResult>(`/projects/${projectId}/chat`, { content }),
}