fix: A21 phase 2c — 190 routes hors loop, 86 % total (v7.26.0)
FlowDeck CI / lint (push) Successful in 1m55s
FlowDeck CI / test (push) Successful in 15m23s
FlowDeck CI / docker (push) Canceled after 0s

4 passes (283 → 93 routes async sur 667 = 86 % hors loop, avant 61 %) :

A. RACINE AUTH — `get_current_user` (auth/session.py) était `async def`
   SANS aucun await (cookie decode = synchrone) ; idem ses clones :
   `agent._current_user_id/_workspace_id/_current_admin` (34 sites) et
   `sso._require_admin` (corps 0 await, 6 sites) → `def` +
   47 `await` supprimés. Piège : 3 call sites passaient par l'alias `gcu`
   (grep littéral aveugle) — 8 tests en échec → corrigés.

B. Re-scan : 19 routes devenues SANS await → `def` (agent 8, sso 5,
   web_clipper 3, projects 2, auth 1…).

C/D. 155 routes dont les seuls awaits = `request.json()` / événements :
   - try/except `body = {}` → `Body(default={})` (même tolérance)
   - try/except `raise HTTPException(400)` → `Body(...)` REQUIS
     (422 FastAPI — aucun test ne couvrait le 400)
   - forme conditionnelle `request.json() if content-type else {}`
     (54 sites) → défaut `{}` (sans corps = `{}` dans les 2 cas)
   - `await fire_*` → `run_event_sync(...)` ; imports `Body` /
     `run_event_sync` ajoutés aux routers convertis

Reste async (93, justifié) : form/upload/file (22), réseau gitea/llm/oidc,
`_json_body` (9), 2 JSON inline en argument, 1 fallback logique
(capture_frontend_error), 1 lecture conditionnelle (web_clipper), mixtes.

suite **1089/1089** · ruff OK · docs à jour
This commit is contained in:
2026-10-01 22:17:48 -04:00
parent 8d0d69e7b8
commit 45e59009c3
34 changed files with 2479 additions and 680 deletions
+28 -47
View File
@@ -8,12 +8,12 @@ import json
import logging
import sqlite3
from fastapi import APIRouter, HTTPException, Request
from fastapi import APIRouter, Body, HTTPException, Request
from fastapi.responses import HTMLResponse, StreamingResponse
from app.auth.session import SessionManager
from app.db import get_conn
from app.services.automations import fire_event
from app.services.automations import fire_event, run_event_sync
logger = logging.getLogger(__name__)
router = APIRouter(tags=["workspace"], prefix="/workspace")
@@ -57,8 +57,7 @@ def _require_ws_admin(request: Request, ws_id: int) -> None:
# ── Workspaces ──
@router.post("")
async def create_workspace(request: Request):
body = await request.json() if request.headers.get("content-type") else {}
def create_workspace(request: Request, body: dict = Body(default={})):
name = body.get("name", "Default Workspace")
user = _current_user(request)
uid = user.get("id", 1)
@@ -91,9 +90,8 @@ def list_members(request: Request, ws_id: int):
@router.post("/{ws_id}/members")
async def add_member(request: Request, ws_id: int):
def add_member(request: Request, ws_id: int, body: dict = Body(default={})):
_require_ws_admin(request, ws_id)
body = await request.json() if request.headers.get("content-type") else {}
user_id = body.get("user_id")
role = body.get("role", "editor")
if role not in ROLES:
@@ -108,9 +106,8 @@ async def add_member(request: Request, ws_id: int):
@router.put("/{ws_id}/members/{user_id}")
async def update_member_role(request: Request, ws_id: int, user_id: int):
def update_member_role(request: Request, ws_id: int, user_id: int, body: dict = Body(default={})):
_require_ws_admin(request, ws_id)
body = await request.json() if request.headers.get("content-type") else {}
role = body.get("role", "editor")
if role not in ROLES:
raise HTTPException(400, f"Invalid role: {role}")
@@ -143,8 +140,7 @@ def list_comments(request: Request, page_id: int):
@router.post("/pages/{page_id}/comments")
async def add_comment(request: Request, page_id: int):
body = await request.json() if request.headers.get("content-type") else {}
def add_comment(request: Request, page_id: int, body: dict = Body(default={})):
b = body.get("body", "").strip()
if not b:
raise HTTPException(400, "body required")
@@ -158,15 +154,14 @@ async def add_comment(request: Request, page_id: int):
(page_id, uid, b, parent_id))
conn.commit()
try:
await fire_event("comment.added", {"comment_id": cur.lastrowid, "page_id": page_id, "user_id": uid})
run_event_sync(fire_event("comment.added", {"comment_id": cur.lastrowid, "page_id": page_id, "user_id": uid}))
except Exception:
logger.exception("add_comment")
return {"id": cur.lastrowid, "status": "created"}
@router.put("/comments/{comment_id}")
async def update_comment(request: Request, comment_id: int):
body = await request.json() if request.headers.get("content-type") else {}
def update_comment(request: Request, comment_id: int, body: dict = Body(default={})):
b = body.get("body")
resolved = body.get("resolved")
with get_conn() as conn:
@@ -178,7 +173,7 @@ async def update_comment(request: Request, comment_id: int):
conn.commit()
if resolved and row and not int(row["resolved"] or 0):
try:
await fire_event("comment.resolved", {"comment_id": comment_id, "page_id": row["page_id"]})
run_event_sync(fire_event("comment.resolved", {"comment_id": comment_id, "page_id": row["page_id"]}))
except Exception:
logger.exception("update_comment")
return {"status": "updated"}
@@ -197,8 +192,7 @@ def page_history(request: Request, page_id: int):
@router.post("/pages/{page_id}/history")
async def record_history(request: Request, page_id: int):
body = await request.json() if request.headers.get("content-type") else {}
def record_history(request: Request, page_id: int, body: dict = Body(default={})):
user = _current_user(request)
uid = user.get("id", 1)
with get_conn() as conn:
@@ -230,8 +224,7 @@ def list_favorites(request: Request):
@router.post("/favorites")
async def add_favorite(request: Request):
body = await request.json() if request.headers.get("content-type") else {}
def add_favorite(request: Request, body: dict = Body(default={})):
user = _current_user(request)
uid = user.get("id", 1)
page_id = body.get("page_id")
@@ -244,7 +237,7 @@ async def add_favorite(request: Request):
)
conn.commit()
try:
await fire_event("favorite.added", {"page_id": page_id, "collection_id": collection_id, "user_id": uid})
run_event_sync(fire_event("favorite.added", {"page_id": page_id, "collection_id": collection_id, "user_id": uid}))
except Exception:
logger.exception("add_favorite")
return {"status": "favorited"}
@@ -268,8 +261,7 @@ def list_db_templates(request: Request):
@router.post("/templates/database")
async def create_db_template(request: Request):
body = await request.json() if request.headers.get("content-type") else {}
def create_db_template(request: Request, body: dict = Body(default={})):
cur = None
with get_conn() as conn:
cur = conn.execute(
@@ -282,9 +274,8 @@ async def create_db_template(request: Request):
@router.post("/templates/database/{tid}/apply")
async def apply_db_template(request: Request, tid: int):
def apply_db_template(request: Request, tid: int, body: dict = Body(default={})):
from app.services.db_templates import create_from_template
body = await request.json() if request.headers.get("content-type") else {}
name = body.get("name", "New Database")
with get_conn() as conn:
tmpl = conn.execute("SELECT * FROM database_templates WHERE id=?", (tid,)).fetchone()
@@ -305,8 +296,7 @@ def list_page_templates(request: Request, collection_id: int):
@router.post("/collections/{collection_id}/templates/page")
async def create_page_template(request: Request, collection_id: int):
body = await request.json() if request.headers.get("content-type") else {}
def create_page_template(request: Request, collection_id: int, body: dict = Body(default={})):
with get_conn() as conn:
cur = conn.execute(
"INSERT INTO page_templates (collection_id, name, property_values_json) VALUES (?,?,?)",
@@ -317,8 +307,7 @@ async def create_page_template(request: Request, collection_id: int):
@router.post("/collections/{collection_id}/templates/page/{tid}/apply")
async def apply_page_template(request: Request, collection_id: int, tid: int):
body = await request.json() if request.headers.get("content-type") else {}
def apply_page_template(request: Request, collection_id: int, tid: int, body: dict = Body(default={})):
with get_conn() as conn:
tmpl = conn.execute("SELECT * FROM page_templates WHERE id=? AND collection_id=?", (tid, collection_id)).fetchone()
if not tmpl:
@@ -331,19 +320,18 @@ async def apply_page_template(request: Request, collection_id: int, tid: int):
(collection_id, body.get("title", "New Page"), max_pos, tmpl["property_values_json"]),
)
conn.commit()
await fire_event("page.created", {
run_event_sync(fire_event("page.created", {
"page_id": cur.lastrowid,
"collection_id": collection_id,
"title": body.get("title", "New Page"),
"properties": json.loads(tmpl["property_values_json"]) if tmpl["property_values_json"] else {},
})
}))
return {"id": cur.lastrowid, "status": "created"}
@router.put("/collections/{collection_id}/templates/page/{tid}")
async def update_page_template(request: Request, collection_id: int, tid: int):
def update_page_template(request: Request, collection_id: int, tid: int, body: dict = Body(default={})):
"""Update a page template — name, properties, content, recurrence."""
body = await request.json() if request.headers.get("content-type") else {}
with get_conn() as conn:
tmpl = conn.execute(
"SELECT * FROM page_templates WHERE id=? AND collection_id=?", (tid, collection_id)
@@ -395,9 +383,8 @@ def list_dashboards(request: Request, collection_id: int):
@router.post("/collections/{collection_id}/dashboards")
async def create_dashboard(request: Request, collection_id: int):
def create_dashboard(request: Request, collection_id: int, body: dict = Body(default={})):
"""Create a new dashboard for a collection."""
body = await request.json() if request.headers.get("content-type") else {}
name = body.get("name", "Dashboard").strip()
layout = json.dumps(body.get("layout", {"columns": 1, "widgets": []}))
@@ -414,9 +401,8 @@ async def create_dashboard(request: Request, collection_id: int):
@router.put("/collections/{collection_id}/dashboards/{did}")
async def update_dashboard(request: Request, collection_id: int, did: int):
def update_dashboard(request: Request, collection_id: int, did: int, body: dict = Body(default={})):
"""Update a dashboard — name or layout (widgets grid)."""
body = await request.json() if request.headers.get("content-type") else {}
with get_conn() as conn:
dash = conn.execute(
"SELECT * FROM collection_dashboards WHERE id=? AND collection_id=?", (did, collection_id)
@@ -471,9 +457,8 @@ def list_sprints(request: Request, collection_id: int):
@router.post("/collections/{collection_id}/sprints")
async def create_sprint(request: Request, collection_id: int):
def create_sprint(request: Request, collection_id: int, body: dict = Body(default={})):
"""Create a new sprint."""
body = await request.json() if request.headers.get("content-type") else {}
name = body.get("name", "").strip()
start_date = body.get("start_date", "")
end_date = body.get("end_date", "")
@@ -491,16 +476,15 @@ async def create_sprint(request: Request, collection_id: int):
)
conn.commit()
try:
await fire_event("sprint.created", {"sprint_id": cur.lastrowid, "collection_id": collection_id, "name": name})
run_event_sync(fire_event("sprint.created", {"sprint_id": cur.lastrowid, "collection_id": collection_id, "name": name}))
except Exception:
logger.exception("create_sprint")
return {"id": cur.lastrowid, "name": name, "status": "created"}
@router.put("/collections/{collection_id}/sprints/{sid}")
async def update_sprint(request: Request, collection_id: int, sid: int):
def update_sprint(request: Request, collection_id: int, sid: int, body: dict = Body(default={})):
"""Update a sprint."""
body = await request.json() if request.headers.get("content-type") else {}
with get_conn() as conn:
sprint = conn.execute(
"SELECT * FROM sprints WHERE id=? AND collection_id=?", (sid, collection_id)
@@ -521,7 +505,7 @@ async def update_sprint(request: Request, collection_id: int, sid: int):
)
conn.commit()
try:
await fire_event("sprint.updated", {"sprint_id": sid, "collection_id": collection_id, "name": name, "status": status})
run_event_sync(fire_event("sprint.updated", {"sprint_id": sid, "collection_id": collection_id, "name": name, "status": status}))
except Exception:
logger.exception("update_sprint")
return {"id": sid, "status": "updated"}
@@ -542,9 +526,8 @@ def delete_sprint(request: Request, collection_id: int, sid: int):
@router.post("/collections/{collection_id}/sprints/{sid}/assign")
async def assign_page_to_sprint(request: Request, collection_id: int, sid: int):
def assign_page_to_sprint(request: Request, collection_id: int, sid: int, body: dict = Body(default={})):
"""Assign a page to a sprint."""
body = await request.json() if request.headers.get("content-type") else {}
page_id = body.get("page_id")
if not page_id:
raise HTTPException(400, "page_id is required")
@@ -634,8 +617,7 @@ def sprint_burndown(request: Request, collection_id: int, sid: int):
# ── CSV Import/Export ──
@router.post("/collections/{collection_id}/import/csv")
async def import_csv(request: Request, collection_id: int):
body = await request.json() if request.headers.get("content-type") else {}
def import_csv(request: Request, collection_id: int, body: dict = Body(default={})):
csv_data = body.get("csv", "")
if not csv_data:
raise HTTPException(400, "csv field required")
@@ -698,9 +680,8 @@ def list_webhooks(request: Request):
@router.post("/webhooks")
async def create_webhook(request: Request):
def create_webhook(request: Request, body: dict = Body(default={})):
_require_admin(request)
body = await request.json() if request.headers.get("content-type") else {}
url = body.get("url", "").strip()
event = body.get("event", "page.created")
secret = body.get("secret", "")