feat: interactive Vikunja tasks — checkbox toggles done, click opens task

- Added POST /api/tasks/toggle endpoint to mark tasks as done/undone
- Added toggle_task_done() in vikunja_service (POST /tasks/{id})
- Cache invalidated after toggle for immediate refresh
- Checkbox click toggles done state with visual feedback
- Click on task row opens Vikunja in new tab (/tasks/{id})
- ExternalLink icon appears on hover as affordance

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Sam 2026-03-02 23:43:01 +01:00
parent c6db0ab569
commit bc2dcb5589
5 changed files with 134 additions and 25 deletions

View file

@ -6,10 +6,11 @@ import logging
from typing import Any, Dict
from fastapi import APIRouter
from pydantic import BaseModel
from server.cache import cache
from server.config import get_settings
from server.services.vikunja_service import fetch_tasks
from server.services.vikunja_service import fetch_tasks, toggle_task_done
logger = logging.getLogger(__name__)
@ -20,20 +21,12 @@ CACHE_KEY = "tasks"
@router.get("/tasks")
async def get_tasks() -> Dict[str, Any]:
"""Return Vikunja task data.
"""Return Vikunja task data."""
The exact shape depends on what ``fetch_tasks`` returns; on failure an
error stub is returned instead::
{ "error": true, "message": "..." }
"""
# --- cache hit? -----------------------------------------------------------
cached = await cache.get(CACHE_KEY)
if cached is not None:
return cached
# --- cache miss -----------------------------------------------------------
try:
data: Dict[str, Any] = await fetch_tasks(
get_settings().vikunja_url,
@ -45,3 +38,27 @@ async def get_tasks() -> Dict[str, Any]:
await cache.set(CACHE_KEY, data, get_settings().vikunja_cache_ttl)
return data
class TaskToggleRequest(BaseModel):
task_id: int
done: bool
@router.post("/tasks/toggle")
async def toggle_task(body: TaskToggleRequest) -> Dict[str, Any]:
"""Toggle a Vikunja task's done state."""
settings = get_settings()
result = await toggle_task_done(
settings.vikunja_url,
settings.vikunja_token,
body.task_id,
body.done,
)
if result.get("ok"):
# Invalidate cache so next fetch reflects the change
await cache.invalidate(CACHE_KEY)
logger.info("[TASKS] task/%d → done=%s", body.task_id, body.done)
return result