From 2f0c4d16369f97af7c31e881078b7d5f4cf467d0 Mon Sep 17 00:00:00 2001 From: markov Date: Tue, 24 Feb 2026 12:27:29 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20dual=20system=20messages=20=E2=80=94=20?= =?UTF-8?q?detailed=20in=20task,=20brief=20in=20chat?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Task comments: who, what, when (detailed history) - Chat messages: brief notifications - PATCH /tasks accepts ?actor= for attribution --- src/tracker/api/tasks.py | 158 +++++++++++++++++++++++++++++---------- 1 file changed, 117 insertions(+), 41 deletions(-) diff --git a/src/tracker/api/tasks.py b/src/tracker/api/tasks.py index 32dcc0d..859e6c4 100644 --- a/src/tracker/api/tasks.py +++ b/src/tracker/api/tasks.py @@ -80,48 +80,83 @@ 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. +async def _get_project_chat_id(db: AsyncSession, project_id) -> str | None: + """Find project chat UUID.""" + result = await db.execute( + select(Chat).where(Chat.project_id == project_id, Chat.kind == "project") + ) + chat = result.scalar_one_or_none() + return chat.id if chat else None + + +async def _system_message( + db: AsyncSession, + task: Task, + chat_text: str, + task_text: str | None = None, + project_slug: str = "", + actor_slug: str = "system", +): + """Create system messages: one in project chat + one in task comments. - Message goes to the project chat (visible in ChatPanel) with task_id reference. + chat_text: short message for project chat + task_text: detailed message for task history (if None, same as chat_text) """ from tracker.ws.manager import manager - - # Find project chat - chat_id = None - if task.project_id: - result = await db.execute( - select(Chat).where(Chat.project_id == task.project_id, Chat.kind == "project") - ) - chat = result.scalar_one_or_none() - if chat: - chat_id = chat.id - - msg = Message( - chat_id=chat_id, - task_id=task.id, - author_type="system", - author_slug="system", - content=content, - mentions=[], - ) - db.add(msg) - await db.flush() # get msg.id + import datetime prefix = project_slug[:2].upper() if project_slug else "XX" key = f"{prefix}-{task.number}" + task_text = task_text or chat_text + now = datetime.datetime.now(datetime.timezone.utc) - event_data = { - "id": str(msg.id), - "chat_id": str(chat_id) if chat_id else None, + chat_id = await _get_project_chat_id(db, task.project_id) + + # 1. Task comment — detailed history + task_msg = Message( + task_id=task.id, + author_type="system", + author_slug=actor_slug, + content=task_text, + mentions=[], + ) + db.add(task_msg) + + # 2. Chat message — brief notification + chat_msg = None + if chat_id: + chat_msg = Message( + chat_id=chat_id, + author_type="system", + author_slug=actor_slug, + content=chat_text, + mentions=[], + ) + db.add(chat_msg) + + await db.flush() + + # Broadcast task comment + await manager.broadcast_task_event(str(task.project_id), "message.new", { + "id": str(task_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 "", - } - await manager.broadcast_task_event(str(task.project_id), "message.new", event_data) + "author_slug": actor_slug, + "content": task_text, + "created_at": task_msg.created_at.isoformat() if task_msg.created_at else now.isoformat(), + }) + + # Broadcast chat message + if chat_msg and chat_id: + await manager.broadcast_task_event(str(task.project_id), "message.new", { + "id": str(chat_msg.id), + "chat_id": str(chat_id), + "author_type": "system", + "author_slug": actor_slug, + "content": chat_text, + "created_at": chat_msg.created_at.isoformat() if chat_msg.created_at else now.isoformat(), + }) def _task_out(t: Task, project_slug: str = "") -> dict: @@ -248,21 +283,29 @@ async def create_task( # System message slug = project.slug key = f"{slug[:2].upper()}-{task.number}" - sys_text = f"Задача {key} создана: {task.title}" + chat_text = f"{key} создана: {task.title}" + task_text = f"Задача создана: {task.title}" if task.assignee_slug: - sys_text += f". Назначена на @{task.assignee_slug}" - await _system_message(db, task_full, sys_text, slug) + chat_text += f" → @{task.assignee_slug}" + task_text += f". Назначена на @{task.assignee_slug}" + await _system_message(db, task_full, chat_text=chat_text, task_text=task_text, project_slug=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)): +async def update_task( + task_id: str, + req: TaskUpdate, + actor: Optional[str] = Query(None, description="Who made the change"), + 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}" + who = f"@{actor}" if actor else "Кто-то" old_status = task.status old_assignee = task.assignee_slug @@ -287,13 +330,33 @@ async def update_task(task_id: str, req: TaskUpdate, db: AsyncSession = Depends( task.position = req.position # System messages for important changes + import datetime + now_str = datetime.datetime.now(datetime.timezone.utc).strftime("%H:%M UTC") if req.status is not None and req.status != old_status: - await _system_message(db, task, f"{key}: статус изменён {old_status} → {req.status}", slug) + await _system_message( + db, task, + chat_text=f"{key}: {old_status} → {req.status}", + task_text=f"{who} изменил статус: {old_status} → {req.status} ({now_str})", + project_slug=slug, + actor_slug=actor or "system", + ) 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) + await _system_message( + db, task, + chat_text=f"{key}: назначена на @{req.assignee_slug}", + task_text=f"{who} назначил задачу на @{req.assignee_slug} ({now_str})", + project_slug=slug, + actor_slug=actor or "system", + ) else: - await _system_message(db, task, f"{key}: исполнитель снят (был @{old_assignee})", slug) + await _system_message( + db, task, + chat_text=f"{key}: исполнитель снят", + task_text=f"{who} снял исполнителя @{old_assignee} ({now_str})", + project_slug=slug, + actor_slug=actor or "system", + ) await db.commit() await db.refresh(task) @@ -343,7 +406,15 @@ async def take_task(task_id: str, slug: str = Query(...), db: AsyncSession = Dep 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) + import datetime + now_str = datetime.datetime.now(datetime.timezone.utc).strftime("%H:%M UTC") + await _system_message( + db, task, + chat_text=f"{key}: @{slug} взял в работу", + task_text=f"@{slug} взял задачу в работу ({now_str})", + project_slug=proj_slug, + actor_slug=slug, + ) await db.commit() # Broadcast task.assigned event @@ -402,7 +473,12 @@ async def assign_task(task_id: str, req: AssignRequest, db: AsyncSession = Depen 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 _system_message( + db, task, + chat_text=f"{key}: назначена на @{req.assignee_slug}", + task_text=f"Задача назначена на @{req.assignee_slug}", + project_slug=proj_slug, + ) await db.commit() # Broadcast task.assigned event