Phase 5: Tasks API + Kanban board + Card UI
This commit is contained in:
parent
3805caaa30
commit
a73f1b143b
6 changed files with 113 additions and 29 deletions
32
backend/api/tasks.py
Normal file
32
backend/api/tasks.py
Normal 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"}
|
||||||
|
|
@ -8,7 +8,7 @@ from pathlib import Path
|
||||||
from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect
|
from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.staticfiles import StaticFiles
|
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
|
import asyncio, json
|
||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
|
|
@ -22,6 +22,7 @@ app.include_router(agents.router)
|
||||||
app.include_router(memory.router)
|
app.include_router(memory.router)
|
||||||
app.include_router(skills.router)
|
app.include_router(skills.router)
|
||||||
app.include_router(connectors.router)
|
app.include_router(connectors.router)
|
||||||
|
app.include_router(tasks.router)
|
||||||
|
|
||||||
# WebSocket для real-time обновлений
|
# WebSocket для real-time обновлений
|
||||||
@app.websocket("/ws")
|
@app.websocket("/ws")
|
||||||
|
|
|
||||||
|
|
@ -11,3 +11,12 @@ class Agent(BaseModel):
|
||||||
|
|
||||||
class AgentUpdate(BaseModel):
|
class AgentUpdate(BaseModel):
|
||||||
status: Literal["online", "offline", "busy"]
|
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"
|
||||||
|
|
|
||||||
|
|
@ -1,37 +1,36 @@
|
||||||
import { CheckCircle2, Circle, Plus } from 'lucide-react'
|
import { TaskCard } from './TaskCard'
|
||||||
import { useState } from 'react'
|
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() {
|
export function Kanban() {
|
||||||
const [tasks] = useState<Task[]>([
|
const { data: tasks = [] } = useQuery({
|
||||||
{ id: '1', title: 'Реализовать EMA-кроссовер', done: true, project: 'Trading' },
|
queryKey: ['tasks'],
|
||||||
{ id: '2', title: 'Настроить CI/CD пайплайн', done: false, project: 'ACC' },
|
queryFn: async () => {
|
||||||
{ id: '3', title: 'Добавить health-check эндпоинт', done: true, project: 'ACC' },
|
const res = await fetch('/api/v1/tasks')
|
||||||
{ id: '4', title: 'Обновить документацию API', done: false, project: 'ACC' },
|
return res.json()
|
||||||
])
|
},
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between mb-4">
|
<h2 className="text-2xl font-semibold mb-6">Project Kanban</h2>
|
||||||
<h3 className="text-sm font-semibold uppercase tracking-wider text-muted-foreground">Tasks</h3>
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||||
<button className="p-1.5 rounded-lg hover:bg-secondary text-muted-foreground hover:text-foreground transition-colors">
|
{columns.map((status) => (
|
||||||
<Plus className="w-4 h-4" />
|
<div key={status} className="bg-card border border-border rounded-3xl p-4">
|
||||||
</button>
|
<h3 className="uppercase text-xs tracking-widest text-muted-foreground mb-4 px-1">
|
||||||
</div>
|
{status.replace('_', ' ')}
|
||||||
<div className="space-y-1.5">
|
<span className="ml-2 text-zinc-600">
|
||||||
{tasks.map(task => (
|
{tasks.filter((t: any) => t.status === status).length}
|
||||||
<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">
|
</span>
|
||||||
{task.done
|
</h3>
|
||||||
? <CheckCircle2 className="w-4 h-4 text-emerald-500 shrink-0" />
|
<div className="space-y-3">
|
||||||
: <Circle className="w-4 h-4 text-muted-foreground shrink-0" />
|
{tasks
|
||||||
}
|
.filter((t: any) => t.status === status)
|
||||||
<span className={`text-sm flex-1 ${task.done ? 'line-through text-muted-foreground' : ''}`}>
|
.map((task: any) => (
|
||||||
{task.title}
|
<TaskCard key={task.id} task={task} />
|
||||||
</span>
|
))}
|
||||||
<span className="text-xs text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity">
|
</div>
|
||||||
{task.project}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
29
frontend/src/components/tasks/TaskCard.tsx
Normal file
29
frontend/src/components/tasks/TaskCard.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
14
frontend/src/components/ui/card.tsx
Normal file
14
frontend/src/components/ui/card.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue