Files
FabledScribe/src/fabledassistant/services/task_logs.py
T
bvandeusen 9bf047ec45 Task work log, inline writing assistant, task editor sidebar layout
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>
2026-03-05 13:05:26 -05:00

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