9bf047ec45
Backend: - Migration 0021: task_logs table (FK → notes + users, CASCADE, indexed) - models/task_log.py: SQLAlchemy model with to_dict() - services/task_logs.py: CRUD with ownership checks, _UNSET sentinel for optional duration clear - routes/task_logs.py: GET/POST/PATCH/DELETE /api/tasks/<id>/logs - services/tools.py: log_work LLM tool (resolves task by title, creates log entry) - services/generation_task.py: retry assist generation up to 3× on HTTP 500 Frontend: - types/task.ts: TaskLog interface - TaskLogSection.vue: chronological work log with date+time timestamps, duration badge, inline edit, autofocus - InlineAssistPanel.vue: streaming preview + diff review rendered inline in editor column - useAssist.ts: removed chatStore.chatReady gate; toast notifications for errors - NoteEditorView.vue + TaskEditorView.vue: inline assist panel, aside restricted to idle state - TaskEditorView.vue: two-column layout (editor+log left, metadata sidebar right), body defaults to Preview, sidebarOpen accordion for mobile - editor-shared.css: .assist-active-hint style Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
85 lines
2.5 KiB
Python
85 lines
2.5 KiB
Python
"""Task work log service."""
|
|
import logging
|
|
from datetime import datetime, timezone
|
|
|
|
from sqlalchemy import select
|
|
|
|
from fabledassistant.models import async_session
|
|
from fabledassistant.models.task_log import TaskLog
|
|
from fabledassistant.models.note import Note
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_UNSET = object()
|
|
|
|
|
|
async def create_log(
|
|
user_id: int,
|
|
task_id: int,
|
|
content: str,
|
|
duration_minutes: int | None = None,
|
|
) -> TaskLog:
|
|
async with async_session() as session:
|
|
# Verify task exists and belongs to user
|
|
result = await session.execute(
|
|
select(Note).where(Note.id == task_id, Note.user_id == user_id)
|
|
)
|
|
if result.scalars().first() is None:
|
|
raise ValueError(f"Task {task_id} not found")
|
|
log = TaskLog(
|
|
task_id=task_id,
|
|
user_id=user_id,
|
|
content=content,
|
|
duration_minutes=duration_minutes,
|
|
)
|
|
session.add(log)
|
|
await session.commit()
|
|
await session.refresh(log)
|
|
return log
|
|
|
|
|
|
async def list_logs(user_id: int, task_id: int) -> list[TaskLog]:
|
|
async with async_session() as session:
|
|
result = await session.execute(
|
|
select(TaskLog)
|
|
.where(TaskLog.task_id == task_id, TaskLog.user_id == user_id)
|
|
.order_by(TaskLog.created_at.asc())
|
|
)
|
|
return list(result.scalars().all())
|
|
|
|
|
|
async def update_log(
|
|
user_id: int,
|
|
log_id: int,
|
|
content: str | None = None,
|
|
duration_minutes: object = _UNSET,
|
|
) -> TaskLog | None:
|
|
async with async_session() as session:
|
|
result = await session.execute(
|
|
select(TaskLog).where(TaskLog.id == log_id, TaskLog.user_id == user_id)
|
|
)
|
|
log = result.scalars().first()
|
|
if log is None:
|
|
return None
|
|
if content is not None:
|
|
log.content = content
|
|
if duration_minutes is not _UNSET:
|
|
log.duration_minutes = duration_minutes # type: ignore[assignment]
|
|
log.updated_at = datetime.now(timezone.utc)
|
|
await session.commit()
|
|
await session.refresh(log)
|
|
return log
|
|
|
|
|
|
async def delete_log(user_id: int, log_id: int) -> bool:
|
|
async with async_session() as session:
|
|
result = await session.execute(
|
|
select(TaskLog).where(TaskLog.id == log_id, TaskLog.user_id == user_id)
|
|
)
|
|
log = result.scalars().first()
|
|
if log is None:
|
|
return False
|
|
await session.delete(log)
|
|
await session.commit()
|
|
return True
|