* 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>
147 lines
4.3 KiB
TypeScript
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
|
|
}
|