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:
parent
c6db0ab569
commit
bc2dcb5589
5 changed files with 134 additions and 25 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -171,6 +171,46 @@ async def fetch_tasks(base_url: str, token: str) -> Dict[str, Any]:
|
|||
return result
|
||||
|
||||
|
||||
async def toggle_task_done(
|
||||
base_url: str,
|
||||
token: str,
|
||||
task_id: int,
|
||||
done: bool,
|
||||
) -> Dict[str, Any]:
|
||||
"""Toggle the done state of a single Vikunja task.
|
||||
|
||||
Args:
|
||||
base_url: Vikunja instance base URL.
|
||||
token: API token for authentication.
|
||||
task_id: The task ID to update.
|
||||
done: New done state.
|
||||
|
||||
Returns:
|
||||
Dictionary with ``ok`` and optionally ``error``.
|
||||
"""
|
||||
if not base_url or not token:
|
||||
return {"ok": False, "error": "Missing Vikunja base URL or token"}
|
||||
|
||||
clean_url = base_url.rstrip("/")
|
||||
headers = {
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10, headers=headers) as client:
|
||||
resp = await client.post(
|
||||
f"{clean_url}/tasks/{task_id}",
|
||||
json={"done": done},
|
||||
)
|
||||
resp.raise_for_status()
|
||||
return {"ok": True, "task": {"id": task_id, "done": done}}
|
||||
except httpx.HTTPStatusError as exc:
|
||||
return {"ok": False, "error": f"HTTP {exc.response.status_code}"}
|
||||
except Exception as exc:
|
||||
return {"ok": False, "error": str(exc)}
|
||||
|
||||
|
||||
async def fetch_single_project(
|
||||
base_url: str,
|
||||
token: str,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue