From f7ee6d1a7cb79bde3fa17370ac1fb17a558402bd Mon Sep 17 00:00:00 2001 From: markov Date: Tue, 24 Feb 2026 12:11:43 +0100 Subject: [PATCH] feat: system messages for task lifecycle (create, assign, status change) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tracker now auto-generates system messages visible to all members/agents. No more side effects in router — agents see system messages and act via tools. --- src/tracker/api/tasks.py | 75 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 70 insertions(+), 5 deletions(-) diff --git a/src/tracker/api/tasks.py b/src/tracker/api/tasks.py index aa39b64..af05ad4 100644 --- a/src/tracker/api/tasks.py +++ b/src/tracker/api/tasks.py @@ -80,6 +80,33 @@ class AssignRequest(BaseModel): # --- Helpers --- +async def _system_message(db: AsyncSession, task: Task, content: str, project_slug: str = ""): + """Create a system message for a task and broadcast it via WS.""" + from tracker.ws.manager import manager + msg = Message( + task_id=task.id, + author_type="system", + author_slug="system", + content=content, + mentions=[], + ) + db.add(msg) + await db.flush() # get msg.id + + prefix = project_slug[:2].upper() if project_slug else "XX" + key = f"{prefix}-{task.number}" + + await manager.broadcast_task_event(str(task.project_id), "message.new", { + "id": str(msg.id), + "task_id": str(task.id), + "task_key": key, + "author_type": "system", + "author_slug": "system", + "content": content, + "created_at": msg.created_at.isoformat() if msg.created_at else "", + }) + + def _task_out(t: Task, project_slug: str = "") -> dict: prefix = project_slug[:2].upper() if project_slug else "XX" return { @@ -200,13 +227,28 @@ async def create_task( "created_at": task.created_at.isoformat() if task.created_at else "", } await manager.broadcast_task_event(str(project.id), "task.created", task_data) - - return _task_out(task_full, task_full.project.slug if task_full.project else "") + + # System message + slug = project.slug + key = f"{slug[:2].upper()}-{task.number}" + sys_text = f"Задача {key} создана: {task.title}" + if task.assignee_slug: + sys_text += f". Назначена на @{task.assignee_slug}" + await _system_message(db, task_full, sys_text, slug) + await db.commit() + + return _task_out(task_full, slug) @router.patch("/tasks/{task_id}", response_model=TaskOut) async def update_task(task_id: str, req: TaskUpdate, db: AsyncSession = Depends(get_db)): task = await _get_task(task_id, db) + slug = task.project.slug if task.project else "" + prefix = slug[:2].upper() if slug else "XX" + key = f"{prefix}-{task.number}" + + old_status = task.status + old_assignee = task.assignee_slug if req.title is not None: task.title = req.title @@ -227,6 +269,15 @@ async def update_task(task_id: str, req: TaskUpdate, db: AsyncSession = Depends( if req.position is not None: task.position = req.position + # System messages for important changes + if req.status is not None and req.status != old_status: + await _system_message(db, task, f"{key}: статус изменён {old_status} → {req.status}", slug) + if req.assignee_slug is not None and req.assignee_slug != old_assignee: + if req.assignee_slug: + await _system_message(db, task, f"{key}: назначена на @{req.assignee_slug}", slug) + else: + await _system_message(db, task, f"{key}: исполнитель снят (был @{old_assignee})", slug) + await db.commit() await db.refresh(task) @@ -240,7 +291,7 @@ async def update_task(task_id: str, req: TaskUpdate, db: AsyncSession = Depends( } await manager.broadcast_task_event(str(task.project_id), "task.updated", task_data) - return _task_out(task, task.project.slug if task.project else "") + return _task_out(task, slug) @router.delete("/tasks/{task_id}") @@ -271,6 +322,13 @@ async def take_task(task_id: str, slug: str = Query(...), db: AsyncSession = Dep await db.commit() await db.refresh(task) + # System message + proj_slug = task.project.slug if task.project else "" + prefix = proj_slug[:2].upper() if proj_slug else "XX" + key = f"{prefix}-{task.number}" + await _system_message(db, task, f"{key}: @{slug} взял задачу в работу", proj_slug) + await db.commit() + # Broadcast task.assigned event from tracker.ws.manager import manager task_data = { @@ -281,7 +339,7 @@ async def take_task(task_id: str, slug: str = Query(...), db: AsyncSession = Dep } await manager.broadcast_task_event(str(task.project_id), "task.assigned", task_data) - return _task_out(task, task.project.slug if task.project else "") + return _task_out(task, proj_slug) @router.post("/tasks/{task_id}/reject") @@ -323,6 +381,13 @@ async def assign_task(task_id: str, req: AssignRequest, db: AsyncSession = Depen await db.commit() await db.refresh(task) + # System message + proj_slug = task.project.slug if task.project else "" + prefix = proj_slug[:2].upper() if proj_slug else "XX" + key = f"{prefix}-{task.number}" + await _system_message(db, task, f"{key}: назначена на @{req.assignee_slug}", proj_slug) + await db.commit() + # Broadcast task.assigned event from tracker.ws.manager import manager task_data = { @@ -333,7 +398,7 @@ async def assign_task(task_id: str, req: AssignRequest, db: AsyncSession = Depen } await manager.broadcast_task_event(str(task.project_id), "task.assigned", task_data) - return _task_out(task, task.project.slug if task.project else "") + return _task_out(task, proj_slug) @router.post("/tasks/{task_id}/watch")