fix: A21 phase 1 — 352 routes async sans await → threadpool (v7.8.0)
- Conversion `async def` → `def` de TOUTES les routes dont le corps ne contient ni `await`, ni `async with`, ni `async for`, ni `asyncio` (scan automatique corps par corps sur app/ : 352 converties, 0 dangereuses, vérifié `asyncio`/`run_coroutine`/`.result()` absents). FastAPI exécute ces handlers dans son threadpool → tout leur SQLite (`get_conn()` + `conn.execute`) quitte l'event loop, sans changer une ligne de logique. - Répartition : api_v2 60, dashboard 40, collections 25, board 23, workspace 19, wiki 17, permissions 14, api 14, main.py 6, + 35 fichiers. - Les 4 routers prioritaires de l'audit sont couverts par ce lot : api_v2 60 + dashboard 40 + collections 25 + board 23 = 148 conversions (le reste de leurs routes attend la phase 2 : elles ont de vrais `await`). - Reste (phase 2) : les 311 routes avec de vrais `await` → enrouler les blocs DB dans `await anyio.to_thread.run_sync(...)` ; pas de wrapper partagé livré (rien ne l'appellerait — YAGNI jusqu'au premier usage). suite **1037/1037** (233 s) · `ruff check app tests` OK · docs à jour
This commit is contained in:
+40
-40
@@ -252,7 +252,7 @@ def _sidebar_data(request: Request, repos: list[dict], include_workspace: bool =
|
||||
|
||||
|
||||
@router.get("/trash", response_class=HTMLResponse)
|
||||
async def trash_page(request: Request, owner: str = Query(default=""), repo: str = Query(default="")):
|
||||
def trash_page(request: Request, owner: str = Query(default=""), repo: str = Query(default="")):
|
||||
"""Trash page — scoped to workspace if owner/repo provided."""
|
||||
from app.routers.board import _sidebar_data as board_sidebar
|
||||
from app.templating import ENV
|
||||
@@ -279,7 +279,7 @@ async def trash_page(request: Request, owner: str = Query(default=""), repo: str
|
||||
|
||||
|
||||
@router.get("/library", response_class=HTMLResponse)
|
||||
async def library_page(request: Request, owner: str = Query(default=""), repo: str = Query(default="")):
|
||||
def library_page(request: Request, owner: str = Query(default=""), repo: str = Query(default="")):
|
||||
"""Library page — tabbed view (recents, favorites, shared, published, private, workspace).
|
||||
|
||||
Sidebar data is kept intact. Tab content is loaded client-side via /api/library/* endpoints.
|
||||
@@ -308,7 +308,7 @@ async def library_page(request: Request, owner: str = Query(default=""), repo: s
|
||||
|
||||
|
||||
@router.get("/pages/{page_id}", response_class=HTMLResponse)
|
||||
async def view_page_root(request: Request, page_id: int):
|
||||
def view_page_root(request: Request, page_id: int):
|
||||
"""Render a Markdown page at root level with workspace context — or file viewer.
|
||||
?embed=1 — minimal mode for side peek (editor only, no header)."""
|
||||
embed = request.query_params.get("embed") == "1"
|
||||
@@ -434,7 +434,7 @@ async def view_page_root(request: Request, page_id: int):
|
||||
|
||||
|
||||
@router.get("/accounts", response_class=HTMLResponse)
|
||||
async def accounts_page(request: Request):
|
||||
def accounts_page(request: Request):
|
||||
"""Account management panel."""
|
||||
from app.templating import ENV
|
||||
env = ENV
|
||||
@@ -451,7 +451,7 @@ async def accounts_page(request: Request):
|
||||
|
||||
|
||||
@router.get("/help", response_class=HTMLResponse)
|
||||
async def help_page(request: Request):
|
||||
def help_page(request: Request):
|
||||
"""Comprehensive help & documentation page."""
|
||||
from app.templating import ENV
|
||||
env = ENV
|
||||
@@ -656,7 +656,7 @@ favorites, tags, sharing, sprints and templates; pagination (<code>?limit=&o
|
||||
|
||||
|
||||
@router.get("/accounts/settings", response_class=HTMLResponse)
|
||||
async def settings_page(request: Request):
|
||||
def settings_page(request: Request):
|
||||
"""User settings page — profile, forges, tokens."""
|
||||
from app.templating import ENV
|
||||
env = ENV
|
||||
@@ -731,7 +731,7 @@ async def update_password(request: Request):
|
||||
|
||||
|
||||
@router.post("/api/user/token")
|
||||
async def generate_token(request: Request):
|
||||
def generate_token(request: Request):
|
||||
import secrets
|
||||
uid = _require_user_id(request)
|
||||
token = secrets.token_hex(32)
|
||||
@@ -745,7 +745,7 @@ async def generate_token(request: Request):
|
||||
|
||||
|
||||
@router.delete("/api/user/forge/{provider}")
|
||||
async def disconnect_forge(request: Request, provider: str):
|
||||
def disconnect_forge(request: Request, provider: str):
|
||||
uid = _require_user_id(request)
|
||||
with get_conn() as conn:
|
||||
conn.execute(
|
||||
@@ -834,7 +834,7 @@ async def dashboard(
|
||||
# ═══════════ Workspace ═══════════
|
||||
|
||||
@router.get("/workspace", response_class=HTMLResponse)
|
||||
async def workspace_page(request: Request):
|
||||
def workspace_page(request: Request):
|
||||
"""Unified workspace showing all projects."""
|
||||
from app.templating import ENV
|
||||
env = ENV
|
||||
@@ -851,7 +851,7 @@ async def workspace_page(request: Request):
|
||||
|
||||
|
||||
@router.get("/gitea-workspace", response_class=HTMLResponse)
|
||||
async def gitea_workspace_page(request: Request):
|
||||
def gitea_workspace_page(request: Request):
|
||||
"""Gitea workspace — browse repo files."""
|
||||
import json
|
||||
|
||||
@@ -971,7 +971,7 @@ async def create_workspace_project(request: Request):
|
||||
# ═══════════ Workspace Members API ═══════════
|
||||
|
||||
@router.get("/api/workspace/{ws_id:int}/members")
|
||||
async def list_members(request: Request, ws_id: int):
|
||||
def list_members(request: Request, ws_id: int):
|
||||
"""List all members of a workspace."""
|
||||
with get_conn() as conn:
|
||||
rows = conn.execute(
|
||||
@@ -1022,7 +1022,7 @@ async def update_member_role(request: Request, ws_id: int, user_id: int):
|
||||
|
||||
|
||||
@router.delete("/api/workspace/{ws_id:int}/members/{user_id:int}")
|
||||
async def remove_member(request: Request, ws_id: int, user_id: int):
|
||||
def remove_member(request: Request, ws_id: int, user_id: int):
|
||||
"""Remove a member from a workspace."""
|
||||
with get_conn() as conn:
|
||||
conn.execute(
|
||||
@@ -1036,7 +1036,7 @@ async def remove_member(request: Request, ws_id: int, user_id: int):
|
||||
# ═══════════ Local Workspace (file/folder CRUD) ═══════════
|
||||
|
||||
@router.get("/local-workspace", response_class=HTMLResponse)
|
||||
async def local_workspace_page(request: Request, folder: int = None):
|
||||
def local_workspace_page(request: Request, folder: int = None):
|
||||
"""Local workspace page with file/folder tree.
|
||||
|
||||
If ?folder=ID is provided, shows that folder's contents with breadcrumb.
|
||||
@@ -1084,7 +1084,7 @@ async def local_workspace_page(request: Request, folder: int = None):
|
||||
|
||||
|
||||
@router.get("/api/local-workspace/tree")
|
||||
async def local_workspace_tree(request: Request, folder: int = None):
|
||||
def local_workspace_tree(request: Request, folder: int = None):
|
||||
"""Return the file/folder tree filtered by active workspace.
|
||||
|
||||
If ?folder=ID is provided, returns only that folder's children.
|
||||
@@ -1109,7 +1109,7 @@ async def local_workspace_tree(request: Request, folder: int = None):
|
||||
|
||||
|
||||
@router.get("/api/local-workspace/page-content/{page_id:int}")
|
||||
async def get_page_content(page_id: int):
|
||||
def get_page_content(page_id: int):
|
||||
"""Return the raw content of a page (for preview)."""
|
||||
with get_conn() as conn:
|
||||
row = conn.execute(
|
||||
@@ -1174,7 +1174,7 @@ def _require_page_view(request: Request, page_id: int) -> None:
|
||||
|
||||
|
||||
@router.get("/api/pages/{page_id}/download")
|
||||
async def download_page_file(request: Request, page_id: int):
|
||||
def download_page_file(request: Request, page_id: int):
|
||||
"""Download the original uploaded file of a ``file`` page (attachment)."""
|
||||
_require_page_view(request, page_id)
|
||||
with get_conn() as conn:
|
||||
@@ -1197,7 +1197,7 @@ async def download_page_file(request: Request, page_id: int):
|
||||
|
||||
|
||||
@router.get("/api/pages/{page_id}/file-content")
|
||||
async def page_file_content(request: Request, page_id: int):
|
||||
def page_file_content(request: Request, page_id: int):
|
||||
"""Return the textual content of a ``file`` page (for copy to clipboard).
|
||||
|
||||
Binary files (PDF, images…) answer ``{ok: false}`` — the clipboard copy is
|
||||
@@ -1225,7 +1225,7 @@ async def page_file_content(request: Request, page_id: int):
|
||||
|
||||
|
||||
@router.get("/api/local-workspace/breadcrumb")
|
||||
async def local_workspace_breadcrumb(request: Request, folder: int):
|
||||
def local_workspace_breadcrumb(request: Request, folder: int):
|
||||
"""Return breadcrumb trail for a folder."""
|
||||
with get_conn() as conn:
|
||||
breadcrumb = _build_breadcrumb(conn, folder)
|
||||
@@ -1393,7 +1393,7 @@ def _nav_breadcrumb(conn, page_id: int) -> list:
|
||||
|
||||
|
||||
@router.get("/api/nav/menu")
|
||||
async def nav_menu(request: Request, workspace_id: int = None, parent_id: int = None):
|
||||
def nav_menu(request: Request, workspace_id: int = None, parent_id: int = None):
|
||||
"""Return the pages at one level for the header breadcrumb navigation menu.
|
||||
|
||||
If ``parent_id`` is given, returns that page's children; otherwise the
|
||||
@@ -1483,7 +1483,7 @@ async def rename_local_workspace_item(request: Request, item_id: int):
|
||||
|
||||
|
||||
@router.delete("/api/local-workspace/items/{item_id:int}")
|
||||
async def delete_local_workspace_item(request: Request, item_id: int):
|
||||
def delete_local_workspace_item(request: Request, item_id: int):
|
||||
"""Soft-delete a file/folder (sets deleted_at)."""
|
||||
from datetime import datetime
|
||||
with get_conn() as conn:
|
||||
@@ -1496,7 +1496,7 @@ async def delete_local_workspace_item(request: Request, item_id: int):
|
||||
|
||||
|
||||
@router.post("/api/local-workspace/items/{item_id:int}/restore")
|
||||
async def restore_local_workspace_item(request: Request, item_id: int):
|
||||
def restore_local_workspace_item(request: Request, item_id: int):
|
||||
"""Restore a soft-deleted file/folder."""
|
||||
with get_conn() as conn:
|
||||
conn.execute(
|
||||
@@ -1508,7 +1508,7 @@ async def restore_local_workspace_item(request: Request, item_id: int):
|
||||
|
||||
|
||||
@router.get("/api/files/{ws_id:int}/{filename:path}")
|
||||
async def serve_uploaded_file(ws_id: int, filename: str):
|
||||
def serve_uploaded_file(ws_id: int, filename: str):
|
||||
"""Serve an uploaded file from disk."""
|
||||
import mimetypes
|
||||
from pathlib import Path
|
||||
@@ -1774,7 +1774,7 @@ def _get_active_workspace(request: Request, user_id: int = None) -> dict | None:
|
||||
|
||||
|
||||
@router.get("/workspaces", response_class=HTMLResponse)
|
||||
async def workspaces_page(request: Request):
|
||||
def workspaces_page(request: Request):
|
||||
"""Workspaces list page."""
|
||||
from app.templating import ENV
|
||||
env = ENV
|
||||
@@ -1791,7 +1791,7 @@ async def workspaces_page(request: Request):
|
||||
|
||||
|
||||
@router.get("/api/workspaces")
|
||||
async def list_workspaces(request: Request):
|
||||
def list_workspaces(request: Request):
|
||||
"""List all workspaces for the current user."""
|
||||
user = SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
|
||||
uid = user["id"] if user and user.get("id") else 1
|
||||
@@ -1855,7 +1855,7 @@ async def rename_workspace(request: Request, ws_id: int):
|
||||
|
||||
|
||||
@router.delete("/api/workspaces/{ws_id:int}")
|
||||
async def delete_workspace(request: Request, ws_id: int):
|
||||
def delete_workspace(request: Request, ws_id: int):
|
||||
"""Delete a workspace and all its pages."""
|
||||
with get_conn() as conn:
|
||||
conn.execute("DELETE FROM pages WHERE workspace_id=?", (ws_id,))
|
||||
@@ -1866,7 +1866,7 @@ async def delete_workspace(request: Request, ws_id: int):
|
||||
|
||||
|
||||
@router.post("/api/workspaces/{ws_id:int}/select")
|
||||
async def select_workspace(request: Request, ws_id: int):
|
||||
def select_workspace(request: Request, ws_id: int):
|
||||
"""Set the active workspace via cookie."""
|
||||
from fastapi.responses import JSONResponse
|
||||
response = JSONResponse({"status": "ok", "workspace_id": ws_id})
|
||||
@@ -1877,7 +1877,7 @@ async def select_workspace(request: Request, ws_id: int):
|
||||
# ═══════════ Settings Page ═══════════
|
||||
|
||||
@router.get("/settings", response_class=HTMLResponse)
|
||||
async def app_settings_page(request: Request):
|
||||
def app_settings_page(request: Request):
|
||||
"""Settings & configuration page."""
|
||||
from app.templating import ENV
|
||||
env = ENV
|
||||
@@ -1923,7 +1923,7 @@ async def upload_avatar(request: Request):
|
||||
|
||||
|
||||
@router.get("/api/settings/avatar/{filename:path}")
|
||||
async def serve_avatar_file(filename: str):
|
||||
def serve_avatar_file(filename: str):
|
||||
"""Serve an uploaded avatar image file."""
|
||||
from pathlib import Path
|
||||
|
||||
@@ -1942,7 +1942,7 @@ async def serve_avatar_file(filename: str):
|
||||
|
||||
|
||||
@router.get("/api/avatar/{user_id:int}")
|
||||
async def get_avatar(user_id: int):
|
||||
def get_avatar(user_id: int):
|
||||
"""Redirect to the user's avatar."""
|
||||
with get_conn() as conn:
|
||||
row = conn.execute("SELECT avatar_url FROM users WHERE id = ?", (user_id,)).fetchone()
|
||||
@@ -2002,7 +2002,7 @@ async def update_tag_global(tag_id: int, request: Request):
|
||||
|
||||
|
||||
@router.delete("/api/settings/tags/{tag_id:int}")
|
||||
async def delete_tag_global(tag_id: int, request: Request):
|
||||
def delete_tag_global(tag_id: int, request: Request):
|
||||
"""Delete a tag — only if owned by user."""
|
||||
uid = _get_user_id(request)
|
||||
with get_conn() as conn:
|
||||
@@ -2013,7 +2013,7 @@ async def delete_tag_global(tag_id: int, request: Request):
|
||||
|
||||
|
||||
@router.get("/api/settings/tags/all")
|
||||
async def list_all_tags_global(request: Request):
|
||||
def list_all_tags_global(request: Request):
|
||||
"""List current user's tags with counts."""
|
||||
uid = _get_user_id(request)
|
||||
with get_conn() as conn:
|
||||
@@ -2032,7 +2032,7 @@ async def list_all_tags_global(request: Request):
|
||||
# ═══════════ Workspace Tags API ═══════════
|
||||
|
||||
@router.get("/api/local-workspace/tags")
|
||||
async def list_tags(request: Request):
|
||||
def list_tags(request: Request):
|
||||
"""List ALL user tags with counts scoped to the active workspace."""
|
||||
ws = _get_active_workspace(request, user_id=_get_user_id(request))
|
||||
ws_id = ws["id"] if ws else None
|
||||
@@ -2055,7 +2055,7 @@ async def list_tags(request: Request):
|
||||
|
||||
|
||||
@router.get("/api/local-workspace/items/{item_id:int}/tags")
|
||||
async def get_item_tags(item_id: int):
|
||||
def get_item_tags(item_id: int):
|
||||
"""Get tags for a specific item."""
|
||||
with get_conn() as conn:
|
||||
rows = conn.execute(
|
||||
@@ -2104,7 +2104,7 @@ async def add_item_tag(request: Request, item_id: int):
|
||||
|
||||
|
||||
@router.delete("/api/local-workspace/items/{item_id:int}/tags/{tag_id:int}")
|
||||
async def remove_item_tag(item_id: int, tag_id: int):
|
||||
def remove_item_tag(item_id: int, tag_id: int):
|
||||
"""Remove a tag from an item."""
|
||||
with get_conn() as conn:
|
||||
conn.execute(
|
||||
@@ -2116,7 +2116,7 @@ async def remove_item_tag(item_id: int, tag_id: int):
|
||||
|
||||
|
||||
@router.get("/api/local-workspace/tags/search")
|
||||
async def search_by_tags(request: Request, tags: str = ""):
|
||||
def search_by_tags(request: Request, tags: str = ""):
|
||||
"""Search items by tags (comma-separated)."""
|
||||
ws = _get_active_workspace(request, user_id=_get_user_id(request))
|
||||
ws_id = ws["id"] if ws else None
|
||||
@@ -2198,7 +2198,7 @@ async def update_account(request: Request):
|
||||
# ═══════════ Sidebar Refresh API ═══════════
|
||||
|
||||
@router.get("/api/sidebar/workspace-tree")
|
||||
async def sidebar_workspace_tree(request: Request):
|
||||
def sidebar_workspace_tree(request: Request):
|
||||
"""Return the sidebar workspace tree as HTML fragment.
|
||||
|
||||
Called by appState().refreshSidebarTree() after CRUD operations
|
||||
@@ -2260,7 +2260,7 @@ async def sidebar_workspace_tree(request: Request):
|
||||
|
||||
|
||||
@router.get("/p/{slug}", response_class=HTMLResponse)
|
||||
async def public_published_page(request: Request, slug: str):
|
||||
def public_published_page(request: Request, slug: str):
|
||||
"""Serve a published page at /p/<slug> — no auth required."""
|
||||
from app.templating import ENV
|
||||
|
||||
@@ -2525,7 +2525,7 @@ def _render_blocks_public(blocks: list, titles: dict | None = None) -> str:
|
||||
# ═══════════ Library page actions API ═══════════
|
||||
|
||||
@router.get("/api/pages/{page_id:int}/content")
|
||||
async def api_page_content(page_id: int):
|
||||
def api_page_content(page_id: int):
|
||||
"""Get page content for side peek preview."""
|
||||
with get_conn() as conn:
|
||||
row = conn.execute(
|
||||
@@ -2563,7 +2563,7 @@ async def api_rename_page(page_id: int, request: Request):
|
||||
|
||||
|
||||
@router.post("/api/pages/{page_id:int}/trash")
|
||||
async def api_trash_page(page_id: int):
|
||||
def api_trash_page(page_id: int):
|
||||
"""Soft-delete a page (move to trash)."""
|
||||
with get_conn() as conn:
|
||||
conn.execute(
|
||||
@@ -2641,7 +2641,7 @@ async def api_convert_to_database(page_id: int, request: Request):
|
||||
|
||||
|
||||
@router.get("/api/collections/{collection_id:int}/table-data")
|
||||
async def api_collection_table_data(collection_id: int):
|
||||
def api_collection_table_data(collection_id: int):
|
||||
"""Get collection properties + pages for rendering the table view."""
|
||||
with get_conn() as conn:
|
||||
coll = conn.execute(
|
||||
|
||||
Reference in New Issue
Block a user