feat: dual system messages — detailed in task, brief in chat
Some checks failed
Deploy Tracker / deploy (push) Failing after 1s

- Task comments: who, what, when (detailed history)
- Chat messages: brief notifications
- PATCH /tasks accepts ?actor= for attribution
This commit is contained in:
markov 2026-02-24 12:27:29 +01:00
parent b527e19db1
commit 2f0c4d1636

View File

@ -80,48 +80,83 @@ class AssignRequest(BaseModel):
# --- Helpers --- # --- Helpers ---
async def _system_message(db: AsyncSession, task: Task, content: str, project_slug: str = ""): async def _get_project_chat_id(db: AsyncSession, project_id) -> str | None:
"""Create a system message for a task and broadcast it via WS. """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 from tracker.ws.manager import manager
import datetime
# 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
prefix = project_slug[:2].upper() if project_slug else "XX" prefix = project_slug[:2].upper() if project_slug else "XX"
key = f"{prefix}-{task.number}" key = f"{prefix}-{task.number}"
task_text = task_text or chat_text
now = datetime.datetime.now(datetime.timezone.utc)
event_data = { chat_id = await _get_project_chat_id(db, task.project_id)
"id": str(msg.id),
"chat_id": str(chat_id) if chat_id else None, # 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_id": str(task.id),
"task_key": key, "task_key": key,
"author_type": "system", "author_type": "system",
"author_slug": "system", "author_slug": actor_slug,
"content": content, "content": task_text,
"created_at": msg.created_at.isoformat() if msg.created_at else "", "created_at": task_msg.created_at.isoformat() if task_msg.created_at else now.isoformat(),
} })
await manager.broadcast_task_event(str(task.project_id), "message.new", event_data)
# 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: def _task_out(t: Task, project_slug: str = "") -> dict:
@ -248,21 +283,29 @@ async def create_task(
# System message # System message
slug = project.slug slug = project.slug
key = f"{slug[:2].upper()}-{task.number}" 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: if task.assignee_slug:
sys_text += f". Назначена на @{task.assignee_slug}" chat_text += f" → @{task.assignee_slug}"
await _system_message(db, task_full, sys_text, 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() await db.commit()
return _task_out(task_full, slug) return _task_out(task_full, slug)
@router.patch("/tasks/{task_id}", response_model=TaskOut) @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) task = await _get_task(task_id, db)
slug = task.project.slug if task.project else "" slug = task.project.slug if task.project else ""
prefix = slug[:2].upper() if slug else "XX" prefix = slug[:2].upper() if slug else "XX"
key = f"{prefix}-{task.number}" key = f"{prefix}-{task.number}"
who = f"@{actor}" if actor else "Кто-то"
old_status = task.status old_status = task.status
old_assignee = task.assignee_slug 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 task.position = req.position
# System messages for important changes # 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: 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 is not None and req.assignee_slug != old_assignee:
if req.assignee_slug: 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: 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.commit()
await db.refresh(task) 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 "" proj_slug = task.project.slug if task.project else ""
prefix = proj_slug[:2].upper() if proj_slug else "XX" prefix = proj_slug[:2].upper() if proj_slug else "XX"
key = f"{prefix}-{task.number}" 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() await db.commit()
# Broadcast task.assigned event # 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 "" proj_slug = task.project.slug if task.project else ""
prefix = proj_slug[:2].upper() if proj_slug else "XX" prefix = proj_slug[:2].upper() if proj_slug else "XX"
key = f"{prefix}-{task.number}" 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() await db.commit()
# Broadcast task.assigned event # Broadcast task.assigned event