Files
hermes-workspace/src/lib/tasks-api.ts
Eric 4f177f9b8d feat(tasks): unify Workspace task board with Hermes Kanban backend (#311) (#348)
* wip(hermesworld): viral sprint checkpoint - landing rebuild + character pipeline scaffold

- standalone /hermes-world and /world routes bypass workspace shell
- root overlay leaks gated for landing + game surfaces
- character pipeline scaffolding (player/npc/glb-body components)
- canonical asset path public/assets/hermesworld/characters/
- docs: landing-page-spec, graphics-usability-plan, agora-believable-checklist, master-roadmap
- handoff at memory/goals/2026-05-05-hermesworld-viral-sprint/handoff.md

Local-only checkpoint. Not for upstream yet.

* feat(playground): persistent admin mode toggle with shield button

- Admin mode now persists via localStorage (key: hermes-playground-admin)
- Shield icon button in HUD (right rail, below focus toggle, md+)
- Click toggles admin panel and saves preference
- ?admin=1 URL param still works as override
- gitignore swarm worker scratch dirs

Mission: memory/swarm/missions/2026-05-05-pr-triage.md (5 swarm lanes dispatched on 19 open PRs, no-merge contract)

* feat(landing): add Play Now CTAs to HermesWorld landing

- Hero: Play Now (primary, gold), View on GitHub (demoted), Read Roadmap
- Header nav: Play badge (highlighted gold)
- Final CTA: Play Now (primary), GitHub + Roadmap (secondary)

All Play buttons go to /playground which mounts the title screen
(username + character customizer + Enter). Sets up the public-URL
deploy: hermes-world.ai → / serves landing → click Play → /playground.

* fix(tasks): use shared kanban backend

---------

Co-authored-by: Aurora release bot <release@outsourc-e.com>
2026-05-05 16:46:24 -04:00

147 lines
4.3 KiB
TypeScript

const BASE = '/api/claude-tasks'
export type TaskColumn = 'backlog' | 'todo' | 'in_progress' | 'review' | 'blocked' | 'done'
export type TaskPriority = 'high' | 'medium' | 'low'
export type ClaudeTask = {
id: string
title: string
description: string
column: TaskColumn
priority: TaskPriority
assignee: string | null
tags: Array<string>
due_date: string | null
position: number
created_by: string
created_at: string
updated_at: string
}
export type CreateTaskInput = {
title: string
description?: string
column?: TaskColumn
priority?: TaskPriority
assignee?: string | null
tags?: Array<string>
due_date?: string | null
created_by?: string
}
export type UpdateTaskInput = Partial<Omit<CreateTaskInput, 'created_by'>>
export type TaskAssignee = {
id: string
label: string
isHuman: boolean
}
export type AssigneesResponse = {
assignees: Array<TaskAssignee>
humanReviewer: string | null
}
export async function fetchAssignees(): Promise<AssigneesResponse> {
const res = await fetch('/api/claude-tasks-assignees')
if (!res.ok) return { assignees: [], humanReviewer: null }
return res.json()
}
export async function fetchTasks(params?: {
column?: TaskColumn
assignee?: string
priority?: TaskPriority
include_done?: boolean
}): Promise<Array<ClaudeTask>> {
const q = new URLSearchParams()
if (params?.column) q.set('column', params.column)
if (params?.assignee) q.set('assignee', params.assignee)
if (params?.priority) q.set('priority', params.priority)
if (params?.include_done) q.set('include_done', 'true')
const url = q.toString() ? `${BASE}?${q}` : BASE
const res = await fetch(url)
if (!res.ok) throw new Error(`Failed to fetch tasks: ${res.status}`)
const data = await res.json()
return data.tasks ?? []
}
export async function createTask(input: CreateTaskInput): Promise<ClaudeTask> {
const res = await fetch(BASE, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(input),
})
if (!res.ok) {
const body = await res.json().catch(() => ({}))
throw new Error(body.detail || `Failed to create task: ${res.status}`)
}
return (await res.json()).task
}
export async function updateTask(taskId: string, input: UpdateTaskInput): Promise<ClaudeTask> {
const res = await fetch(`${BASE}/${taskId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(input),
})
if (!res.ok) throw new Error(`Failed to update task: ${res.status}`)
return (await res.json()).task
}
export async function deleteTask(taskId: string): Promise<void> {
const res = await fetch(`${BASE}/${taskId}`, { method: 'DELETE' })
if (!res.ok) throw new Error(`Failed to delete task: ${res.status}`)
}
export async function moveTask(taskId: string, column: TaskColumn, movedBy = 'user'): Promise<ClaudeTask> {
const res = await fetch(`${BASE}/${taskId}?action=move`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ column, moved_by: movedBy }),
})
if (!res.ok) {
const body = await res.json().catch(() => ({}))
throw new Error(body.detail || `Failed to move task: ${res.status}`)
}
return (await res.json()).task
}
export const COLUMN_LABELS: Record<TaskColumn, string> = {
backlog: 'Triage',
todo: 'Ready',
in_progress: 'Running',
review: 'Review',
blocked: 'Blocked',
done: 'Done',
}
export const COLUMN_ORDER: Array<TaskColumn> = ['backlog', 'todo', 'in_progress', 'review', 'blocked', 'done']
export const PRIORITY_COLORS: Record<TaskPriority, string> = {
high: '#ef4444',
medium: '#f97316',
low: '#6b7280',
}
export const COLUMN_COLORS: Record<TaskColumn, string> = {
backlog: '#6b7280',
todo: '#3b82f6',
in_progress: '#f97316',
review: '#a855f7',
blocked: '#ef4444',
done: '#22c55e',
}
export function isOverdue(task: ClaudeTask): boolean {
if (!task.due_date) return false
// Parse YYYY-MM-DD manually to avoid UTC-vs-local offset issues.
// new Date("2026-04-02") parses as UTC midnight, which in EST is the
// previous evening — causing everything to appear one day early.
const [year, month, day] = task.due_date.split('-').map(Number)
const due = new Date(year, month - 1, day) // local midnight
const today = new Date()
today.setHours(0, 0, 0, 0)
return due < today
}