Phase 5: Tasks API + Kanban board + Card UI

This commit is contained in:
ochenstarik-ui 2026-07-20 22:11:50 +07:00
parent 3805caaa30
commit a73f1b143b
6 changed files with 113 additions and 29 deletions

32
backend/api/tasks.py Normal file
View file

@ -0,0 +1,32 @@
from fastapi import APIRouter
from typing import List
from models import Task
router = APIRouter(prefix="/api/v1", tags=["tasks"])
tasks_db: List[Task] = [
Task(id="t1", title="Реализовать EMA-кроссовер", description="Бэктест стратегии на Brent", status="in_progress", priority="high", assignee_agent_id="worker-code", project_id="p1"),
Task(id="t2", title="Подготовить отчёт", description="Результаты бэктеста за Q3", status="todo", priority="medium", project_id="p1"),
Task(id="t3", title="Настроить CI/CD", description="GitHub Actions для автотестов", status="review", priority="high", assignee_agent_id="worker-fast", project_id="p1"),
Task(id="t4", title="Обновить вики", description="Документация по API", status="done", priority="low", project_id="p1"),
Task(id="t5", title="Добавить OAuth", description="GitHub + Google интеграция", status="todo", priority="medium", project_id="p1"),
]
@router.get("/tasks", response_model=List[Task])
async def get_tasks():
return tasks_db
@router.post("/tasks", response_model=Task)
async def create_task(task: Task):
tasks_db.append(task)
return task
@router.patch("/tasks/{task_id}")
async def update_task(task_id: str, update: dict):
for task in tasks_db:
if task.id == task_id:
for key, val in update.items():
if hasattr(task, key):
setattr(task, key, val)
return task
return {"error": "Task not found"}

View file

@ -8,7 +8,7 @@ from pathlib import Path
from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from api import runs, agents, memory, skills, connectors
from api import runs, agents, memory, skills, connectors, tasks
import asyncio, json
app = FastAPI(
@ -22,6 +22,7 @@ app.include_router(agents.router)
app.include_router(memory.router)
app.include_router(skills.router)
app.include_router(connectors.router)
app.include_router(tasks.router)
# WebSocket для real-time обновлений
@app.websocket("/ws")

View file

@ -11,3 +11,12 @@ class Agent(BaseModel):
class AgentUpdate(BaseModel):
status: Literal["online", "offline", "busy"]
class Task(BaseModel):
id: str
title: str
description: str = ""
status: Literal["todo", "in_progress", "review", "done"] = "todo"
priority: Literal["low", "medium", "high"] = "medium"
assignee_agent_id: Optional[str] = None
project_id: str = "default"

View file

@ -1,37 +1,36 @@
import { CheckCircle2, Circle, Plus } from 'lucide-react'
import { useState } from 'react'
import { TaskCard } from './TaskCard'
import { useQuery } from '@tanstack/react-query'
interface Task { id: string; title: string; done: boolean; project: string }
const columns = ['todo', 'in_progress', 'review', 'done']
export function Kanban() {
const [tasks] = useState<Task[]>([
{ id: '1', title: 'Реализовать EMA-кроссовер', done: true, project: 'Trading' },
{ id: '2', title: 'Настроить CI/CD пайплайн', done: false, project: 'ACC' },
{ id: '3', title: 'Добавить health-check эндпоинт', done: true, project: 'ACC' },
{ id: '4', title: 'Обновить документацию API', done: false, project: 'ACC' },
])
const { data: tasks = [] } = useQuery({
queryKey: ['tasks'],
queryFn: async () => {
const res = await fetch('/api/v1/tasks')
return res.json()
},
})
return (
<div>
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-semibold uppercase tracking-wider text-muted-foreground">Tasks</h3>
<button className="p-1.5 rounded-lg hover:bg-secondary text-muted-foreground hover:text-foreground transition-colors">
<Plus className="w-4 h-4" />
</button>
</div>
<div className="space-y-1.5">
{tasks.map(task => (
<div key={task.id} className="flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-secondary/50 transition-colors cursor-pointer group">
{task.done
? <CheckCircle2 className="w-4 h-4 text-emerald-500 shrink-0" />
: <Circle className="w-4 h-4 text-muted-foreground shrink-0" />
}
<span className={`text-sm flex-1 ${task.done ? 'line-through text-muted-foreground' : ''}`}>
{task.title}
</span>
<span className="text-xs text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity">
{task.project}
</span>
<h2 className="text-2xl font-semibold mb-6">Project Kanban</h2>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
{columns.map((status) => (
<div key={status} className="bg-card border border-border rounded-3xl p-4">
<h3 className="uppercase text-xs tracking-widest text-muted-foreground mb-4 px-1">
{status.replace('_', ' ')}
<span className="ml-2 text-zinc-600">
{tasks.filter((t: any) => t.status === status).length}
</span>
</h3>
<div className="space-y-3">
{tasks
.filter((t: any) => t.status === status)
.map((task: any) => (
<TaskCard key={task.id} task={task} />
))}
</div>
</div>
))}
</div>

View file

@ -0,0 +1,29 @@
import { Card } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
const statusColors: Record<string, string> = {
todo: 'bg-zinc-700',
in_progress: 'bg-blue-600',
review: 'bg-amber-600',
done: 'bg-emerald-600',
}
export function TaskCard({ task }: { task: any }) {
return (
<Card className="p-4 hover:shadow-lg transition-shadow cursor-pointer border-border hover:border-primary/30">
<div className="flex justify-between items-start">
<h4 className="font-medium text-sm">{task.title}</h4>
<Badge variant="outline">{task.status.replace('_', ' ')}</Badge>
</div>
{task.description && (
<p className="text-xs text-muted-foreground mt-2 line-clamp-2">{task.description}</p>
)}
{task.assignee_agent_id && (
<div className="mt-3 text-xs text-muted-foreground flex items-center gap-1">
<span className="w-2 h-2 rounded-full bg-emerald-500" />
{task.assignee_agent_id}
</div>
)}
</Card>
)
}

View file

@ -0,0 +1,14 @@
import { cn } from '@/lib/utils'
interface CardProps {
className?: string
children: React.ReactNode
}
export function Card({ className, children }: CardProps) {
return (
<div className={cn('bg-card border border-border rounded-2xl', className)}>
{children}
</div>
)
}