Replace internal todos with Vikunja API wrapper

- New app/vikunja.py: get_tasks, create_task, update_task, delete_task
- /todos page now reads from and writes to Vikunja (create, mark done, delete)
- Auto-todos (clean build plate, filament low) post to Vikunja after DB commit
- /api/todos router proxies to Vikunja
- .env.install: VIKUNJA_URL, VIKUNJA_API_TOKEN, VIKUNJA_PROJECT_ID

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Martin Hohenberg
2026-06-19 07:40:54 +02:00
parent 223a1b7f1e
commit a2e8c9e194
5 changed files with 171 additions and 34 deletions

View File

@@ -1,21 +1,24 @@
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from app.db import get_db
from app.models.todo import Todo
from fastapi import APIRouter
from app.vikunja import create_task, delete_task, get_tasks, update_task
router = APIRouter(prefix="/todos", tags=["todos"])
@router.get("/")
def list_todos(db: Session = Depends(get_db)):
return [t.to_dict() for t in db.query(Todo).all()]
def list_todos():
return get_tasks()
@router.get("/{todo_id}")
def get_todo(todo_id: int, db: Session = Depends(get_db)):
todo = db.query(Todo).filter(Todo.id == todo_id).first()
if not todo:
from fastapi import HTTPException
raise HTTPException(status_code=404, detail="Todo not found")
return todo.to_dict()
@router.post("/")
def create_todo(body: dict):
return create_task(body.get("title", ""))
@router.post("/{task_id}")
def update_todo(task_id: int, body: dict):
update_task(task_id, **body)
@router.delete("/{task_id}")
def delete_todo(task_id: int):
delete_task(task_id)

View File

@@ -20,7 +20,7 @@ from app.models.printer import Printer, PrinterStatus
from app.models.printer_log import PrinterLog
from app.models.printer_type import PrinterType
from app.models.pricing import PricingConfig
from app.models.todo import Todo
from app.vikunja import create_task as vikunja_create, delete_task as vikunja_delete, get_tasks as vikunja_tasks, update_task as vikunja_update
from app.constants import COLORS, MATERIALS
from app.estimation import estimate # also used in start_job
from app.filamentdb import (available_colors, available_materials, find_matching_ams_roll,
@@ -353,6 +353,7 @@ def start_job(job_id: int, printer_id: int = Form(...), db: Session = Depends(ge
if roll:
note = (f"POPS {job.job_uuid[:8] if job.job_uuid else job.id}: "
f"{job.file_name or job.name} on {printer.name}")
filament_low = False
if log_print_on_roll(roll["roll_id"], weight_g, note):
db.add(JobLog(job_id=job_id, message=(
f"FilamentDB: logged {weight_g} g against roll #{roll['roll_id']} "
@@ -360,7 +361,7 @@ def start_job(job_id: int, printer_id: int = Form(...), db: Session = Depends(ge
f"{roll['weight_remaining_g']} g remaining, least-full roll selected)"
)))
if roll["weight_remaining_g"] - weight_g < 100:
db.add(Todo(title="Check filament status"))
filament_low = True
else:
db.add(JobLog(job_id=job_id, message=f"FilamentDB: print log failed for roll #{roll['roll_id']}"))
else:
@@ -369,6 +370,8 @@ def start_job(job_id: int, printer_id: int = Form(...), db: Session = Depends(ge
)))
db.commit()
if filament_low:
vikunja_create("Check filament status")
return RedirectResponse(url=f"/jobs/{job_id}", status_code=HTTP_303_SEE_OTHER)
@@ -426,15 +429,18 @@ def finish_job(job_id: int, db: Session = Depends(get_db)):
db.add(JobLog(job_id=job_id, message=f"Job finished on {printer_name}"))
# Every 3rd completed print on this printer → cleaning reminder
cleaning_todo = False
if job.printer_id:
done_count = db.query(PrintJob).filter(
PrintJob.printer_id == job.printer_id,
PrintJob.status == JobStatus.done,
).count()
if (done_count + 1) % 3 == 0:
db.add(Todo(title=f"Clean build plate on {printer_name}", printer_id=job.printer_id))
cleaning_todo = True
db.commit()
if cleaning_todo:
vikunja_create(f"Clean build plate on {printer_name}")
return RedirectResponse(url=f"/jobs/{job_id}", status_code=HTTP_303_SEE_OTHER)
@@ -485,17 +491,37 @@ def bracket_detail_redirect(bracket_id: int):
return RedirectResponse(url=f"/jobs?bracket_id={bracket_id}", status_code=HTTP_303_SEE_OTHER)
# ── To-Do ──────────────────────────────────────────────────────────────────────
# ── To-Do (Vikunja) ────────────────────────────────────────────────────────────
@router.get("/todos", response_class=HTMLResponse)
def todos_page(request: Request, db: Session = Depends(get_db)):
def todos_page(request: Request):
tasks = vikunja_tasks()
tasks.sort(key=lambda t: (t.get("done", False), t.get("created", "")), reverse=False)
return templates.TemplateResponse("todos.html", {
"request": request,
"active": "todos",
"todos": db.query(Todo).order_by(Todo.done, Todo.created_at.desc()).all(),
"todos": tasks,
})
@router.post("/todos")
def create_todo(title: str = Form(...)):
vikunja_create(title.strip())
return RedirectResponse(url="/todos", status_code=HTTP_303_SEE_OTHER)
@router.post("/todos/{task_id}/done")
def done_todo(task_id: int):
vikunja_update(task_id, done=True)
return RedirectResponse(url="/todos", status_code=HTTP_303_SEE_OTHER)
@router.post("/todos/{task_id}/delete")
def delete_todo(task_id: int):
vikunja_delete(task_id)
return RedirectResponse(url="/todos", status_code=HTTP_303_SEE_OTHER)
# ── Pricing ────────────────────────────────────────────────────────────────────
@router.get("/pricing", response_class=HTMLResponse)