This commit is contained in:
timka123 committed 2026-10-18 18:02:05 +03:00
commit 17a0a3c4c9
121 files changed
+17641

No files matched your search

+121
View File
@@ -0,0 +1,121 @@
"""Task endpoints: the aggregated list, refreshing it, and acting on it."""
from __future__ import annotations
from fastapi import APIRouter, HTTPException, Query
from app.api.deps import DbSession, Settings
from app.api.schemas import TaskCreateRequest, TaskDoneRequest, TaskUpdateRequest
from app.models import TaskRecord
from app.services import tasks_service
from app.sources.registry import SOURCE_LABELS, check_source
from app.timeutil import ensure_utc, local_date
router = APIRouter(prefix="/api/tasks", tags=["tasks"])
def task_dict(record: TaskRecord, today) -> dict:
return {
"id": record.id,
"source": record.source,
"source_label": SOURCE_LABELS.get(record.source, record.source),
"external_id": record.external_id,
"title": record.title,
"description": record.description,
"url": record.url,
"project": record.project,
"status": record.status,
"priority": record.priority,
"due_date": record.due_date.isoformat() if record.due_date else None,
"start_date": record.start_date.isoformat() if record.start_date else None,
"labels": record.labels or [],
"is_done": record.is_done,
"completed_at": ensure_utc(record.completed_at).isoformat() if record.completed_at else None,
"pinned": record.pinned,
"stale": record.stale,
"estimated_pomodoros": record.estimated_pomodoros,
"days_overdue": (today - record.due_date).days if record.due_date and record.due_date < today else 0,
"bucket": _bucket_of(record, today),
}
def _bucket_of(record: TaskRecord, today) -> str:
if record.due_date is None:
return "someday"
if record.due_date < today:
return "overdue"
if record.due_date == today:
return "today"
return "upcoming"
@router.get("")
def list_tasks(
session: DbSession,
settings: Settings,
include_done: bool = Query(default=False),
include_stale: bool = Query(default=False),
source: list[str] | None = Query(default=None),
) -> dict:
today = local_date(settings.timezone)
records = tasks_service.list_tasks(
session, include_done=include_done, include_stale=include_stale, sources=source
)
buckets = tasks_service.bucket_tasks(records, today)
return {
"date": today.isoformat(),
"tasks": [task_dict(record, today) for record in records],
"counts": {name: len(items) for name, items in buckets.items()},
"sources": sorted({record.source for record in records}),
}
@router.post("/refresh")
def refresh(session: DbSession, settings: Settings) -> dict:
"""Pull every enabled source now. Failures come back per source, not as a 500."""
return tasks_service.refresh_tasks(session, settings)
@router.post("")
def create(body: TaskCreateRequest, session: DbSession, settings: Settings) -> dict:
record = tasks_service.create_local_task(
session,
title=body.title,
description=body.description,
due_date=body.due_date,
priority=body.priority,
project=body.project,
estimated_pomodoros=body.estimated_pomodoros,
)
return task_dict(record, local_date(settings.timezone))
@router.patch("/{task_id}")
def update(task_id: int, body: TaskUpdateRequest, session: DbSession, settings: Settings) -> dict:
try:
record = tasks_service.update_task(session, task_id, **body.model_dump(exclude_unset=True))
except LookupError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
return task_dict(record, local_date(settings.timezone))
@router.post("/{task_id}/done")
def mark_done(task_id: int, body: TaskDoneRequest, session: DbSession, settings: Settings) -> dict:
try:
record, warning = tasks_service.complete_task(session, settings, task_id, done=body.done)
except LookupError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
return {"task": task_dict(record, local_date(settings.timezone)), "warning": warning}
@router.delete("/{task_id}")
def delete(task_id: int, session: DbSession) -> dict:
tasks_service.delete_task(session, task_id)
return {"deleted": task_id}
@router.post("/sources/{name}/test")
def test_source(name: str, settings: Settings) -> dict:
"""What the settings page's "Test connection" button calls."""
ok, detail = check_source(settings, name)
return {"source": name, "ok": ok, "detail": detail}