Files
flowdeck/app/routers/sidebar_config.py
T
bruno 224bda74d5
FlowDeck CI / lint (push) Canceled after 0s
FlowDeck CI / test (push) Canceled after 0s
FlowDeck CI / docker (push) Canceled after 0s
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
2026-10-01 10:53:26 -04:00

117 lines
3.8 KiB
Python

"""FlowDeck — Sidebar customization API (v4.6.0)."""
from __future__ import annotations
import json
import logging
from fastapi import APIRouter, HTTPException, Request
from app.auth.session import SessionManager
from app.db import get_conn
logger = logging.getLogger(__name__)
router = APIRouter(tags=["sidebar"], prefix="/api/sidebar")
def _get_user(request: Request) -> dict:
user = SessionManager.decode_session(request.cookies.get("flowdeck_session", ""))
if not user:
raise HTTPException(status_code=401, detail="Not authenticated")
return user
DEFAULT_CONFIG = {
"workspace": {"visible": True, "order": 0, "show_count": None},
"gitea": {"visible": True, "order": 1, "show_count": None},
"meetings": {"visible": True, "order": 2, "show_count": 5},
"recents": {"visible": True, "order": 3, "show_count": 10},
"favorites": {"visible": True, "order": 4, "show_count": 10},
"agents": {"visible": True, "order": 5, "show_count": None},
"teamspaces": {"visible": True, "order": 6, "show_count": None},
"shared": {"visible": True, "order": 7, "show_count": 10},
"published": {"visible": True, "order": 8, "show_count": 10},
"private": {"visible": True, "order": 9, "show_count": None},
}
@router.get("/config")
def get_sidebar_config(request: Request):
"""Get the current user's sidebar customization config."""
user = _get_user(request)
with get_conn() as conn:
row = conn.execute(
"SELECT sidebar_config FROM users WHERE id=?", (user["id"],)
).fetchone()
if not row:
return {"config": DEFAULT_CONFIG}
raw = row["sidebar_config"]
if not raw:
return {"config": DEFAULT_CONFIG}
try:
stored = json.loads(raw)
except (json.JSONDecodeError, TypeError):
return {"config": DEFAULT_CONFIG}
# Merge with defaults to ensure all keys exist
merged = dict(DEFAULT_CONFIG)
merged.update(stored)
return {"config": merged}
def get_sidebar_config_sync(user_id: int) -> dict:
"""Synchronous helper to get sidebar config (used during template rendering)."""
with get_conn() as conn:
row = conn.execute(
"SELECT sidebar_config FROM users WHERE id=?", (user_id,)
).fetchone()
if not row:
return dict(DEFAULT_CONFIG)
raw = row["sidebar_config"]
if not raw:
return dict(DEFAULT_CONFIG)
try:
stored = json.loads(raw)
except (json.JSONDecodeError, TypeError):
return dict(DEFAULT_CONFIG)
merged = dict(DEFAULT_CONFIG)
merged.update(stored)
return merged
@router.put("/config")
async def save_sidebar_config(request: Request):
"""Save the current user's sidebar customization config."""
user = _get_user(request)
try:
body = await request.json()
except Exception:
raise HTTPException(status_code=400, detail="Invalid JSON body") from None
config = body.get("config")
if not config or not isinstance(config, dict):
raise HTTPException(status_code=400, detail="config object is required")
# Merge with defaults to ensure validity
merged = dict(DEFAULT_CONFIG)
for key, val in config.items():
if key in DEFAULT_CONFIG and isinstance(val, dict):
merged[key] = {
"visible": val.get("visible", DEFAULT_CONFIG[key]["visible"]),
"order": val.get("order", DEFAULT_CONFIG[key]["order"]),
"show_count": val.get("show_count", DEFAULT_CONFIG[key]["show_count"]),
}
elif key not in DEFAULT_CONFIG:
# Allow new custom sections
merged[key] = val
with get_conn() as conn:
conn.execute(
"UPDATE users SET sidebar_config=? WHERE id=?",
(json.dumps(merged), user["id"]),
)
conn.commit()
return {"status": "ok", "config": merged}