- Route Devii-driven AI gateway cost to the action/tool that triggered it instead of a blanket "internal" bucket, so per-feature AI spend is attributable. - Fix the quiz attempt review to show one previously-answered question at a time instead of all of them at once, and stop a quiz endpoint linked from the quiz flow from responding with raw JSON. - Add DB API async query result route and AI Usage Analyzer annotated source/media routes, with traversal-safe uid/path handling and matching tests. - Add Code Farm action audit logging (plant/harvest/buy-plot/upgrade/ fertilize) and related admin workspace/services/trash/gateway route and doc touch-ups. - Drop redundant docstrings from access_tokens.py per the no-comments convention. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VL9Xn57W5UR3HZbbuuzxdK
75 lines
2.7 KiB
Python
75 lines
2.7 KiB
Python
# retoor <retoor@molodetz.nl>
|
|
|
|
import logging
|
|
from typing import Annotated
|
|
|
|
from fastapi import Depends, APIRouter, Request
|
|
from fastapi.responses import JSONResponse
|
|
|
|
from devplacepy.attachments import get_orphan_attachments_batch, split_attachment_uids
|
|
from devplacepy.models import IssueForm
|
|
from devplacepy.responses import json_error
|
|
from devplacepy.schemas import IssueJobOut
|
|
from devplacepy.services.audit import record as audit
|
|
from devplacepy.services.gitea.config import gitea_config
|
|
from devplacepy.services.jobs import queue
|
|
from devplacepy.utils import not_found, require_user, track_action
|
|
from devplacepy.dependencies import json_or_form
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter()
|
|
|
|
|
|
@router.post("/create")
|
|
async def create_issue(request: Request, data: Annotated[IssueForm, Depends(json_or_form(IssueForm))]):
|
|
user = require_user(request)
|
|
if not gitea_config().is_configured:
|
|
return json_error(503, "The issue tracker is not configured")
|
|
title = data.title.strip()
|
|
uid = queue.enqueue(
|
|
"issue_create",
|
|
{
|
|
"author_uid": user["uid"],
|
|
"title": title,
|
|
"description": data.description.strip(),
|
|
"attachment_uids": get_orphan_attachments_batch(
|
|
split_attachment_uids(data.attachment_uids), user
|
|
),
|
|
},
|
|
"user",
|
|
user["uid"],
|
|
title[:64],
|
|
)
|
|
audit.record(
|
|
request,
|
|
"issue.create.request",
|
|
user=user,
|
|
target_type="issue",
|
|
summary=f"{user['username']} requested an issue report: {title[:60]}",
|
|
metadata={"title": title},
|
|
links=[audit.job(uid)],
|
|
)
|
|
track_action(user["uid"], "issue")
|
|
logger.info("Issue report enqueued by %s: %s", user["username"], title[:60])
|
|
return JSONResponse({"uid": uid, "status_url": f"/issues/jobs/{uid}"})
|
|
|
|
@router.get("/jobs/{uid}")
|
|
async def issue_job_status(request: Request, uid: str):
|
|
job = queue.get_job(uid)
|
|
if not job or job.get("kind") != "issue_create":
|
|
raise not_found("Job not found")
|
|
result = job.get("result", {})
|
|
done = job.get("status") == queue.DONE
|
|
payload = {
|
|
"uid": job.get("uid", ""),
|
|
"kind": job.get("kind", ""),
|
|
"status": job.get("status", ""),
|
|
"number": result.get("number") if done else None,
|
|
"issue_url": result.get("issue_url") if done else None,
|
|
"enhanced": bool(result.get("enhanced")) if done else False,
|
|
"error": job.get("error") or None,
|
|
"created_at": job.get("created_at"),
|
|
"completed_at": job.get("completed_at") or None,
|
|
}
|
|
return JSONResponse(IssueJobOut.model_validate(payload).model_dump(mode="json"))
|