Files
devplacepy/devplacepy/routers/battles.py
T
retoorandClaude Sonnet 5 57087536e5 Attribute Devii AI spend to its invoking action, fix quiz question-at-a-time review, DB API/isslop result routes, workspace docs, and drop redundant docstrings
- 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
2026-09-03 08:47:57 +02:00

132 lines
4.2 KiB
Python

# retoor <retoor@molodetz.nl>
from typing import Annotated
from urllib.parse import quote
from fastapi import APIRouter, Depends, Request
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
from devplacepy.database import resolve_object_url
from devplacepy.dependencies import json_or_form
from devplacepy.models import WarJoinForm
from devplacepy.responses import action_result, json_error, respond, wants_json
from devplacepy.schemas import BattlesOut, WarEventsOut, WarOut
from devplacepy.seo import base_seo_context, site_url, website_schema
from devplacepy.services.opinionwar import WarError, rules, store
from devplacepy.utils import get_current_user, not_found, require_user
router = APIRouter()
def _war_error(request: Request, message: str, redirect_url: str):
if wants_json(request):
return json_error(400, message)
separator = "&" if "?" in redirect_url else "?"
return RedirectResponse(
url=f"{redirect_url}{separator}error={quote(message)}", status_code=302
)
def _post_url(war: dict) -> str:
return resolve_object_url("post", war["post_uid"])
@router.get("", response_class=HTMLResponse)
async def battles_page(
request: Request, filter: str = "active", search: str = "", page: int = 1
):
user = get_current_user(request)
current_filter = filter if filter in store.FILTERS else "active"
battles, pagination = store.list_wars(
viewer=user,
war_filter=current_filter,
search=search,
page=max(1, page),
)
base = site_url(request)
seo_ctx = base_seo_context(
request,
title="Opinion Wars",
description=(
"Week-long faction battles between developers. Pick a side, fight once "
"a day and carry your faction to victory."
),
breadcrumbs=[
{"name": "Home", "url": "/feed"},
{"name": "Battles", "url": "/battles"},
],
schemas=[website_schema(base)],
)
return respond(
request,
"battles.html",
{
**seo_ctx,
"user": user,
"battles": battles,
"current_filter": current_filter,
"counts": store.filter_counts(user, search),
"search": search,
"pagination": pagination,
},
model=BattlesOut,
)
@router.get("/{war_uid}")
async def battle_state(request: Request, war_uid: str):
user = get_current_user(request)
serialized = store.get_war_serialized(store.get_war(war_uid), user)
if not serialized:
raise not_found("Battle not found")
return JSONResponse(WarOut.model_validate(serialized).model_dump())
@router.get("/{war_uid}/events")
async def battle_events(
request: Request, war_uid: str, after: int = 0, limit: int = rules.EVENT_LIMIT_DEFAULT
):
war = store.resolve_if_due(store.get_war(war_uid))
if not war:
raise not_found("Battle not found")
events = store.events_for(war_uid, after_seq=after, limit=limit)
return JSONResponse(
WarEventsOut.model_validate(
{"events": events, "status": war.get("status") or "active"}
).model_dump()
)
@router.post("/{war_uid}/join")
async def join_battle(
request: Request,
war_uid: str,
data: Annotated[WarJoinForm, Depends(json_or_form(WarJoinForm))],
):
user = require_user(request)
war = store.get_war(war_uid)
if not war:
raise not_found("Battle not found")
url = _post_url(war)
try:
war = store.join_war(war, user, data.faction, request)
except WarError as exc:
return _war_error(request, str(exc), url)
serialized = store.get_war_serialized(war, user)
return action_result(request, url, data={"war": serialized})
@router.post("/{war_uid}/fight")
async def fight_battle(request: Request, war_uid: str):
user = require_user(request)
war = store.get_war(war_uid)
if not war:
raise not_found("Battle not found")
url = _post_url(war)
try:
war, damage = store.fight(war, user, request)
except WarError as exc:
return _war_error(request, str(exc), url)
serialized = store.get_war_serialized(war, user)
return action_result(request, url, data={"war": serialized, "damage": damage})