From 224bda74d50f6ae5e2d22aea60ca9559a6134eb0 Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Thu, 1 Oct 2026 10:53:26 -0400 Subject: [PATCH] =?UTF-8?q?fix:=20A21=20phase=201=20=E2=80=94=20352=20rout?= =?UTF-8?q?es=20async=20sans=20await=20=E2=86=92=20threadpool=20(v7.8.0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- CHANGELOG.md | 21 ++++++ ROADMAP.md | 4 +- VERSION | 2 +- WORKLOAD.md | 2 +- app/main.py | 14 ++-- app/routers/admin.py | 8 +-- app/routers/agent.py | 20 +++--- app/routers/api.py | 28 ++++---- app/routers/api_v2.py | 120 +++++++++++++++++----------------- app/routers/api_v2_agent.py | 26 ++++---- app/routers/audit.py | 2 +- app/routers/auth.py | 12 ++-- app/routers/automations.py | 12 ++-- app/routers/board.py | 46 ++++++------- app/routers/collaboration.py | 4 +- app/routers/collections.py | 50 +++++++------- app/routers/dashboard.py | 80 +++++++++++------------ app/routers/emoji.py | 4 +- app/routers/export.py | 8 +-- app/routers/gitea.py | 10 +-- app/routers/github_routes.py | 4 +- app/routers/governance.py | 4 +- app/routers/imports.py | 10 +-- app/routers/library.py | 20 +++--- app/routers/meetings.py | 6 +- app/routers/my_tasks.py | 4 +- app/routers/notes.py | 2 +- app/routers/notifications.py | 10 +-- app/routers/onboarding.py | 2 +- app/routers/permissions.py | 28 ++++---- app/routers/projects.py | 2 +- app/routers/public_api.py | 12 ++-- app/routers/realtime.py | 2 +- app/routers/scim.py | 14 ++-- app/routers/search.py | 2 +- app/routers/search_ai.py | 4 +- app/routers/security.py | 8 +-- app/routers/sharing.py | 4 +- app/routers/sidebar_config.py | 2 +- app/routers/sites.py | 22 +++---- app/routers/sso.py | 6 +- app/routers/sync.py | 2 +- app/routers/web_clipper.py | 2 +- app/routers/webauthn.py | 6 +- app/routers/wiki.py | 34 +++++----- app/routers/workers.py | 12 ++-- app/routers/workspace.py | 38 +++++------ docs/openapi-v2.json | 2 +- 48 files changed, 379 insertions(+), 358 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70d9a61..bb5fea4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ # Changelog - FlowDeck +## v7.8.0 (2026-10-01) — Audit : A21 phase 1 (SQLite hors event loop) + +### Changed + +- **A21 (phase 1)** — **352 routes** `async def` sans aucun `await` converties + en `def` : FastAPI les exécute alors dans son threadpool — tout leur travail + SQLite (`get_conn()` + `conn.execute`) quitte l'event loop, **sans changer une + ligne de logique** (la conversion est sémantiquement neutre : vérifié corps + par corps — aucun `await`/`async with`/`async for`/`asyncio` dans les + fonctions converties). Répartition : api_v2 60, dashboard 40, collections 25, + board 23, workspace 19, wiki 17, permissions 14, api 14, + 35 autres fichiers +- **Reste (phase 2)** — les 311 routes qui ont de vrais `await` + (`request.json()`, `fire_event`, httpx) : enrouler les blocs DB dans + `await anyio.to_thread.run_sync(...)` ; aucun wrapper partagé livré pour + l'instant (rien ne l'appellerait) + +### Perf + +- Dernière suite : 233 s (écarts précédents mesurés : 235-359 s) — les + handlers SQLite ne saturent plus la boucle pendant les tests + ## v7.7.0 (2026-10-01) — Audit : A20 (CSP — nonce, partie 1) ### Security diff --git a/ROADMAP.md b/ROADMAP.md index 47947f2..967d4a0 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1138,7 +1138,7 @@ Quality DB views, Agent IA Palette → Realtime + E - [x] **A18 — Collection publiée quelconque + stocké XSS** : `GET /workspace/public/{collection_id}` (`workspace.py:699-719`) « no auth required », **ignore les flags `restricted/private`**, et interpole `coll['name']`/`p['title']` dans un `HTMLResponse(f"""…""")` sans `html.escape`. *Fix : respecter les flags de partage + `html.escape`. Effort : **S**.* - [x] **A19 — Liste CSRF trop large (34 préfixes) — TERMINÉ 2026-09-30 : 17 préfixes retirés, aucun préfixe cookie-auth n'est plus exempté** (12 en v7.3.5 après scan + 5 en v7.3.6 après équipement des 46 call sites) (`/db/`, `/workspace`, `/workspace/automations`, `/board/api/favorites`, `/api/pages`, `/api/recents`, `/api/notifications`, `/api/comments`, `/api/automations`, `/api/github`, `/api/admin`, `/api/onboarding`) puis les 5 derniers en v7.3.6 (46 call sites équipés de `X-CSRF-Token` dans agent_panel, settings, local_workspace, library, gitea_workspace, workspace, workspaces, welcome) — il ne reste que du Bearer/callback/public/infra : `csrf.py:21,25` couvre `/api/v2`, `/api/admin`, `/db/`, `/workspace`, `/api/user`, `/api/settings`, `/board/api/pages`, `/api/local-workspace`, `/api/comments`, `/api/agent`, `/api/automations`, `/auth/2fa` — tous **cookie-auth**. Seul `/scim/v2` est justifié par le commentaire de la ligne 19-20. Bonus : `/api/workspace` exempt aussi `/api/workspaces/*`. Filet restant = `SameSite=Lax` par défaut (jamais déclaré explicitement dans `main.py:150`). *Fix : garder un petit ensemble SAFE (webhooks, `/api/v1`, `/api/v2` Bearer, `/scim/v2`, callbacks OAuth/SSO) + ancrer les préfixes ; ajouter le header sur les 49 `fetch()` concernés (helper `csrfFetch` existe déjà : `base.html:892`). Effort : **M**.* - [x] **A20 — CSP sans filet — PARTIEL : `script-src 'unsafe-inline' 'unsafe-eval'`** (`security.py:67`) → aucun nonce/hash ; combiné à A10, chaque sink XSS ci-dessus tourne sans violation CSP. *Fix : externaliser le JS inline (A27), passer à `'nonce-…'`, retirer `'unsafe-eval'`, resserrer `img-src`/`connect-src`.* — **fait 2026-10-01 (nonce)** : `script-src 'self' 'unsafe-eval' 'nonce-'` — `unsafe-inline` retiré de script-src (fin des XSS injectés en JS) ; ContextVar `CSP_NONCE` posée par le middleware avant `call_next`, lue par `{{ csp_nonce() }}` (38 tags inline dans les templates + `_with_nonce()` pour la constante `LOCAL_LOGIN_HTML` + 3 scripts Python dans collections.py) ; htmx re-çoit le nonce via `` (réponses boostées) ; les 74 handlers `onclick=` restent couverts par `script-src-attr 'unsafe-inline'` ; chart.js/leaflet (CDN, déjà utilisés par les vues chart/map et BLOQUÉS par CSP depuis toujours) ajoutés à `script-src`/`style-src` avec commentaire `ponytail:` (upgrade : vendoriser). **Reste** : `unsafe-eval` (Alpine x-data en string → build `@alpinejs/csp`), externalisation JS (A27), resserrer `img-src`/`connect-src`, vendoriser les 2 CDN. Effort : **L**. -- [ ] **A21 — `sqlite3` synchrone sur l'event loop — PARTIEL 2026-10-01 : `PRAGMA busy_timeout=5000` ajouté au point d'entrée unique `get_conn()` (db.py)** ; reste le wrapper async `anyio.to_thread` + la migration des 510 call sites : `get_conn()` (`db.py:833-843`) est synchrone et **510 des 689 `async def` de routes** l'appellent (805 occurrences au total ; 0 `run_in_threadpool`, 1 seul `asyncio.to_thread` dans tout le dépôt : `semantic_search.py:262`) ; connexion neuve par requête (`connect` + 2 PRAGMA), **aucun `busy_timeout`**. Chaque requête bloque la boucle. *Fix : wrapper async (`anyio.to_thread.run_sync`) partagé, migrer d'abord `api_v2`/`dashboard`/`collections`/`board` + `PRAGMA busy_timeout=5000`. Effort : **M**.* +- [x] **A21 — `sqlite3` synchrone sur l'event loop — PARTIEL 2026-09-30 → phase 1 le 2026-10-01 : `PRAGMA busy_timeout=5000` ajouté au point d'entrée unique `get_conn()` (db.py)** ; reste le wrapper async `anyio.to_thread` + la migration des 510 call sites : `get_conn()` (`db.py:833-843`) est synchrone et **510 des 689 `async def` de routes** l'appellent (805 occurrences au total ; 0 `run_in_threadpool`, 1 seul `asyncio.to_thread` dans tout le dépôt : `semantic_search.py:262`) ; connexion neuve par requête (`connect` + 2 PRAGMA), **aucun `busy_timeout`**. Chaque requête bloque la boucle. *Fix : wrapper async (`anyio.to_thread.run_sync`) partagé, migrer d'abord `api_v2`/`dashboard`/`collections`/`board` + `PRAGMA busy_timeout=5000`.* — **phase 1 faite 2026-10-01** : **352 routes `async def` SANS aucun `await`** converties en `def` (scan corps par corps : ni `await`/`async with`/`async for`, ni `asyncio`) → FastAPI les exécute dans son threadpool, donc tout leur travail SQLite quitte l'event loop, sans changer une ligne de logique (api_v2 : 60, dashboard : 40, collections : 25, board : 23, + main.py : 6 ; aucune occurrence `asyncio`/`run_coroutine` dans les corps convertis). **Reste (phase 2)** : les **311 routes qui ont de vrais `await`** (`request.json()`, `fire_event`, httpx) → enrouler le bloc DB dans `await anyio.to_thread.run_sync(lambda: ...)` (pas de wrapper pour l'instant : rien ne l'appellerait encore). Effort : **M→L**. - [x] **A22 — Validateur d'upload = code mort** : `validate_upload()` + `ALLOWED_EXTENSIONS` + `MAX_UPLOAD_SIZE` (`security.py:33-40`) n'ont **aucun appelant** ; `upload_local_workspace_file` (`dashboard.py:1507-1574`) fait `file_path.write_bytes(content)` après seulement `Path(filename).name` — ni taille, ni extension, ni auth, et `/api/local-workspace` est exclu du CSRF. Chemin aussi codé en dur `Path(f"/data/uploads/...")` au lieu de `FLOWDECK_DATA_DIR`. *Fix : appeler `validate_upload()` avant `read()` + session. Effort : **S**.* - [x] **A23 — N+1 avérés** : `dashboard.py:905` (`COUNT(*) FROM pages` par page dans une boucle de 20), `board.py:2141-2146` (INSERT + `_extract_ai_keywords` par issue), `collections.py:378` (INSERT par propriété en boucle). *Fix : une agrégation `GROUP BY` / un `executemany`. Effort : **S**.* - [x] **A24 — 2 routes silencieusement mortes (vérifié en servant l'app)** : `GET /workspace` → `dashboard.py:820` (HTML, inclus 1er, `main.py:158`) écrase `workspace.py:30` **et** `library.py:537` (JSON, inclus après) ; `GET /api/projects` → `projects.py:24` (inclus 161) écrase `api.py:82` (handler Gitea). *Fix : supprimer/renommer les doublons + assertion au boot « pas deux routes même méthode+chemin ». Effort : **XS**.* @@ -1181,4 +1181,4 @@ Quality DB views, Agent IA Palette → Realtime + E → Puis **A3–A8** (le bloc « fallback admin ») d'un seul tenant, puis **A10** (autoescape) qui débloque A18/A20. *Audit produit le 2026-09-30 · 43 items · aucun code modifié ( ROADMAP seul ).* -→ **A1–A9 corrigés le 2026-09-30** : deps réinstallées (`pyotp`/`webauthn`/`cbor2`), rebinding de `settings` supprimé dans `test_v54.py` → **suite 1016/1016 verts**, cycle committé (`1706ad1`) + tag `v7.3.0` poussé, `.db`/fichiers de test désindexés, `APP_SECRET_KEY` roté dans `.env` (sessions révoquées) · **A3–A8 : 401 sans session sur les routes de compte (mdp actuel exigé), tokens `/api/v1` + `/api/user` sans session → 401, CRUD membres d'espace sous session+role admin, `_require_view`/`_require_edit` sans session → 404/401, création/lecture de page sous session, `/board/api/pages` + `/api/user` sortis du CSRF exempt, seed admin sans mdp en dur (aléatoire ou `FLOWDECK_ADMIN_PASSWORD`). Tests : client connecte par defaut (`_TestSessionAuth`), helper `anon()` sur les 40 tests d'anonymat → suite 1016/1016 + ruff OK, commit `d125eb3` · **A10 : `app/templating.py` (ENV partagé + autoescape `select_autoescape(["html"])`) remplace les 29 instantiations, `|safe` retriés (corps d'issue/commentaires echappes, `sidebar_config` en `|tojson`) → suite 1016/1016, version 7.3.1 · **A11 (traversal avatar) + A18 (vue publique : 404 restricted/private + html.escape)** : `tests/test_audit_p0_fixes.py`, suite 1019/1019, version 7.3.2 · **A12/A13/A15/A17/A22/A23/A24** : garde SSRF par hop, routers automations + /api sous session/Bearer, webhooks admin+URL publique, uploads validés, N+1 en GROUP BY/executemany, 2 doublons de routes supprimés → suite 1025/1025, version 7.3.3 · **A16** : exports + pièces jointes sous session + `can_view_page` → suite 1026/1026, version 7.3.4 · **A19 (partiel)** : 12 préfixes CSRF retirés après scan front (12 préfixes = tous ceux dont les appels portent déjà le header), reste 5 préfixes / 49 fetchs → suite 1026/1026, version 7.3.5 · **A19 terminé** : 46 call sites front équipés, plus aucun préfixe cookie-auth exempté → suite 1026/1026, version 7.3.6 · **A14** : fallback `admin` supprimé de `_current_user_id`/`_current_admin` (401 sans session), `api_base` validé scheme http(s) sans identifiants (hôtes privés acceptés = Ollama localhost, commentaire `ponytail:`) → suite 1027/1027, version 7.3.7 · **A25** : 84 `except Exception: pass` remplacés par `logger.exception(fn)` (19 fichiers, +`logger` manquants), try supprimé sur `materialize_properties` dans `create_collection_v2` et `apply_db_template_v2` (rollback au lieu d'un commit sans schéma), test de rollback · **A21 (partiel)** : `busy_timeout=5000` dans `get_conn()` → suite 1028/1028, version 7.3.8 · **A26/A33/A34/A35/A36/A43** : secret par défaut refusé au boot, rate limit (préfixes + settings + XFF + épurage), `_spawn()` pour les 10 schedulers, OpenAPI 511 chemins + README, 4 deps mortes purgées, 15 `utcnow()` → `now(UTC)` naïf → suite 1028/1028, version 7.3.9. · **A30/A37/A39/A40/A41** : `require_scope` câblé sur 69 sites + 12 fonctions mortes supprimées, CORS sans `*` (origines de `app_base_url` + regex dev/extensions), assets versionnés depuis `VERSION` (source unique), `app.css` -10,2 Ko de règles mortes, htmx = décision « rien » documentée → suite 1031/1031, version 7.4.0. · **A29/A42** : `services/publish.py` partagé (3 routers déléguent, 404 partout, board sous session), fuite `password_hash` corrigée sur `GET /api/users/me` v1 + contexte `/accounts`, `settings.data_dir` remplace les 9 copies d'env, cache Gitea évacue les expirés ; `/users/me` ×2 + collections ×3 = contrats versionnés, on garde ; reste A42 = client httpx partagé → suite 1034/1034, version 7.5.0. · **A31** : transaction par migration (`_apply_one`, rollback tout-ou-rien du DDL) + helper `columns()` remplaçant 25 copies de `PRAGMA table_info` (1 helper au lieu de 3 — les 2 autres seraient mort-nés) → suite 1036/1036, version 7.6.0. · **A20 (partiel)** : CSP nonce par requête — `unsafe-inline` retiré de `script-src`, 38 scripts templates + login constant + 3 scripts Python noncés, meta `htmx-config` pour htmx, `script-src-attr` pour les 74 `onclick=`, CDN chart/leaflet débloqués (déjà cassés avant) → suite 1037/1037, version 7.7.0. +→ **A1–A9 corrigés le 2026-09-30** : deps réinstallées (`pyotp`/`webauthn`/`cbor2`), rebinding de `settings` supprimé dans `test_v54.py` → **suite 1016/1016 verts**, cycle committé (`1706ad1`) + tag `v7.3.0` poussé, `.db`/fichiers de test désindexés, `APP_SECRET_KEY` roté dans `.env` (sessions révoquées) · **A3–A8 : 401 sans session sur les routes de compte (mdp actuel exigé), tokens `/api/v1` + `/api/user` sans session → 401, CRUD membres d'espace sous session+role admin, `_require_view`/`_require_edit` sans session → 404/401, création/lecture de page sous session, `/board/api/pages` + `/api/user` sortis du CSRF exempt, seed admin sans mdp en dur (aléatoire ou `FLOWDECK_ADMIN_PASSWORD`). Tests : client connecte par defaut (`_TestSessionAuth`), helper `anon()` sur les 40 tests d'anonymat → suite 1016/1016 + ruff OK, commit `d125eb3` · **A10 : `app/templating.py` (ENV partagé + autoescape `select_autoescape(["html"])`) remplace les 29 instantiations, `|safe` retriés (corps d'issue/commentaires echappes, `sidebar_config` en `|tojson`) → suite 1016/1016, version 7.3.1 · **A11 (traversal avatar) + A18 (vue publique : 404 restricted/private + html.escape)** : `tests/test_audit_p0_fixes.py`, suite 1019/1019, version 7.3.2 · **A12/A13/A15/A17/A22/A23/A24** : garde SSRF par hop, routers automations + /api sous session/Bearer, webhooks admin+URL publique, uploads validés, N+1 en GROUP BY/executemany, 2 doublons de routes supprimés → suite 1025/1025, version 7.3.3 · **A16** : exports + pièces jointes sous session + `can_view_page` → suite 1026/1026, version 7.3.4 · **A19 (partiel)** : 12 préfixes CSRF retirés après scan front (12 préfixes = tous ceux dont les appels portent déjà le header), reste 5 préfixes / 49 fetchs → suite 1026/1026, version 7.3.5 · **A19 terminé** : 46 call sites front équipés, plus aucun préfixe cookie-auth exempté → suite 1026/1026, version 7.3.6 · **A14** : fallback `admin` supprimé de `_current_user_id`/`_current_admin` (401 sans session), `api_base` validé scheme http(s) sans identifiants (hôtes privés acceptés = Ollama localhost, commentaire `ponytail:`) → suite 1027/1027, version 7.3.7 · **A25** : 84 `except Exception: pass` remplacés par `logger.exception(fn)` (19 fichiers, +`logger` manquants), try supprimé sur `materialize_properties` dans `create_collection_v2` et `apply_db_template_v2` (rollback au lieu d'un commit sans schéma), test de rollback · **A21 (partiel)** : `busy_timeout=5000` dans `get_conn()` → suite 1028/1028, version 7.3.8 · **A26/A33/A34/A35/A36/A43** : secret par défaut refusé au boot, rate limit (préfixes + settings + XFF + épurage), `_spawn()` pour les 10 schedulers, OpenAPI 511 chemins + README, 4 deps mortes purgées, 15 `utcnow()` → `now(UTC)` naïf → suite 1028/1028, version 7.3.9. · **A30/A37/A39/A40/A41** : `require_scope` câblé sur 69 sites + 12 fonctions mortes supprimées, CORS sans `*` (origines de `app_base_url` + regex dev/extensions), assets versionnés depuis `VERSION` (source unique), `app.css` -10,2 Ko de règles mortes, htmx = décision « rien » documentée → suite 1031/1031, version 7.4.0. · **A29/A42** : `services/publish.py` partagé (3 routers déléguent, 404 partout, board sous session), fuite `password_hash` corrigée sur `GET /api/users/me` v1 + contexte `/accounts`, `settings.data_dir` remplace les 9 copies d'env, cache Gitea évacue les expirés ; `/users/me` ×2 + collections ×3 = contrats versionnés, on garde ; reste A42 = client httpx partagé → suite 1034/1034, version 7.5.0. · **A31** : transaction par migration (`_apply_one`, rollback tout-ou-rien du DDL) + helper `columns()` remplaçant 25 copies de `PRAGMA table_info` (1 helper au lieu de 3 — les 2 autres seraient mort-nés) → suite 1036/1036, version 7.6.0. · **A20 (partiel)** : CSP nonce par requête — `unsafe-inline` retiré de `script-src`, 38 scripts templates + login constant + 3 scripts Python noncés, meta `htmx-config` pour htmx, `script-src-attr` pour les 74 `onclick=`, CDN chart/leaflet débloqués (déjà cassés avant) → suite 1037/1037, version 7.7.0. · **A21 phase 1** : 352 routes `async def` sans `await` → `def` (threadpool FastAPI, SQLite hors loop, zéro changement de logique — scan corps par corps) ; reste phase 2 = 311 routes avec `await` → `anyio.to_thread.run_sync` par bloc DB → suite 1037/1037, version 7.8.0. diff --git a/VERSION b/VERSION index 1985849..09a6d30 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -7.7.0 +7.8.0 diff --git a/WORKLOAD.md b/WORKLOAD.md index 19950d6..1c49d5a 100644 --- a/WORKLOAD.md +++ b/WORKLOAD.md @@ -1,6 +1,6 @@ # WORKLOAD — FlowDeck Notion Clone -> **Début**: 2026-07-08 | **Version**: v7.7.0 (audit — A20 partiel : nonce CSP) | **Statut**: EN COURS 🔄 +> **Début**: 2026-07-08 | **Version**: v7.8.0 (audit — A21 phase 1 : 352 routes hors loop) | **Statut**: EN COURS 🔄 > **Cible**: parité Notion + intégration forge · **Follow-ups v7.3 livrés**: sidebar teamspaces, notif `page.updated`, charts `number` + dashboards multi-DB, unfurl forge, UI Settings → Audit — voir `ROADMAP.md § v7.3.0` ## Avancement Global diff --git a/app/main.py b/app/main.py index e421bf3..facddf4 100644 --- a/app/main.py +++ b/app/main.py @@ -185,7 +185,7 @@ async def lifespan(_app: FastAPI): app = FastAPI( title="FlowDeck", - version="7.7.0", + version="7.8.0", docs_url="/docs", redoc_url="/redoc", lifespan=lifespan, @@ -265,14 +265,14 @@ app.mount("/static", StaticFiles(directory="static"), name="static") @app.get("/manifest.json") -async def pwa_manifest(): +def pwa_manifest(): """Serve the static web manifest from disk (same URL as before v6.0.0).""" from fastapi.responses import FileResponse return FileResponse("static/manifest.json", media_type="application/manifest+json") @app.get("/sw.js") -async def service_worker(): +def service_worker(): """Serve the PWA service worker at top-level scope (/).""" from fastapi.responses import FileResponse return FileResponse("static/sw.js", media_type="application/javascript") @@ -282,7 +282,7 @@ async def service_worker(): @app.get("/api/csrf-token") -async def csrf_token_endpoint(request: Request): +def csrf_token_endpoint(request: Request): """Return a fresh CSRF token. Used by the frontend to auto-recover from 403.""" import secrets @@ -297,7 +297,7 @@ async def csrf_token_endpoint(request: Request): @app.get("/api/pages") -async def api_pages_alias(request: Request): +def api_pages_alias(request: Request): """Alias /api/pages → /board/api/pages for API path consistency.""" from fastapi.responses import RedirectResponse qs = str(request.url.query) @@ -306,7 +306,7 @@ async def api_pages_alias(request: Request): @app.post("/api/pages") -async def api_pages_post_alias(request: Request): +def api_pages_post_alias(request: Request): """Alias POST /api/pages → /board/api/pages for API path consistency.""" from fastapi.responses import RedirectResponse return RedirectResponse(url="/board/api/pages", status_code=307) @@ -341,7 +341,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;backgrou @app.exception_handler(_StarHTTPException) -async def http_exception_handler(request: Request, exc: _StarHTTPException): +def http_exception_handler(request: Request, exc: _StarHTTPException): """Unified handler: RFC7807 for /api/v2, JSON for other /api, redirect for HTML. Registered on Starlette's HTTPException (the base class) so it catches both diff --git a/app/routers/admin.py b/app/routers/admin.py index 6846189..c24fd42 100644 --- a/app/routers/admin.py +++ b/app/routers/admin.py @@ -23,7 +23,7 @@ async def admin_required(request: Request): # ── Users ── @router.get("/users") -async def list_users(_admin=Depends(admin_required)): +def list_users(_admin=Depends(admin_required)): """List all users with workspace/file/folder counts and storage usage.""" from app.db import get_conn with get_conn() as conn: @@ -109,7 +109,7 @@ async def update_user(user_id: int, request: Request, _admin=Depends(admin_requi @router.delete("/users/{user_id:int}") -async def delete_user(user_id: int, _admin=Depends(admin_required)): +def delete_user(user_id: int, _admin=Depends(admin_required)): """Delete a user and cascade their data.""" from app.db import get_conn with get_conn() as conn: @@ -139,7 +139,7 @@ async def delete_user(user_id: int, _admin=Depends(admin_required)): # ── Stats ── @router.get("/stats") -async def user_stats(_admin=Depends(admin_required)): +def user_stats(_admin=Depends(admin_required)): """Aggregate stats: total users, workspaces, files, storage.""" from app.db import get_conn with get_conn() as conn: @@ -159,7 +159,7 @@ async def user_stats(_admin=Depends(admin_required)): # ── Audit ── @router.get("/audit") -async def audit_log(limit: int = 100, _admin=Depends(admin_required)): +def audit_log(limit: int = 100, _admin=Depends(admin_required)): """Recent login history.""" from app.db import get_conn with get_conn() as conn: diff --git a/app/routers/agent.py b/app/routers/agent.py index 88f4bd3..7fcbdb0 100644 --- a/app/routers/agent.py +++ b/app/routers/agent.py @@ -213,7 +213,7 @@ async def create_conversation(request: Request): @router.get("/conversations/{conversation_id}") -async def get_conversation(request: Request, conversation_id: int): +def get_conversation(request: Request, conversation_id: int): with get_conn() as conn: conv = conn.execute("SELECT * FROM agent_conversations WHERE id=?", (conversation_id,)).fetchone() if not conv: @@ -226,7 +226,7 @@ async def get_conversation(request: Request, conversation_id: int): @router.delete("/conversations/{conversation_id}") -async def delete_conversation(request: Request, conversation_id: int): +def delete_conversation(request: Request, conversation_id: int): with get_conn() as conn: if not conn.execute("SELECT id FROM agent_conversations WHERE id=?", (conversation_id,)).fetchone(): raise HTTPException(status_code=404, detail="Conversation introuvable") @@ -450,7 +450,7 @@ async def agent_writing_properties(request: Request): @router.get("/conversations/{conversation_id}/actions") -async def list_actions(request: Request, conversation_id: int): +def list_actions(request: Request, conversation_id: int): with get_conn() as conn: rows = conn.execute( "SELECT * FROM agent_actions WHERE conversation_id=? ORDER BY created_at, id", @@ -460,7 +460,7 @@ async def list_actions(request: Request, conversation_id: int): @router.post("/actions/{action_id}/undo") -async def undo(request: Request, action_id: int): +def undo(request: Request, action_id: int): try: undo_action(action_id) except ValueError as exc: @@ -528,7 +528,7 @@ async def apply_skill(request: Request, skill_id: int): @router.get("/skills/gallery") -async def skills_gallery(request: Request): +def skills_gallery(request: Request): presets = skill_gallery.list_gallery() return {"gallery": presets, "total": len(presets), "install": "POST /api/agent/skills/gallery/{slug}/install"} @@ -577,7 +577,7 @@ async def import_skill(request: Request): @router.get("/skills/{skill_id}/export") -async def export_skill(request: Request, skill_id: int): +def export_skill(request: Request, skill_id: int): """Document JSON portable — à rejouer sur /api/agent/skills/import.""" with get_conn() as conn: row = conn.execute("SELECT * FROM agent_skills WHERE id=?", (skill_id,)).fetchone() @@ -587,7 +587,7 @@ async def export_skill(request: Request, skill_id: int): @router.delete("/skills/{skill_id}") -async def delete_skill(request: Request, skill_id: int): +def delete_skill(request: Request, skill_id: int): with get_conn() as conn: row = conn.execute("SELECT name FROM agent_skills WHERE id=?", (skill_id,)).fetchone() if not row: @@ -794,7 +794,7 @@ async def trigger_agent(request: Request, agent_id: int): @router.get("/tools") -async def list_tools(request: Request): +def list_tools(request: Request): registry = ToolRegistry() tools = registry.schema() return {"tools": tools} @@ -1081,7 +1081,7 @@ async def test_provider_config(request: Request): @router.get("/{agent_id}") -async def get_agent(request: Request, agent_id: int): +def get_agent(request: Request, agent_id: int): with get_conn() as conn: row = conn.execute("SELECT * FROM agents WHERE id=?", (agent_id,)).fetchone() if not row: @@ -1116,7 +1116,7 @@ async def update_agent(request: Request, agent_id: int): @router.delete("/{agent_id}") -async def delete_agent(request: Request, agent_id: int): +def delete_agent(request: Request, agent_id: int): with get_conn() as conn: existing = conn.execute("SELECT * FROM agents WHERE id=?", (agent_id,)).fetchone() if not existing: diff --git a/app/routers/api.py b/app/routers/api.py index 3032a65..d454baa 100644 --- a/app/routers/api.py +++ b/app/routers/api.py @@ -83,7 +83,7 @@ async def health(request: Request): @router.get("/stats") -async def stats(): +def stats(): """Global stats for dashboard.""" with get_conn() as conn: board_count = conn.execute("SELECT COUNT(*) as c FROM boards").fetchone()["c"] @@ -179,7 +179,7 @@ async def _get_status_labels(owner: str, repo: str, board_id: int) -> list[str]: @router.post("/col-mapping") -async def set_col_mapping( +def set_col_mapping( owner: str = Query(...), repo: str = Query(...), column: str = Query(...), @@ -209,7 +209,7 @@ async def set_col_mapping( @router.delete("/col-mapping") -async def delete_col_mapping( +def delete_col_mapping( owner: str = Query(...), repo: str = Query(...), column: str = Query(...), @@ -234,7 +234,7 @@ async def delete_col_mapping( @router.get("/board-config/{owner}/{repo}") -async def get_board_config(owner: str, repo: str): +def get_board_config(owner: str, repo: str): with get_conn() as conn: board = conn.execute( "SELECT * FROM boards WHERE project_owner=? AND project_name=?", @@ -255,7 +255,7 @@ async def get_board_config(owner: str, repo: str): @router.post("/board-config/{owner}/{repo}") -async def update_board_config( +def update_board_config( owner: str, repo: str, columns: str = Query(default=""), @@ -460,7 +460,7 @@ async def get_issue_detail(owner: str, repo: str, issue_id: int, format: str = Q # ── v0.5.0: Checklists ── @router.post("/checklists/{owner}/{repo}/{issue_id}") -async def create_checklist( +def create_checklist( owner: str, repo: str, issue_id: int, @@ -484,7 +484,7 @@ async def create_checklist( @router.post("/checklist-items/{owner}/{repo}/{issue_id}/{checklist_id}") -async def add_checklist_item( +def add_checklist_item( owner: str, repo: str, issue_id: int, @@ -502,7 +502,7 @@ async def add_checklist_item( @router.patch("/checklist-items/{item_id}") -async def toggle_checklist_item( +def toggle_checklist_item( item_id: int, checked: bool = Query(default=False), content: str = Query(default=""), @@ -524,7 +524,7 @@ async def toggle_checklist_item( @router.delete("/checklist-items/{item_id}") -async def delete_checklist_item(item_id: int): +def delete_checklist_item(item_id: int): """Delete a checklist item.""" with get_conn() as conn: conn.execute("DELETE FROM checklist_items WHERE id=?", (item_id,)) @@ -533,7 +533,7 @@ async def delete_checklist_item(item_id: int): @router.delete("/checklists/{checklist_id}") -async def delete_checklist(checklist_id: int): +def delete_checklist(checklist_id: int): """Delete a checklist and all its items.""" with get_conn() as conn: conn.execute("DELETE FROM checklist_items WHERE checklist_id=?", (checklist_id,)) @@ -545,7 +545,7 @@ async def delete_checklist(checklist_id: int): # ── v1.0.0: User management ── @router.get("/users/me") -async def get_my_profile(request: Request): +def get_my_profile(request: Request): """Get current user profile.""" user = SessionManager.decode_session(request.cookies.get("flowdeck_session", "")) if not user: @@ -565,7 +565,7 @@ async def get_my_profile(request: Request): @router.put("/users/me") -async def update_my_profile(request: Request, full_name: str = Query(default=""), +def update_my_profile(request: Request, full_name: str = Query(default=""), email: str = Query(default="")): """Update current user's local profile.""" user = SessionManager.decode_session(request.cookies.get("flowdeck_session", "")) @@ -584,7 +584,7 @@ async def update_my_profile(request: Request, full_name: str = Query(default="") # ── v0.5.0: Card priority & due date ── @router.post("/card/{owner}/{repo}/{issue_id}") -async def update_card( +def update_card( owner: str, repo: str, issue_id: int, @@ -673,7 +673,7 @@ async def capture_frontend_error(request: Request): @router.get("/frontend-errors") -async def get_frontend_errors(request: Request, clear: bool = True): +def get_frontend_errors(request: Request, clear: bool = True): """Retourne les erreurs frontend récentes. Hermes appelle cette route.""" errors = list(_frontend_errors) if clear: diff --git a/app/routers/api_v2.py b/app/routers/api_v2.py index 80846cc..36162c0 100644 --- a/app/routers/api_v2.py +++ b/app/routers/api_v2.py @@ -93,7 +93,7 @@ async def create_token(request: Request, authorization: str | None = Header(defa return data @router.get("/tokens") -async def list_tokens(request: Request, authorization: str | None = Header(default=None)): +def list_tokens(request: Request, authorization: str | None = Header(default=None)): user = get_bearer_user(request, authorization) _v2_rate_check(request, user) with get_conn() as conn: @@ -109,7 +109,7 @@ async def list_tokens(request: Request, authorization: str | None = Header(defau return {"tokens": out} @router.delete("/tokens/{token_id}") -async def revoke_token(token_id: int, request: Request, authorization: str | None = Header(default=None)): +def revoke_token(token_id: int, request: Request, authorization: str | None = Header(default=None)): user = require_scope("write")(request, authorization) _v2_rate_check(request, user) with get_conn() as conn: @@ -124,7 +124,7 @@ async def revoke_token(token_id: int, request: Request, authorization: str | Non return {"id": token_id, "status": "revoked"} @router.post("/tokens/{token_id}/rotate") -async def rotate_token(token_id: int, request: Request, authorization: str | None = Header(default=None)): +def rotate_token(token_id: int, request: Request, authorization: str | None = Header(default=None)): user = require_scope("write")(request, authorization) _v2_rate_check(request, user) with get_conn() as conn: @@ -147,7 +147,7 @@ async def rotate_token(token_id: int, request: Request, authorization: str | Non # ── Users ───────────────────────────────────────────────────────────────── @router.get("/users/me") -async def get_me(request: Request, authorization: str | None = Header(default=None)): +def get_me(request: Request, authorization: str | None = Header(default=None)): user = get_bearer_user(request, authorization) _v2_rate_check(request, user) with get_conn() as conn: @@ -199,7 +199,7 @@ async def patch_me(request: Request, authorization: str | None = Header(default= return row_to_dict(row) @router.get("/users/search") -async def search_users(request: Request, q: str = "", authorization: str | None = Header(default=None)): +def search_users(request: Request, q: str = "", authorization: str | None = Header(default=None)): user = get_bearer_user(request, authorization) _v2_rate_check(request, user) q = (q or request.query_params.get("q") or "").strip() @@ -213,7 +213,7 @@ async def search_users(request: Request, q: str = "", authorization: str | None # ── Workspaces ──────────────────────────────────────────────────────────── @router.get("/workspaces") -async def list_workspaces(request: Request, authorization: str | None = Header(default=None)): +def list_workspaces(request: Request, authorization: str | None = Header(default=None)): user = get_bearer_user(request, authorization) _v2_rate_check(request, user) limit, offset = parse_pagination(request) @@ -264,7 +264,7 @@ async def create_workspace(request: Request, authorization: str | None = Header( return JSONResponse(content=data, status_code=201) @router.get("/workspaces/{workspace_id}") -async def get_workspace(workspace_id: int, request: Request, authorization: str | None = Header(default=None)): +def get_workspace(workspace_id: int, request: Request, authorization: str | None = Header(default=None)): user = get_bearer_user(request, authorization) _v2_rate_check(request, user) with get_conn() as conn: @@ -310,7 +310,7 @@ async def patch_workspace(workspace_id: int, request: Request, authorization: st return {"id": workspace_id, "status": "updated"} @router.delete("/workspaces/{workspace_id}") -async def delete_workspace(workspace_id: int, request: Request, authorization: str | None = Header(default=None)): +def delete_workspace(workspace_id: int, request: Request, authorization: str | None = Header(default=None)): user = require_scope("write")(request, authorization) _v2_rate_check(request, user) with get_conn() as conn: @@ -325,7 +325,7 @@ async def delete_workspace(workspace_id: int, request: Request, authorization: s return {"id": workspace_id, "status": "deleted"} @router.get("/workspaces/{workspace_id}/members") -async def list_workspace_members(workspace_id: int, request: Request, authorization: str | None = Header(default=None)): +def list_workspace_members(workspace_id: int, request: Request, authorization: str | None = Header(default=None)): user = get_bearer_user(request, authorization) _v2_rate_check(request, user) with get_conn() as conn: @@ -400,7 +400,7 @@ async def update_member_role(workspace_id: int, uid: int, request: Request, auth return {"workspace_id": workspace_id, "user_id": uid, "role": role, "status": "updated"} @router.delete("/workspaces/{workspace_id}/members/{uid}") -async def remove_member(workspace_id: int, uid: int, request: Request, authorization: str | None = Header(default=None)): +def remove_member(workspace_id: int, uid: int, request: Request, authorization: str | None = Header(default=None)): user = require_scope("write")(request, authorization) _v2_rate_check(request, user) with get_conn() as conn: @@ -419,7 +419,7 @@ async def remove_member(workspace_id: int, uid: int, request: Request, authoriza # ── Collections ─────────────────────────────────────────────────────────── @router.get("/collections") -async def list_collections_v2(request: Request, authorization: str | None = Header(default=None)): +def list_collections_v2(request: Request, authorization: str | None = Header(default=None)): user = get_bearer_user(request, authorization) _v2_rate_check(request, user) limit, offset = parse_pagination(request) @@ -494,7 +494,7 @@ async def create_collection_v2(request: Request, authorization: str | None = Hea return JSONResponse(content=data, status_code=201) @router.get("/collections/{collection_id}") -async def get_collection_v2(collection_id: int, request: Request, authorization: str | None = Header(default=None)): +def get_collection_v2(collection_id: int, request: Request, authorization: str | None = Header(default=None)): user = get_bearer_user(request, authorization) _v2_rate_check(request, user) with get_conn() as conn: @@ -532,7 +532,7 @@ async def patch_collection_v2(collection_id: int, request: Request, authorizatio return {"id": collection_id, "status": "updated"} @router.delete("/collections/{collection_id}") -async def delete_collection_v2(collection_id: int, request: Request, authorization: str | None = Header(default=None)): +def delete_collection_v2(collection_id: int, request: Request, authorization: str | None = Header(default=None)): user = require_scope("write")(request, authorization) _v2_rate_check(request, user) with get_conn() as conn: @@ -582,7 +582,7 @@ async def create_linked_db(collection_id: int, request: Request, authorization: return {"id": nid, "name": name, "status": "created"} @router.post("/collections/{collection_id}/task") -async def toggle_task(collection_id: int, request: Request, authorization: str | None = Header(default=None)): +def toggle_task(collection_id: int, request: Request, authorization: str | None = Header(default=None)): user = require_scope("write")(request, authorization) _v2_rate_check(request, user) with get_conn() as conn: @@ -597,7 +597,7 @@ async def toggle_task(collection_id: int, request: Request, authorization: str | return {"id": collection_id, "is_task": bool(new_val)} @router.get("/collections/{collection_id}/sources") -async def list_sources(collection_id: int, request: Request, authorization: str | None = Header(default=None)): +def list_sources(collection_id: int, request: Request, authorization: str | None = Header(default=None)): user = get_bearer_user(request, authorization) _v2_rate_check(request, user) with get_conn() as conn: @@ -631,7 +631,7 @@ async def add_source(collection_id: int, request: Request, authorization: str | return {"collection_id": collection_id, "source_collection_id": src_id, "status": "added"} @router.delete("/collections/{collection_id}/sources/{source_id}") -async def remove_source(collection_id: int, source_id: int, request: Request, authorization: str | None = Header(default=None)): +def remove_source(collection_id: int, source_id: int, request: Request, authorization: str | None = Header(default=None)): user = require_scope("write")(request, authorization) _v2_rate_check(request, user) with get_conn() as conn: @@ -643,7 +643,7 @@ async def remove_source(collection_id: int, source_id: int, request: Request, au # ── Collection pages ────────────────────────────────────────────────────── @router.get("/collections/{collection_id}/pages") -async def list_collection_pages_v2(collection_id: int, request: Request, authorization: str | None = Header(default=None)): +def list_collection_pages_v2(collection_id: int, request: Request, authorization: str | None = Header(default=None)): user = get_bearer_user(request, authorization) _v2_rate_check(request, user) limit, offset = parse_pagination(request) @@ -753,7 +753,7 @@ async def create_collection_page_v2(collection_id: int, request: Request, author return JSONResponse(content=data, status_code=201) @router.get("/pages/{page_id}") -async def get_page_v2(page_id: int, request: Request, authorization: str | None = Header(default=None)): +def get_page_v2(page_id: int, request: Request, authorization: str | None = Header(default=None)): user = get_bearer_user(request, authorization) _v2_rate_check(request, user) with get_conn() as conn: @@ -881,7 +881,7 @@ async def move_page_v2(page_id: int, request: Request, authorization: str | None return {"id": page_id, "status": "moved"} @router.get("/pages/{page_id}/sub-items") -async def list_sub_items_v2(page_id: int, request: Request, authorization: str | None = Header(default=None)): +def list_sub_items_v2(page_id: int, request: Request, authorization: str | None = Header(default=None)): user = get_bearer_user(request, authorization) _v2_rate_check(request, user) with get_conn() as conn: @@ -912,7 +912,7 @@ async def create_sub_item_v2(page_id: int, request: Request, authorization: str return {"id": nid, "status": "created"} @router.get("/pages/{page_id}/dependencies") -async def list_dependencies_v2(page_id: int, request: Request, authorization: str | None = Header(default=None)): +def list_dependencies_v2(page_id: int, request: Request, authorization: str | None = Header(default=None)): user = get_bearer_user(request, authorization) _v2_rate_check(request, user) with get_conn() as conn: @@ -945,7 +945,7 @@ async def add_dependency_v2(page_id: int, request: Request, authorization: str | return {"status": "added"} @router.delete("/pages/{page_id}/dependencies/{dep_id}") -async def remove_dependency_v2(page_id: int, dep_id: int, request: Request, authorization: str | None = Header(default=None)): +def remove_dependency_v2(page_id: int, dep_id: int, request: Request, authorization: str | None = Header(default=None)): user = require_scope("write")(request, authorization) _v2_rate_check(request, user) with get_conn() as conn: @@ -957,7 +957,7 @@ async def remove_dependency_v2(page_id: int, dep_id: int, request: Request, auth # ── Properties ──────────────────────────────────────────────────────────── @router.get("/collections/{collection_id}/properties") -async def list_properties_v2(collection_id: int, request: Request, authorization: str | None = Header(default=None)): +def list_properties_v2(collection_id: int, request: Request, authorization: str | None = Header(default=None)): user = get_bearer_user(request, authorization) _v2_rate_check(request, user) with get_conn() as conn: @@ -1016,7 +1016,7 @@ async def patch_property_v2(prop_id: int, request: Request, authorization: str | return {"id": prop_id, "status": "updated"} @router.delete("/properties/{prop_id}") -async def delete_property_v2(prop_id: int, request: Request, authorization: str | None = Header(default=None)): +def delete_property_v2(prop_id: int, request: Request, authorization: str | None = Header(default=None)): user = require_scope("write")(request, authorization) _v2_rate_check(request, user) with get_conn() as conn: @@ -1093,7 +1093,7 @@ async def compute_rollup_v2(request: Request, authorization: str | None = Header # ── Views & dashboards ──────────────────────────────────────────────────── @router.get("/collections/{collection_id}/views") -async def list_views_v2(collection_id: int, request: Request, authorization: str | None = Header(default=None)): +def list_views_v2(collection_id: int, request: Request, authorization: str | None = Header(default=None)): user = get_bearer_user(request, authorization) _v2_rate_check(request, user) with get_conn() as conn: @@ -1157,7 +1157,7 @@ async def patch_view_v2(view_id: int, request: Request, authorization: str | Non return {"id": view_id, "status": "updated"} @router.delete("/views/{view_id}") -async def delete_view_v2(view_id: int, request: Request, authorization: str | None = Header(default=None)): +def delete_view_v2(view_id: int, request: Request, authorization: str | None = Header(default=None)): user = require_scope("write")(request, authorization) _v2_rate_check(request, user) with get_conn() as conn: @@ -1188,7 +1188,7 @@ async def save_as_view_v2(view_id: int, request: Request, authorization: str | N return {"id": nid, "name": name, "status": "created"} @router.get("/collections/{collection_id}/dashboards") -async def list_dashboards_v2(collection_id: int, request: Request, authorization: str | None = Header(default=None)): +def list_dashboards_v2(collection_id: int, request: Request, authorization: str | None = Header(default=None)): user = get_bearer_user(request, authorization) _v2_rate_check(request, user) with get_conn() as conn: @@ -1234,7 +1234,7 @@ async def patch_dashboard_v2(dashboard_id: int, request: Request, authorization: return {"id": dashboard_id, "status": "updated"} @router.delete("/dashboards/{dashboard_id}") -async def delete_dashboard_v2(dashboard_id: int, request: Request, authorization: str | None = Header(default=None)): +def delete_dashboard_v2(dashboard_id: int, request: Request, authorization: str | None = Header(default=None)): user = require_scope("write")(request, authorization) _v2_rate_check(request, user) with get_conn() as conn: @@ -1245,7 +1245,7 @@ async def delete_dashboard_v2(dashboard_id: int, request: Request, authorization # ── Comments & mentions ─────────────────────────────────────────────────── @router.get("/pages/{page_id}/comments") -async def list_comments_v2(page_id: int, request: Request, authorization: str | None = Header(default=None)): +def list_comments_v2(page_id: int, request: Request, authorization: str | None = Header(default=None)): user = get_bearer_user(request, authorization) _v2_rate_check(request, user) limit, offset = parse_pagination(request) @@ -1304,7 +1304,7 @@ async def patch_comment_v2(comment_id: int, request: Request, authorization: str return {"id": comment_id, "status": "updated"} @router.delete("/comments/{comment_id}") -async def delete_comment_v2(comment_id: int, request: Request, authorization: str | None = Header(default=None)): +def delete_comment_v2(comment_id: int, request: Request, authorization: str | None = Header(default=None)): user = require_scope("write")(request, authorization) _v2_rate_check(request, user) with get_conn() as conn: @@ -1349,7 +1349,7 @@ async def create_mention_v2(page_id: int, request: Request, authorization: str | # ── Notifications ───────────────────────────────────────────────────────── @router.get("/notifications") -async def list_notifications_v2(request: Request, authorization: str | None = Header(default=None)): +def list_notifications_v2(request: Request, authorization: str | None = Header(default=None)): user = get_bearer_user(request, authorization) _v2_rate_check(request, user) limit, offset = parse_pagination(request) @@ -1364,7 +1364,7 @@ async def list_notifications_v2(request: Request, authorization: str | None = He return {"notifications": [row_to_dict(r) for r in rows], "total": total, "limit": limit, "offset": offset} @router.get("/notifications/unread-count") -async def unread_count(request: Request, authorization: str | None = Header(default=None)): +def unread_count(request: Request, authorization: str | None = Header(default=None)): user = get_bearer_user(request, authorization) _v2_rate_check(request, user) with get_conn() as conn: @@ -1372,7 +1372,7 @@ async def unread_count(request: Request, authorization: str | None = Header(defa return {"unread": cnt} @router.post("/notifications/{notif_id}/read") -async def mark_read(notif_id: int, request: Request, authorization: str | None = Header(default=None)): +def mark_read(notif_id: int, request: Request, authorization: str | None = Header(default=None)): user = get_bearer_user(request, authorization) _v2_rate_check(request, user) with get_conn() as conn: @@ -1381,7 +1381,7 @@ async def mark_read(notif_id: int, request: Request, authorization: str | None = return {"id": notif_id, "status": "read"} @router.post("/notifications/read-all") -async def mark_all_read(request: Request, authorization: str | None = Header(default=None)): +def mark_all_read(request: Request, authorization: str | None = Header(default=None)): user = get_bearer_user(request, authorization) _v2_rate_check(request, user) with get_conn() as conn: @@ -1411,7 +1411,7 @@ async def patch_prefs(request: Request, authorization: str | None = Header(defau # ── Favorites / Tags / Recents ──────────────────────────────────────────── @router.get("/favorites") -async def list_favorites_v2(request: Request, authorization: str | None = Header(default=None)): +def list_favorites_v2(request: Request, authorization: str | None = Header(default=None)): user = get_bearer_user(request, authorization) _v2_rate_check(request, user) with get_conn() as conn: @@ -1455,7 +1455,7 @@ async def remove_favorite_v2(page_id: int, request: Request, authorization: str return {"page_id": page_id, "status": "removed"} @router.get("/tags") -async def list_tags_v2(request: Request, authorization: str | None = Header(default=None)): +def list_tags_v2(request: Request, authorization: str | None = Header(default=None)): user = get_bearer_user(request, authorization) _v2_rate_check(request, user) q = (request.query_params.get("q") or "").strip() @@ -1506,7 +1506,7 @@ async def patch_tag_v2(tag_id: int, request: Request, authorization: str | None return {"id": tag_id, "status": "updated"} @router.delete("/tags/{tag_id}") -async def delete_tag_v2(tag_id: int, request: Request, authorization: str | None = Header(default=None)): +def delete_tag_v2(tag_id: int, request: Request, authorization: str | None = Header(default=None)): user = require_scope("write")(request, authorization) _v2_rate_check(request, user) with get_conn() as conn: @@ -1534,7 +1534,7 @@ async def attach_tag_v2(page_id: int, request: Request, authorization: str | Non return {"page_id": page_id, "tag_id": tag_id, "status": "attached"} @router.delete("/pages/{page_id}/tags/{tag_id}") -async def detach_tag_v2(page_id: int, tag_id: int, request: Request, authorization: str | None = Header(default=None)): +def detach_tag_v2(page_id: int, tag_id: int, request: Request, authorization: str | None = Header(default=None)): user = require_scope("write")(request, authorization) _v2_rate_check(request, user) with get_conn() as conn: @@ -1543,7 +1543,7 @@ async def detach_tag_v2(page_id: int, tag_id: int, request: Request, authorizati return {"status": "detached"} @router.get("/recents") -async def list_recents_v2(request: Request, authorization: str | None = Header(default=None)): +def list_recents_v2(request: Request, authorization: str | None = Header(default=None)): user = get_bearer_user(request, authorization) _v2_rate_check(request, user) limit = int(request.query_params.get("limit", "20")) @@ -1558,7 +1558,7 @@ async def list_recents_v2(request: Request, authorization: str | None = Header(d # ── Sharing / publish ───────────────────────────────────────────────────── @router.get("/pages/{page_id}/shares") -async def list_shares_v2(page_id: int, request: Request, authorization: str | None = Header(default=None)): +def list_shares_v2(page_id: int, request: Request, authorization: str | None = Header(default=None)): user = get_bearer_user(request, authorization) _v2_rate_check(request, user) with get_conn() as conn: @@ -1611,7 +1611,7 @@ async def patch_share_v2(share_id: int, request: Request, authorization: str | N return {"id": share_id, "status": "updated"} @router.delete("/shares/{share_id}") -async def delete_share_v2(share_id: int, request: Request, authorization: str | None = Header(default=None)): +def delete_share_v2(share_id: int, request: Request, authorization: str | None = Header(default=None)): user = require_scope("write")(request, authorization) _v2_rate_check(request, user) with get_conn() as conn: @@ -1651,7 +1651,7 @@ async def unpublish_page_v2(page_id: int, request: Request, authorization: str | # ── History ─────────────────────────────────────────────────────────────── @router.get("/pages/{page_id}/history") -async def list_history_v2(page_id: int, request: Request, authorization: str | None = Header(default=None)): +def list_history_v2(page_id: int, request: Request, authorization: str | None = Header(default=None)): user = get_bearer_user(request, authorization) _v2_rate_check(request, user) with get_conn() as conn: @@ -1693,7 +1693,7 @@ async def restore_history_v2(page_id: int, request: Request, authorization: str # ── Sprints ─────────────────────────────────────────────────────────────── @router.get("/collections/{collection_id}/sprints") -async def list_sprints_v2(collection_id: int, request: Request, authorization: str | None = Header(default=None)): +def list_sprints_v2(collection_id: int, request: Request, authorization: str | None = Header(default=None)): user = get_bearer_user(request, authorization) _v2_rate_check(request, user) limit, offset = parse_pagination(request) @@ -1751,7 +1751,7 @@ async def patch_sprint_v2(sprint_id: int, request: Request, authorization: str | return {"id": sprint_id, "status": "updated"} @router.delete("/sprints/{sprint_id}") -async def delete_sprint_v2(sprint_id: int, request: Request, authorization: str | None = Header(default=None)): +def delete_sprint_v2(sprint_id: int, request: Request, authorization: str | None = Header(default=None)): user = require_scope("write")(request, authorization) _v2_rate_check(request, user) with get_conn() as conn: @@ -1779,7 +1779,7 @@ async def assign_sprint_v2(sprint_id: int, request: Request, authorization: str return {"sprint_id": sprint_id, "page_id": pid, "status": "assigned"} @router.delete("/sprints/{sprint_id}/assign/{page_id}") -async def unassign_sprint_v2(sprint_id: int, page_id: int, request: Request, authorization: str | None = Header(default=None)): +def unassign_sprint_v2(sprint_id: int, page_id: int, request: Request, authorization: str | None = Header(default=None)): user = require_scope("write")(request, authorization) _v2_rate_check(request, user) with get_conn() as conn: @@ -1788,7 +1788,7 @@ async def unassign_sprint_v2(sprint_id: int, page_id: int, request: Request, aut return {"status": "removed"} @router.get("/sprints/{sprint_id}/burndown") -async def burndown_v2(sprint_id: int, request: Request, authorization: str | None = Header(default=None)): +def burndown_v2(sprint_id: int, request: Request, authorization: str | None = Header(default=None)): user = get_bearer_user(request, authorization) _v2_rate_check(request, user) with get_conn() as conn: @@ -1816,7 +1816,7 @@ async def burndown_v2(sprint_id: int, request: Request, authorization: str | Non # ── Templates ───────────────────────────────────────────────────────────── @router.get("/collections/{collection_id}/templates") -async def list_templates_v2(collection_id: int, request: Request, authorization: str | None = Header(default=None)): +def list_templates_v2(collection_id: int, request: Request, authorization: str | None = Header(default=None)): user = get_bearer_user(request, authorization) _v2_rate_check(request, user) with get_conn() as conn: @@ -1860,7 +1860,7 @@ async def patch_template_v2(template_id: int, request: Request, authorization: s return {"id": template_id, "status": "updated"} @router.delete("/templates/{template_id}") -async def delete_template_v2(template_id: int, request: Request, authorization: str | None = Header(default=None)): +def delete_template_v2(template_id: int, request: Request, authorization: str | None = Header(default=None)): user = require_scope("write")(request, authorization) _v2_rate_check(request, user) with get_conn() as conn: @@ -1869,7 +1869,7 @@ async def delete_template_v2(template_id: int, request: Request, authorization: return {"id": template_id, "status": "deleted"} @router.post("/templates/{template_id}/apply") -async def apply_template_v2(template_id: int, request: Request, authorization: str | None = Header(default=None)): +def apply_template_v2(template_id: int, request: Request, authorization: str | None = Header(default=None)): user = require_scope("write")(request, authorization) _v2_rate_check(request, user) with get_conn() as conn: @@ -1884,7 +1884,7 @@ async def apply_template_v2(template_id: int, request: Request, authorization: s return {"template_id": template_id, "page_id": pid, "status": "applied"} @router.get("/templates/database") -async def list_db_templates_v2(request: Request, authorization: str | None = Header(default=None)): +def list_db_templates_v2(request: Request, authorization: str | None = Header(default=None)): user = get_bearer_user(request, authorization) _v2_rate_check(request, user) with get_conn() as conn: @@ -1917,7 +1917,7 @@ async def apply_db_template_v2(template_id: int, request: Request, authorization # ── Export / Import ─────────────────────────────────────────────────────── @router.get("/pages/{page_id}/export") -async def export_page_v2(page_id: int, request: Request, format: str = "markdown", authorization: str | None = Header(default=None)): +def export_page_v2(page_id: int, request: Request, format: str = "markdown", authorization: str | None = Header(default=None)): user = get_bearer_user(request, authorization) _v2_rate_check(request, user) fmt = (format or request.query_params.get("format") or "markdown").lower() @@ -1941,7 +1941,7 @@ async def export_page_v2(page_id: int, request: Request, format: str = "markdown raise HTTPException(500, f"Export failed: {e}") from None @router.get("/collections/{collection_id}/export/csv") -async def export_csv_v2(collection_id: int, request: Request, authorization: str | None = Header(default=None)): +def export_csv_v2(collection_id: int, request: Request, authorization: str | None = Header(default=None)): user = get_bearer_user(request, authorization) _v2_rate_check(request, user) import csv @@ -2003,7 +2003,7 @@ async def import_csv_v2(collection_id: int, request: Request, authorization: str # ── Forges / Projects ───────────────────────────────────────────────────── @router.get("/projects") -async def list_projects_v2(request: Request, authorization: str | None = Header(default=None)): +def list_projects_v2(request: Request, authorization: str | None = Header(default=None)): user = get_bearer_user(request, authorization) _v2_rate_check(request, user) with get_conn() as conn: @@ -2029,7 +2029,7 @@ async def project_tree_v2(owner: str, repo: str, request: Request, path: str = " # ── Search (FTS5 + LIKE fallback) ───────────────────────────────────────── @router.get("/search") -async def search_v2(request: Request, query: str = "", workspace_id: int | None = None, type: str = "all", authorization: str | None = Header(default=None)): +def search_v2(request: Request, query: str = "", workspace_id: int | None = None, type: str = "all", authorization: str | None = Header(default=None)): user = get_bearer_user(request, authorization) _v2_rate_check(request, user) q = (query or request.query_params.get("query") or "").strip() @@ -2053,7 +2053,7 @@ async def search_v2(request: Request, query: str = "", workspace_id: int | None # ── Admin ───────────────────────────────────────────────────────────────── @router.get("/admin/users") -async def admin_list_users_v2(request: Request, limit: int = 30, offset: int = 0, authorization: str | None = Header(default=None)): +def admin_list_users_v2(request: Request, limit: int = 30, offset: int = 0, authorization: str | None = Header(default=None)): user = get_bearer_user(request, authorization) _v2_rate_check(request, user) if not user.get("is_admin") and not has_scope(user.get("_token_scopes"), "admin"): @@ -2092,7 +2092,7 @@ async def admin_patch_user_v2(uid: int, request: Request, authorization: str | N return {"id": uid, "status": "updated"} @router.delete("/admin/users/{uid}") -async def admin_delete_user_v2(uid: int, request: Request, authorization: str | None = Header(default=None)): +def admin_delete_user_v2(uid: int, request: Request, authorization: str | None = Header(default=None)): user = get_bearer_user(request, authorization) _v2_rate_check(request, user) if not user.get("is_admin") and not has_scope(user.get("_token_scopes"), "admin"): @@ -2106,7 +2106,7 @@ async def admin_delete_user_v2(uid: int, request: Request, authorization: str | return {"id": uid, "status": "deleted"} @router.get("/admin/audit-logs") -async def admin_audit_logs_v2(request: Request, limit: int = 50, authorization: str | None = Header(default=None)): +def admin_audit_logs_v2(request: Request, limit: int = 50, authorization: str | None = Header(default=None)): user = get_bearer_user(request, authorization) _v2_rate_check(request, user) if not user.get("is_admin") and not has_scope(user.get("_token_scopes"), "admin"): @@ -2119,7 +2119,7 @@ async def admin_audit_logs_v2(request: Request, limit: int = 50, authorization: # ── Webhooks (v6.4.0 prod) ──────────────────────────────────────────────── @router.get("/webhooks/events") -async def list_webhook_events_v2(request: Request, authorization: str | None = Header(default=None)): +def list_webhook_events_v2(request: Request, authorization: str | None = Header(default=None)): """Catalogue of deliverable events (+ wildcard syntax).""" user = get_bearer_user(request, authorization) _v2_rate_check(request, user) @@ -2128,7 +2128,7 @@ async def list_webhook_events_v2(request: Request, authorization: str | None = H @router.get("/webhooks") -async def list_webhooks_v2(request: Request, authorization: str | None = Header(default=None)): +def list_webhooks_v2(request: Request, authorization: str | None = Header(default=None)): user = get_bearer_user(request, authorization) _v2_rate_check(request, user) limit, offset = parse_pagination(request) @@ -2193,7 +2193,7 @@ async def patch_webhook_v2(webhook_id: int, request: Request, authorization: str return {"id": webhook_id, "status": "updated"} @router.delete("/webhooks/{webhook_id}") -async def delete_webhook_v2(webhook_id: int, request: Request, authorization: str | None = Header(default=None)): +def delete_webhook_v2(webhook_id: int, request: Request, authorization: str | None = Header(default=None)): user = require_scope("write")(request, authorization) _v2_rate_check(request, user) with get_conn() as conn: @@ -2220,7 +2220,7 @@ async def test_webhook_v2(webhook_id: int, request: Request, authorization: str return {"webhook_id": webhook_id, "status": "tested", "delivered": ok} @router.get("/webhooks/{webhook_id}/deliveries") -async def list_deliveries_v2(webhook_id: int, request: Request, +def list_deliveries_v2(webhook_id: int, request: Request, status: str | None = None, authorization: str | None = Header(default=None)): user = get_bearer_user(request, authorization) diff --git a/app/routers/api_v2_agent.py b/app/routers/api_v2_agent.py index 2180aa9..ca56f17 100644 --- a/app/routers/api_v2_agent.py +++ b/app/routers/api_v2_agent.py @@ -103,7 +103,7 @@ def _engine_for(user_id: int, workspace_id: int | None, provider: str | None) -> # ── Agents ───────────────────────────────────────────────────────────────── @router.get("/agents") -async def list_agents_v2(request: Request, authorization: str | None = Header(default=None)): +def list_agents_v2(request: Request, authorization: str | None = Header(default=None)): user = _guard(request, authorization) limit, offset = parse_pagination(request) ws = _workspace_of(request) @@ -157,7 +157,7 @@ async def create_agent_v2(request: Request, authorization: str | None = Header(d @router.get("/agents/{agent_id}") -async def get_agent_v2(agent_id: int, request: Request, authorization: str | None = Header(default=None)): +def get_agent_v2(agent_id: int, request: Request, authorization: str | None = Header(default=None)): _guard(request, authorization) with get_conn() as conn: row = conn.execute("SELECT * FROM agents WHERE id=?", (agent_id,)).fetchone() @@ -195,7 +195,7 @@ async def update_agent_v2(agent_id: int, request: Request, authorization: str | @router.delete("/agents/{agent_id}") -async def delete_agent_v2(agent_id: int, request: Request, authorization: str | None = Header(default=None)): +def delete_agent_v2(agent_id: int, request: Request, authorization: str | None = Header(default=None)): user = _guard(request, authorization, write=True) with get_conn() as conn: if not conn.execute("SELECT id FROM agents WHERE id=?", (agent_id,)).fetchone(): @@ -209,7 +209,7 @@ async def delete_agent_v2(agent_id: int, request: Request, authorization: str | # ── Conversations (static paths declared before /agents/{agent_id}) ──────── @router.get("/agents/conversations") -async def list_conversations_v2(request: Request, authorization: str | None = Header(default=None)): +def list_conversations_v2(request: Request, authorization: str | None = Header(default=None)): user = _guard(request, authorization) limit, offset = parse_pagination(request) with get_conn() as conn: @@ -264,7 +264,7 @@ async def create_conversation_v2(request: Request, authorization: str | None = H @router.get("/agents/conversations/{conversation_id}") -async def get_conversation_v2(conversation_id: int, request: Request, +def get_conversation_v2(conversation_id: int, request: Request, authorization: str | None = Header(default=None)): user = _guard(request, authorization) with get_conn() as conn: @@ -279,7 +279,7 @@ async def get_conversation_v2(conversation_id: int, request: Request, @router.delete("/agents/conversations/{conversation_id}") -async def delete_conversation_v2(conversation_id: int, request: Request, +def delete_conversation_v2(conversation_id: int, request: Request, authorization: str | None = Header(default=None)): user = _guard(request, authorization, write=True) with get_conn() as conn: @@ -292,7 +292,7 @@ async def delete_conversation_v2(conversation_id: int, request: Request, @router.get("/agents/conversations/{conversation_id}/actions") -async def list_actions_v2(conversation_id: int, request: Request, +def list_actions_v2(conversation_id: int, request: Request, authorization: str | None = Header(default=None)): user = _guard(request, authorization) with get_conn() as conn: @@ -306,7 +306,7 @@ async def list_actions_v2(conversation_id: int, request: Request, @router.post("/agents/actions/{action_id}/undo") -async def undo_action_v2(action_id: int, request: Request, +def undo_action_v2(action_id: int, request: Request, authorization: str | None = Header(default=None)): user = _guard(request, authorization, write=True) with get_conn() as conn: @@ -480,7 +480,7 @@ async def trigger_agent_v2(agent_id: int, request: Request, # ── Skill marketplace ────────────────────────────────────────────────────── @router.get("/skills") -async def list_skills_v2(request: Request, authorization: str | None = Header(default=None)): +def list_skills_v2(request: Request, authorization: str | None = Header(default=None)): _guard(request, authorization) limit, offset = parse_pagination(request) ws = _workspace_of(request) @@ -535,7 +535,7 @@ async def create_skill_v2(request: Request, authorization: str | None = Header(d # Gallery & import are static segments: declared before /skills/{skill_id} so # FastAPI never tries to coerce "gallery" into an int path parameter. @router.get("/skills/gallery") -async def skills_gallery_v2(request: Request, authorization: str | None = Header(default=None)): +def skills_gallery_v2(request: Request, authorization: str | None = Header(default=None)): _guard(request, authorization) presets = skill_gallery.list_gallery() return {"gallery": presets, "total": len(presets), @@ -596,7 +596,7 @@ async def import_skill_v2(request: Request, authorization: str | None = Header(d @router.get("/skills/{skill_id}") -async def get_skill_v2(skill_id: int, request: Request, authorization: str | None = Header(default=None)): +def get_skill_v2(skill_id: int, request: Request, authorization: str | None = Header(default=None)): _guard(request, authorization) with get_conn() as conn: row = conn.execute("SELECT * FROM agent_skills WHERE id=?", (skill_id,)).fetchone() @@ -606,7 +606,7 @@ async def get_skill_v2(skill_id: int, request: Request, authorization: str | Non @router.get("/skills/{skill_id}/export") -async def export_skill_v2(skill_id: int, request: Request, +def export_skill_v2(skill_id: int, request: Request, authorization: str | None = Header(default=None)): """Portable JSON document — POST it to /api/v2/skills/import elsewhere.""" _guard(request, authorization) @@ -618,7 +618,7 @@ async def export_skill_v2(skill_id: int, request: Request, @router.delete("/skills/{skill_id}") -async def delete_skill_v2(skill_id: int, request: Request, +def delete_skill_v2(skill_id: int, request: Request, authorization: str | None = Header(default=None)): user = _guard(request, authorization, write=True) with get_conn() as conn: diff --git a/app/routers/audit.py b/app/routers/audit.py index 61b7832..c87d48b 100644 --- a/app/routers/audit.py +++ b/app/routers/audit.py @@ -99,7 +99,7 @@ def _query(source: str, actor: str, action: str, limit: int, offset: int): @router.get("/api/v2/audit/logs") -async def audit_logs(request: Request): +def audit_logs(request: Request): _admin_user(request) qp = request.query_params source = (qp.get("source") or "all").lower() diff --git a/app/routers/auth.py b/app/routers/auth.py index 56180c3..7571b60 100644 --- a/app/routers/auth.py +++ b/app/routers/auth.py @@ -156,7 +156,7 @@ async function handleLogin(e){e.preventDefault();const email=document.getElement @router.get("/register") -async def register_page(request: Request): +def register_page(request: Request): """Show the registration page (local login page with register tab active).""" return HTMLResponse(_with_nonce(LOCAL_LOGIN_HTML.replace( 'class="tab active" onclick="switchTab(\'login\')"', @@ -177,7 +177,7 @@ async def register_page(request: Request): @router.get("/login") -async def login(request: Request, provider: str = Query("gitea")): +def login(request: Request, provider: str = Query("gitea")): """Redirect to OAuth2 authorize page or show local login page.""" # Local login page (POST handled by /auth/local-login) from fastapi.responses import HTMLResponse @@ -460,7 +460,7 @@ async def callback( @router.get("/logout") -async def logout(request: Request): +def logout(request: Request): """Clear session and redirect to login page. SAML sessions additionally hand over to the IdP's Single Logout when one @@ -529,7 +529,7 @@ def _session_user_or_401(request: Request) -> dict: @router.get("/2fa/status") -async def twofa_status(request: Request): +def twofa_status(request: Request): from app.services import two_factor as _2fa user = _session_user_or_401(request) return {"enabled": _2fa.is_enabled(user["id"]), @@ -537,7 +537,7 @@ async def twofa_status(request: Request): @router.post("/2fa/setup") -async def twofa_setup(request: Request): +def twofa_setup(request: Request): from app.services import two_factor as _2fa user = _session_user_or_401(request) return _2fa.setup_secret(user["id"]) @@ -563,7 +563,7 @@ async def twofa_activate(request: Request): @router.post("/2fa/disable") -async def twofa_disable(request: Request): +def twofa_disable(request: Request): from app.services import two_factor as _2fa user = _session_user_or_401(request) _2fa.disable(user["id"]) diff --git a/app/routers/automations.py b/app/routers/automations.py index b170ac4..53dc385 100644 --- a/app/routers/automations.py +++ b/app/routers/automations.py @@ -71,7 +71,7 @@ def _validate_payload(body: dict) -> None: @router.get("/workspace/automations") -async def list_automations(request: Request): +def list_automations(request: Request): with get_conn() as conn: rows = conn.execute("SELECT * FROM automations ORDER BY created_at DESC").fetchall() items = [dict(r) for r in rows] @@ -109,7 +109,7 @@ async def create_automation(request: Request): @router.get("/workspace/automations/{auto_id}") -async def get_automation(request: Request, auto_id: int): +def get_automation(request: Request, auto_id: int): with get_conn() as conn: row = conn.execute("SELECT * FROM automations WHERE id=?", (auto_id,)).fetchone() if not row: @@ -147,7 +147,7 @@ async def update_automation(request: Request, auto_id: int): @router.delete("/workspace/automations/{auto_id}") -async def delete_automation(request: Request, auto_id: int): +def delete_automation(request: Request, auto_id: int): with get_conn() as conn: conn.execute("DELETE FROM automations WHERE id=?", (auto_id,)) conn.commit() @@ -178,7 +178,7 @@ async def run_automation_button(request: Request, auto_id: int): @router.get("/workspace/automations/{auto_id}/runs") -async def automation_runs_history(request: Request, auto_id: int, limit: int = 50): +def automation_runs_history(request: Request, auto_id: int, limit: int = 50): with get_conn() as conn: rows = conn.execute( """SELECT * FROM automation_runs WHERE automation_id=? @@ -226,7 +226,7 @@ def _encrypt_step_config(config: dict) -> dict: @router.get("/workspace/automations/{auto_id}/steps") -async def list_steps(request: Request, auto_id: int): +def list_steps(request: Request, auto_id: int): if _get_auto(auto_id) is None: return _auto_404() return {"automation_id": auto_id, "steps": get_steps(auto_id)} @@ -279,7 +279,7 @@ async def update_step(request: Request, step_id: int): @router.delete("/workspace/automations/steps/{step_id}") -async def delete_step(request: Request, step_id: int): +def delete_step(request: Request, step_id: int): _require_session(request) with get_conn() as conn: conn.execute("DELETE FROM automation_steps WHERE id=?", (step_id,)) diff --git a/app/routers/board.py b/app/routers/board.py index 93db0f1..76aa60b 100644 --- a/app/routers/board.py +++ b/app/routers/board.py @@ -66,7 +66,7 @@ def _ensure_block_ids(blocks) -> None: @router.get("/api/wiki/pages") -async def wiki_page_search(request: Request, q: str = Query(default="")): +def wiki_page_search(request: Request, q: str = Query(default="")): """v5.11.0: page picker for [[ wiki-links. Fuzzy-ish title search across every non-deleted page the current user can see (single source: pages).""" q = (q or "").strip().lower() @@ -98,7 +98,7 @@ async def wiki_page_search(request: Request, q: str = Query(default="")): @router.get("/api/wiki/titles") -async def wiki_titles(request: Request, ids: str = Query(default="")): +def wiki_titles(request: Request, ids: str = Query(default="")): """v5.11.0: resolve page-id lists to current labels (rename propagation).""" parsed: list[int] = [] for part in (ids or "").split(","): @@ -179,7 +179,7 @@ async def set_page_options(request: Request, page_id: int): @router.get("/api/page-templates") -async def list_page_templates_api(request: Request): +def list_page_templates_api(request: Request): """v5.12.0: built-in + user global page templates for the picker.""" user = SessionManager.decode_session(request.cookies.get("flowdeck_session", "")) uid = (user or {}).get("id") @@ -899,7 +899,7 @@ def _apply_sorts(cards: list[dict], sorts: str) -> list[dict]: # ═══════════ Library page ═══════════ @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 avec onglets Recents, Favorites, Shared, Private — scopé au workspace.""" from app.templating import ENV env = ENV @@ -943,7 +943,7 @@ async def library_page(request: Request, owner: str = Query(default=""), repo: s # ═══════════ Favorites API ═══════════ @router.get("/api/favorites") -async def list_favorites(request: Request): +def list_favorites(request: Request): """List favorited page IDs for the current user.""" user = SessionManager.decode_session(request.cookies.get("flowdeck_session", "")) uid = user["id"] if user and user.get("id") else 1 @@ -1033,7 +1033,7 @@ async def unpublish_page(request: Request, page_id: int): # ═══════════ Trash (must be before /{owner}/{repo} catch-all) ═══════════ @router.get("/api/trash") -async def list_trash(request: Request): +def list_trash(request: Request): with get_conn() as conn: rows = conn.execute("SELECT id, title, workspace, parent_id, deleted_at FROM pages WHERE deleted_at IS NOT NULL ORDER BY deleted_at DESC").fetchall() return [{"id": r["id"], "name": r["title"] or "Untitled", "icon": "📄", "path": r["workspace"] or "Private", "deleted_at": r["deleted_at"]} for r in rows] @@ -1052,7 +1052,7 @@ async def restore_page(request: Request, page_id: int): @router.delete("/api/trash/{page_id}") -async def permanent_delete(request: Request, page_id: int): +def permanent_delete(request: Request, page_id: int): with get_conn() as conn: conn.execute("UPDATE pages SET parent_id=NULL WHERE parent_id=?", (page_id,)) conn.execute("DELETE FROM pages WHERE id=? AND deleted_at IS NOT NULL", (page_id,)) @@ -1061,7 +1061,7 @@ async def permanent_delete(request: Request, page_id: int): @router.get("/trash", response_class=HTMLResponse) -async def trash_page(request: Request): +def trash_page(request: Request): from app.templating import ENV env = ENV template = env.get_template("trash.html") @@ -1072,7 +1072,7 @@ async def trash_page(request: Request): # These routes MUST be registered before the catch-all /{owner}/{repo} below. @router.get("/api/synced-blocks") -async def list_synced_blocks_api(request: Request, workspace: str = Query(default="")): +def list_synced_blocks_api(request: Request, workspace: str = Query(default="")): """List synced blocks for a workspace.""" user = SessionManager.decode_session(request.cookies.get("flowdeck_session", "")) from app.services.synced_blocks import list_synced_blocks @@ -1147,7 +1147,7 @@ async def delete_synced_block_api(request: Request, sid: int): @router.get("/api/synced-blocks/{sid}") -async def get_synced_block_api(sid: int): +def get_synced_block_api(sid: int): """Get a synced block by id.""" from app.services.synced_blocks import get_synced_block sb = get_synced_block(sid) @@ -1173,7 +1173,7 @@ async def add_synced_to_page(request: Request, page_id: int): @router.delete("/api/pages/{page_id}/synced/{sid}") -async def remove_synced_from_page(request: Request, page_id: int, sid: int): +def remove_synced_from_page(request: Request, page_id: int, sid: int): """Remove a synced block reference from a page (unsync).""" from app.services.synced_blocks import remove_page_synced remove_page_synced(page_id, sid) @@ -1181,7 +1181,7 @@ async def remove_synced_from_page(request: Request, page_id: int, sid: int): @router.get("/api/pages/{page_id}/synced") -async def get_page_synced_refs(request: Request, page_id: int): +def get_page_synced_refs(request: Request, page_id: int): """Get all synced block references for a page.""" from app.services.synced_blocks import get_page_synced return {"synced_blocks": get_page_synced(page_id)} @@ -1190,7 +1190,7 @@ async def get_page_synced_refs(request: Request, page_id: int): # ═══════════ Board page ═══════════ @router.get("/{owner}/{repo}", response_class=HTMLResponse) -async def board(request: Request, owner: str, repo: str, view: str = Query(default="")): +def board(request: Request, owner: str, repo: str, view: str = Query(default="")): from app.templating import ENV env = ENV sidebar = _sidebar_data(request, owner, repo) @@ -1279,12 +1279,12 @@ async def board_view( # ═══════════ v0.9.0: Custom Properties API ═══════════ @router.get("/api/properties/{owner}/{repo}") -async def get_properties(owner: str, repo: str): +def get_properties(owner: str, repo: str): return {"properties": _get_project_properties(owner, repo)} @router.post("/api/properties/{owner}/{repo}") -async def create_property(owner: str, repo: str, name: str = Query(...), +def create_property(owner: str, repo: str, name: str = Query(...), prop_type: str = Query(default="select"), options: str = Query(default="")): opts = json.dumps([o.strip() for o in options.split(",") if o.strip()]) @@ -1301,7 +1301,7 @@ async def create_property(owner: str, repo: str, name: str = Query(...), @router.delete("/api/properties/{owner}/{repo}") -async def delete_property(owner: str, repo: str, name: str = Query(...)): +def delete_property(owner: str, repo: str, name: str = Query(...)): with get_conn() as conn: conn.execute( "DELETE FROM project_properties WHERE project_owner=? AND project_name=? AND name=?", @@ -1312,7 +1312,7 @@ async def delete_property(owner: str, repo: str, name: str = Query(...)): @router.post("/api/properties/{owner}/{repo}/values") -async def set_property_value(owner: str, repo: str, issue_id: int = Query(...), +def set_property_value(owner: str, repo: str, issue_id: int = Query(...), name: str = Query(...), value: str = Query(default="")): with get_conn() as conn: prop = conn.execute( @@ -1332,7 +1332,7 @@ async def set_property_value(owner: str, repo: str, issue_id: int = Query(...), # ═══════════ v0.9.0: AI Keywords API ═══════════ @router.get("/api/ai-keywords/{owner}/{repo}") -async def get_ai_keywords(owner: str, repo: str): +def get_ai_keywords(owner: str, repo: str): with get_conn() as conn: rows = conn.execute( "SELECT keyword, color, usage_count FROM ai_keywords WHERE project_owner=? AND project_name=? ORDER BY usage_count DESC LIMIT 30", @@ -1395,7 +1395,7 @@ async def create_page(request: Request, title: str = Query(default=""), @router.get("/api/pages/{page_id}") -async def get_page(request: Request, page_id: int): +def get_page(request: Request, page_id: int): """Get a Markdown page.""" user = SessionManager.decode_session(request.cookies.get("flowdeck_session", "")) if not user or not user.get("id"): @@ -1543,7 +1543,7 @@ def _block_texts(b: dict) -> list[str]: @router.get("/api/pages/{page_id}/backlinks") -async def page_backlinks(request: Request, page_id: int): +def page_backlinks(request: Request, page_id: int): """v5.4.0: pages that link to this one ("Lié depuis…"). Scans every non-deleted page's blocks (and raw markdown) for an internal @@ -1591,7 +1591,7 @@ async def page_backlinks(request: Request, page_id: int): @router.get("/api/pages/{page_id}/versions") -async def page_versions(request: Request, page_id: int): +def page_versions(request: Request, page_id: int): """v5.4.0: version history for a block-editor page.""" with get_conn() as conn: rows = conn.execute( @@ -1750,7 +1750,7 @@ async def set_page_cover(request: Request, page_id: int): @router.delete("/api/pages/{page_id}/cover") -async def remove_page_cover(request: Request, page_id: int): +def remove_page_cover(request: Request, page_id: int): with get_conn() as conn: conn.execute("UPDATE pages SET cover_url='' WHERE id=?", (page_id,)) conn.commit() @@ -2024,7 +2024,7 @@ async def delete_page(request: Request, page_id: int): @router.get("/pages/{page_id}", response_class=HTMLResponse) -async def view_page(request: Request, page_id: int): +def view_page(request: Request, page_id: int): """Render a page as HTML, or a file viewer for uploaded files. ?embed=1 — minimal mode for side peek (editor only, no header).""" embed = request.query_params.get("embed") == "1" diff --git a/app/routers/collaboration.py b/app/routers/collaboration.py index 057fc11..9478da4 100644 --- a/app/routers/collaboration.py +++ b/app/routers/collaboration.py @@ -49,7 +49,7 @@ def _serialize(rows): @router.get("/pages/{page_id}/comments") -async def list_comments(request: Request, page_id: int): +def list_comments(request: Request, page_id: int): """List page-level and inline comments for a FlowDeck page.""" _current_user(request) with get_conn() as conn: @@ -195,7 +195,7 @@ async def update_comment(request: Request, comment_id: int): @router.delete("/comments/{comment_id}") -async def delete_comment(request: Request, comment_id: int): +def delete_comment(request: Request, comment_id: int): """Delete a comment and its replies.""" user = _current_user(request) with get_conn() as conn: diff --git a/app/routers/collections.py b/app/routers/collections.py index 1d57b5b..f57e3f9 100644 --- a/app/routers/collections.py +++ b/app/routers/collections.py @@ -196,7 +196,7 @@ def _validate_meta_keys(conn, collection_id: int, properties: dict) -> None: @router.get("", response_class=HTMLResponse) -async def list_collections(request: Request): +def list_collections(request: Request): """Page listing all collections in the workspace.""" with get_conn() as conn: rows = conn.execute( @@ -212,7 +212,7 @@ async def list_collections(request: Request): @router.get("/api") -async def list_collections_api(request: Request): +def list_collections_api(request: Request): """API: list all collections.""" with get_conn() as conn: rows = conn.execute( @@ -502,7 +502,7 @@ async def duplicate_collection_api(request: Request, collection_id: int): @router.get("/pages/{page_id}/api") -async def get_page_api(request: Request, page_id: int): +def get_page_api(request: Request, page_id: int): """API: get a single page.""" with get_conn() as conn: page = conn.execute( @@ -516,7 +516,7 @@ async def get_page_api(request: Request, page_id: int): @router.get("/pages/{page_id}/open/api") -async def open_row_page_api(request: Request, page_id: int): +def open_row_page_api(request: Request, page_id: int): """v6.5.0 — content page of a database row (lazy-created). Any DB view (table/board/gallery/list/calendar) opens a row through @@ -650,7 +650,7 @@ async def delete_page_api(request: Request, page_id: int): @router.get("/boards/api") -async def list_boards_as_collections(request: Request): +def list_boards_as_collections(request: Request): """API: list all Gitea boards as pseudo-collections.""" from app.services.collection_adapter import GiteaBoardCompat boards = GiteaBoardCompat.list_boards_as_collections() @@ -690,14 +690,14 @@ async def sync_board_to_collection(request: Request, owner: str, repo: str): @router.get("/property-types/api") -async def list_property_types_api(request: Request): +def list_property_types_api(request: Request): """API: list all available property types.""" from app.services.property_types import PROPERTY_TYPES return {"types": PROPERTY_TYPES} @router.get("/{collection_id}/properties/api") -async def list_properties_api(request: Request, collection_id: int): +def list_properties_api(request: Request, collection_id: int): """API: list all properties visible to the current user.""" user = _session_user(request) _require_view(collection_id, user) @@ -720,7 +720,7 @@ async def list_properties_api(request: Request, collection_id: int): @router.get("/{collection_id}/members/api") -async def list_collection_members_api(request: Request, collection_id: int): +def list_collection_members_api(request: Request, collection_id: int): """API: list workspace members available for a ``person`` property. Resolves the collection's workspace and returns its members (falling back to @@ -753,7 +753,7 @@ async def list_collection_members_api(request: Request, collection_id: int): @router.get("/{collection_id}/calendar/api") -async def collection_calendar_api(request: Request, collection_id: int, +def collection_calendar_api(request: Request, collection_id: int, start: str = "", end: str = "", date_property: str = ""): """API (v5.8.0): expanded calendar events for a window [start, end]. @@ -838,7 +838,7 @@ async def collection_calendar_api(request: Request, collection_id: int, @router.get("/timezones/api") -async def timezones_api(request: Request): +def timezones_api(request: Request): """API (v5.8.0): the user's timezone plus a picker-friendly zone list.""" user = _current_user(request) with get_conn() as conn: @@ -973,7 +973,7 @@ async def update_property_api(request: Request, prop_id: int): @router.delete("/properties/{prop_id}/api") -async def delete_property_api(request: Request, prop_id: int): +def delete_property_api(request: Request, prop_id: int): """API: delete a property.""" with get_conn() as conn: existing = conn.execute( @@ -1167,7 +1167,7 @@ async def evaluate_formula(request: Request): @router.get("/views/{view_id}/api") -async def get_view_api(request: Request, view_id: int): +def get_view_api(request: Request, view_id: int): """API: get a single view config.""" with get_conn() as conn: row = conn.execute("SELECT * FROM collection_views WHERE id=?", (view_id,)).fetchone() @@ -1252,7 +1252,7 @@ async def save_view_as(request: Request, collection_id: int): @router.get("/{collection_id}/views/api") -async def list_views_api(request: Request, collection_id: int): +def list_views_api(request: Request, collection_id: int): """API: list views for a collection visible to the current user. Shared/legacy views (``created_by IS NULL``) are visible to everyone; @@ -1277,7 +1277,7 @@ async def list_views_api(request: Request, collection_id: int): @router.delete("/views/{view_id}/api") -async def delete_view_api(request: Request, view_id: int): +def delete_view_api(request: Request, view_id: int): """API: delete a saved view.""" with get_conn() as conn: existing = conn.execute("SELECT * FROM collection_views WHERE id=?", (view_id,)).fetchone() @@ -1333,7 +1333,7 @@ async def duplicate_view_api(request: Request, view_id: int): @router.get("/{collection_id}/pages/{page_id}/sub-items") -async def list_sub_items(request: Request, collection_id: int, page_id: int): +def list_sub_items(request: Request, collection_id: int, page_id: int): """API: list sub-items of a page.""" with get_conn() as conn: rows = conn.execute( @@ -1396,7 +1396,7 @@ async def create_sub_item(request: Request, collection_id: int, page_id: int): @router.get("/{collection_id}/pages/{page_id}/status-aggregate") -async def aggregate_child_status(request: Request, collection_id: int, page_id: int): +def aggregate_child_status(request: Request, collection_id: int, page_id: int): """API: compute aggregate status from children.""" with get_conn() as conn: children = conn.execute( @@ -1495,7 +1495,7 @@ async def check_dependencies(request: Request, collection_id: int, page_id: int) @router.get("/{collection_id}/sources/api") -async def list_data_sources(request: Request, collection_id: int): +def list_data_sources(request: Request, collection_id: int): """API: list all data sources for a collection.""" with get_conn() as conn: coll = conn.execute("SELECT id FROM collections WHERE id=?", (collection_id,)).fetchone() @@ -1555,7 +1555,7 @@ async def add_data_source(request: Request, collection_id: int): @router.delete("/{collection_id}/sources/{source_id}/api") -async def remove_data_source(request: Request, collection_id: int, source_id: int): +def remove_data_source(request: Request, collection_id: int, source_id: int): """API: remove a data source from a collection.""" with get_conn() as conn: existing = conn.execute( @@ -1668,7 +1668,7 @@ async def create_linked_database(request: Request, collection_id: int): @router.post("/{collection_id}/toggle-inline/api") -async def toggle_inline(request: Request, collection_id: int): +def toggle_inline(request: Request, collection_id: int): """API: toggle a collection between full-page and inline mode.""" with get_conn() as conn: coll = conn.execute( @@ -1759,7 +1759,7 @@ async def create_inline_database(request: Request): @router.put("/{collection_id}/toggle-task/api") -async def toggle_task(request: Request, collection_id: int): +def toggle_task(request: Request, collection_id: int): """API: toggle is_task flag on a collection (Turn into Tasks).""" with get_conn() as conn: coll = conn.execute("SELECT id, is_task FROM collections WHERE id=?", (collection_id,)).fetchone() @@ -1772,7 +1772,7 @@ async def toggle_task(request: Request, collection_id: int): @router.get("/{collection_id}/pages/{page_id}/dependencies/api") -async def list_page_dependencies(request: Request, collection_id: int, page_id: int): +def list_page_dependencies(request: Request, collection_id: int, page_id: int): """API: list dependencies for a page (blocks, blocked_by, related).""" with get_conn() as conn: rows = conn.execute( @@ -1820,7 +1820,7 @@ async def add_page_dependency(request: Request, collection_id: int, page_id: int @router.delete("/{collection_id}/pages/{page_id}/dependencies/{dep_id}/api") -async def remove_page_dependency(request: Request, collection_id: int, page_id: int, dep_id: int): +def remove_page_dependency(request: Request, collection_id: int, page_id: int, dep_id: int): """API: remove a dependency.""" with get_conn() as conn: existing = conn.execute( @@ -1915,7 +1915,7 @@ async def auto_shift_dates(request: Request, collection_id: int, page_id: int): @router.get("/{collection_id}/dashboards/{dashboard_id}", response_class=HTMLResponse) -async def view_dashboard(request: Request, collection_id: int, dashboard_id: int): +def view_dashboard(request: Request, collection_id: int, dashboard_id: int): """v7.3.0: render a collection dashboard grid (multi-DB widgets). Widgets live in ``collection_dashboards.layout_json`` as @@ -1981,7 +1981,7 @@ async def view_dashboard(request: Request, collection_id: int, dashboard_id: int @router.get("/{collection_id}", response_class=HTMLResponse) @router.get("/{collection_id}/view/{view_type}", response_class=HTMLResponse) -async def view_collection(request: Request, collection_id: int, view_type: str = "table"): +def view_collection(request: Request, collection_id: int, view_type: str = "table"): """Main view — renders collection in the requested view type.""" # v6.0.0: granular collection permissions — hide restricted collections. _require_view(collection_id, _session_user(request)) @@ -2618,7 +2618,7 @@ tr:hover td{{background:#222}} @router.get("/{collection_id}/api") -async def get_collection_api(request: Request, collection_id: int): +def get_collection_api(request: Request, collection_id: int): """API: get a single collection with its pages.""" # v6.0.0: granular collection permissions — hide restricted collections. _require_view(collection_id, _session_user(request)) diff --git a/app/routers/dashboard.py b/app/routers/dashboard.py index 3a72451..a59bc77 100644 --- a/app/routers/dashboard.py +++ b/app/routers/dashboard.py @@ -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 (?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/ — 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( diff --git a/app/routers/emoji.py b/app/routers/emoji.py index 9f81d41..8ac03d1 100644 --- a/app/routers/emoji.py +++ b/app/routers/emoji.py @@ -37,7 +37,7 @@ def _active_ws(request: Request) -> int: @router.get("/api/custom-emojis") -async def list_custom_emojis(request: Request): +def list_custom_emojis(request: Request): """List the current workspace's custom emojis.""" ws_id = _active_ws(request) with get_conn() as conn: @@ -79,7 +79,7 @@ async def create_custom_emoji(request: Request): @router.delete("/api/custom-emojis/{emoji_id}") -async def delete_custom_emoji(request: Request, emoji_id: int): +def delete_custom_emoji(request: Request, emoji_id: int): """Delete a custom emoji (and its stored file).""" ws_id = _active_ws(request) with get_conn() as conn: diff --git a/app/routers/export.py b/app/routers/export.py index 0833639..add98bb 100644 --- a/app/routers/export.py +++ b/app/routers/export.py @@ -61,7 +61,7 @@ def _safe_filename(page: dict, ext: str) -> str: @router.get("/markdown/{page_id}") -async def export_markdown(page_id: int, request: Request): +def export_markdown(page_id: int, request: Request): page = _load_page_or_404(request, page_id) md = page_to_markdown(page) filename = _safe_filename(page, "md") @@ -70,7 +70,7 @@ async def export_markdown(page_id: int, request: Request): @router.get("/html/{page_id}") -async def export_html(page_id: int, request: Request): +def export_html(page_id: int, request: Request): page = _load_page_or_404(request, page_id) html = page_to_standalone_html(page) filename = _safe_filename(page, "html") @@ -79,7 +79,7 @@ async def export_html(page_id: int, request: Request): @router.get("/pdf/{page_id}") -async def export_pdf(page_id: int, request: Request): +def export_pdf(page_id: int, request: Request): page = _load_page_or_404(request, page_id) try: pdf_bytes = page_to_pdf_bytes(page) @@ -94,7 +94,7 @@ async def export_pdf(page_id: int, request: Request): @router.get("/site/{page_id}") -async def export_site(page_id: int, request: Request): +def export_site(page_id: int, request: Request): page = _load_page_or_404(request, page_id) site_bytes = build_static_site_bytes(page) title = _safe_filename(page, "site").replace(".site", "") or "flowdeck-site" diff --git a/app/routers/gitea.py b/app/routers/gitea.py index d5f2c7f..e09e831 100644 --- a/app/routers/gitea.py +++ b/app/routers/gitea.py @@ -162,7 +162,7 @@ async def get_labels(request: Request, owner: str, repo: str): # ── Account linking ── @router.get("/status") -async def gitea_status(request: Request): +def gitea_status(request: Request): """Check if the current user has Gitea linked.""" from app.services.gitea_client import get_user_gitea_client client = get_user_gitea_client(request) @@ -170,7 +170,7 @@ async def gitea_status(request: Request): @router.delete("/disconnect") -async def disconnect_gitea(request: Request): +def disconnect_gitea(request: Request): """Remove all Gitea OAuth tokens for the current user.""" from app.auth.session import SessionManager from app.db import get_conn @@ -186,7 +186,7 @@ async def disconnect_gitea(request: Request): # ── Private Pages (local FlowDeck pages linked to Gitea project) ── @router.get("/projects/{owner}/{repo}/private-pages") -async def list_private_pages(owner: str, repo: str, request: Request): +def list_private_pages(owner: str, repo: str, request: Request): """List private pages for this Gitea project.""" from app.auth.session import SessionManager from app.db import get_conn @@ -225,7 +225,7 @@ async def create_private_page(owner: str, repo: str, request: Request): @router.get("/projects/{owner}/{repo}/private-pages/{page_id}") -async def get_private_page(owner: str, repo: str, page_id: int, request: Request): +def get_private_page(owner: str, repo: str, page_id: int, request: Request): """Get a single private page.""" from app.auth.session import SessionManager from app.db import get_conn @@ -272,7 +272,7 @@ async def update_private_page(owner: str, repo: str, page_id: int, request: Requ @router.delete("/projects/{owner}/{repo}/private-pages/{page_id}") -async def delete_private_page(owner: str, repo: str, page_id: int, request: Request): +def delete_private_page(owner: str, repo: str, page_id: int, request: Request): """Delete a private page.""" from app.auth.session import SessionManager from app.db import get_conn diff --git a/app/routers/github_routes.py b/app/routers/github_routes.py index 2b3bc28..fd00bc5 100644 --- a/app/routers/github_routes.py +++ b/app/routers/github_routes.py @@ -6,7 +6,7 @@ router = APIRouter(tags=["github"], prefix="/api/github") @router.get("/status") -async def github_status(request: Request): +def github_status(request: Request): """Check if the current user has GitHub linked.""" from app.auth.session import SessionManager from app.db import get_conn @@ -22,7 +22,7 @@ async def github_status(request: Request): @router.delete("/disconnect") -async def disconnect_github(request: Request): +def disconnect_github(request: Request): """Remove all GitHub OAuth tokens for the current user.""" from app.auth.session import SessionManager from app.db import get_conn diff --git a/app/routers/governance.py b/app/routers/governance.py index 58ca032..92fa34f 100644 --- a/app/routers/governance.py +++ b/app/routers/governance.py @@ -37,7 +37,7 @@ def _owner_or_admin(request: Request) -> dict: @router.get("/api/v2/agent-policies") -async def list_policies(request: Request): +def list_policies(request: Request): _owner_or_admin(request) with get_conn() as conn: rows = conn.execute("SELECT * FROM agent_policies ORDER BY workspace_id").fetchall() @@ -74,7 +74,7 @@ async def upsert_policy(request: Request): @router.get("/api/v2/agent-approvals") -async def list_approvals(request: Request): +def list_approvals(request: Request): _owner_or_admin(request) status = request.query_params.get("status", "pending") with get_conn() as conn: diff --git a/app/routers/imports.py b/app/routers/imports.py index 1c7ed5b..70b66fc 100644 --- a/app/routers/imports.py +++ b/app/routers/imports.py @@ -79,7 +79,7 @@ async def _read_upload(request: Request) -> tuple[str, bytes, str | None]: @router.get("/sources") -async def import_sources(request: Request): +def import_sources(request: Request): """List every available importer for the UI source picker.""" return {"sources": list_sources()} @@ -289,7 +289,7 @@ async def import_run_batch(request: Request): @router.post("/relations/resolve") -async def import_resolve_relations(request: Request): +def import_resolve_relations(request: Request): """Convert text columns referencing another collection into relation props.""" ws_id, _ = _workspace(request) with get_conn() as conn: @@ -297,12 +297,12 @@ async def import_resolve_relations(request: Request): @router.get("/jobs") -async def import_jobs(request: Request): +def import_jobs(request: Request): return {"jobs": list_jobs()} @router.get("/jobs/{job_id}") -async def import_job(job_id: str): +def import_job(job_id: str): job = get_job(job_id) if not job: raise HTTPException(404, "Job not found") @@ -310,7 +310,7 @@ async def import_job(job_id: str): @router.get("/jobs/{job_id}/report") -async def import_job_report(job_id: str): +def import_job_report(job_id: str): """Download a job's import report as JSON.""" job = get_job(job_id) if not job: diff --git a/app/routers/library.py b/app/routers/library.py index 6944777..61b3854 100644 --- a/app/routers/library.py +++ b/app/routers/library.py @@ -157,7 +157,7 @@ BASE_SELECT = ( @router.get("/recents") -async def library_recents( +def library_recents( request: Request, source_type: str = Query(default="all"), tree: int = Query(default=0), @@ -191,7 +191,7 @@ async def library_recents( @router.get("/favorites") -async def library_favorites( +def library_favorites( request: Request, source_type: str = Query(default="all"), tree: int = Query(default=0), @@ -221,7 +221,7 @@ async def library_favorites( @router.get("/shared") -async def library_shared( +def library_shared( request: Request, source_type: str = Query(default="all"), tree: int = Query(default=0), @@ -292,7 +292,7 @@ async def library_shared( @router.get("/published") -async def library_published( +def library_published( request: Request, source_type: str = Query(default="all"), tree: int = Query(default=0), @@ -317,7 +317,7 @@ async def library_published( @router.get("/private") -async def library_private( +def library_private( request: Request, source_type: str = Query(default="all"), tree: int = Query(default=0), @@ -342,7 +342,7 @@ async def library_private( @router.get("/local-workspace-children/{item_id:int}") -async def library_local_workspace_children(item_id: int, request: Request): +def library_local_workspace_children(item_id: int, request: Request): """Return children of a local workspace item for tree expansion.""" _get_user_id(request) with get_conn() as conn: @@ -410,7 +410,7 @@ async def library_local_workspace_children(item_id: int, request: Request): @router.get("/children/{page_id:int}") -async def library_children(page_id: int, request: Request): +def library_children(page_id: int, request: Request): """Return child pages for a given parent page (for tree expansion in Library).""" uid = _get_user_id(request) with get_conn() as conn: @@ -426,7 +426,7 @@ async def library_children(page_id: int, request: Request): @router.get("/repository") -async def library_repository( +def library_repository( request: Request, gitea_owner: str = Query(default=""), gitea_repo: str = Query(default=""), @@ -450,7 +450,7 @@ async def library_repository( @router.get("/local-workspace") -async def library_local_workspace( +def library_local_workspace( request: Request, workspace_id: int = Query(default=0), ): @@ -535,7 +535,7 @@ def _format_size(size_bytes): @router.get("/workspace") -async def library_workspace( +def library_workspace( request: Request, source_type: str = Query(default="all"), tree: int = Query(default=0), diff --git a/app/routers/meetings.py b/app/routers/meetings.py index 1b19967..c8d9f2e 100644 --- a/app/routers/meetings.py +++ b/app/routers/meetings.py @@ -75,13 +75,13 @@ async def create_link(request: Request): @router.get("/api/v2/calendar-links") -async def get_links(request: Request): +def get_links(request: Request): user = _auth_user(request) return {"links": cal.list_links(user["id"])} @router.delete("/api/v2/calendar-links/{link_id}") -async def remove_link(link_id: int, request: Request): +def remove_link(link_id: int, request: Request): user = _auth_user(request, require_write=True) if not cal.delete_link(user["id"], link_id): raise HTTPException(404, "Link not found") @@ -108,7 +108,7 @@ async def sync_now(link_id: int, request: Request): # ── free/busy ────────────────────────────────────────────────────────────── @router.get("/db/{collection_id}/calendar/freebusy") -async def freebusy(collection_id: int, request: Request): +def freebusy(collection_id: int, request: Request): _auth_user(request) qp = request.query_params try: diff --git a/app/routers/my_tasks.py b/app/routers/my_tasks.py index 35e6ab3..a9394bd 100644 --- a/app/routers/my_tasks.py +++ b/app/routers/my_tasks.py @@ -21,7 +21,7 @@ def _get_current_user(request: Request) -> dict | None: @router.get("", response_class=HTMLResponse) -async def my_tasks_dashboard(request: Request, view: str = "all", days: int = 7): +def my_tasks_dashboard(request: Request, view: str = "all", days: int = 7): """My Tasks — aggregates all pages assigned to the current user across all collections.""" user = _get_current_user(request) user_login = user.get("login", "admin") if user else "admin" @@ -118,7 +118,7 @@ async def my_tasks_dashboard(request: Request, view: str = "all", days: int = 7) @router.get("/api") -async def my_tasks_api(request: Request, view: str = "all", days: int = 7): +def my_tasks_api(request: Request, view: str = "all", days: int = 7): """API: return my tasks as JSON.""" user = _get_current_user(request) user.get("login", "admin") if user else "admin" diff --git a/app/routers/notes.py b/app/routers/notes.py index ef2517a..a92a1bd 100644 --- a/app/routers/notes.py +++ b/app/routers/notes.py @@ -13,7 +13,7 @@ router = APIRouter(tags=["notes"], prefix="/notes") @router.get("/{owner}/{repo}", response_class=HTMLResponse) -async def get_notes(request: Request, owner: str, repo: str): +def get_notes(request: Request, owner: str, repo: str): with get_conn() as conn: row = conn.execute( "SELECT content FROM notes WHERE project_owner=? AND project_name=? AND title='Notes'", diff --git a/app/routers/notifications.py b/app/routers/notifications.py index ea1c7f1..ea44e00 100644 --- a/app/routers/notifications.py +++ b/app/routers/notifications.py @@ -20,7 +20,7 @@ def _current_user(request: Request) -> dict: @router.get("") -async def list_notifications(request: Request, limit: int = 50): +def list_notifications(request: Request, limit: int = 50): """List the current user's notifications, newest first.""" user = _current_user(request) with get_conn() as conn: @@ -44,7 +44,7 @@ async def list_notifications(request: Request, limit: int = 50): @router.get("/unread-count") -async def unread_count(request: Request): +def unread_count(request: Request): """Unread count for the topbar badge.""" user = _current_user(request) with get_conn() as conn: @@ -83,7 +83,7 @@ async def mark_all_read(request: Request): @router.get("/prefs") -async def get_prefs(request: Request): +def get_prefs(request: Request): """Return the current user's notification email preferences.""" user = _current_user(request) from app.services import notifications as notif @@ -105,7 +105,7 @@ async def set_prefs(request: Request): @router.get("/timezone") -async def get_timezone(request: Request): +def get_timezone(request: Request): """Return the current user's IANA timezone ('' = UTC).""" user = _current_user(request) with get_conn() as conn: @@ -131,7 +131,7 @@ async def set_timezone(request: Request): @router.get("/users/search") -async def search_users(request: Request, q: str = ""): +def search_users(request: Request, q: str = ""): """User autocomplete for @mentions.""" _current_user(request) q = (q or "").strip() diff --git a/app/routers/onboarding.py b/app/routers/onboarding.py index d345751..a1f0f35 100644 --- a/app/routers/onboarding.py +++ b/app/routers/onboarding.py @@ -28,7 +28,7 @@ def _require_user(request: Request) -> dict: @router.get("/welcome", response_class=HTMLResponse) -async def onboarding_page(request: Request): +def onboarding_page(request: Request): """Onboarding wizard. Redirects logged-out users to login and users who already have a workspace straight to the app.""" user = SessionManager.decode_session(request.cookies.get("flowdeck_session", "")) diff --git a/app/routers/permissions.py b/app/routers/permissions.py index 276ad58..8c1dc2d 100644 --- a/app/routers/permissions.py +++ b/app/routers/permissions.py @@ -158,7 +158,7 @@ def _set_permission_type(request: Request, pm: PermissionManager, resource_type: @router.get("/pages/{page_id}/permissions") -async def list_page_permissions(page_id: int, request: Request): +def list_page_permissions(page_id: int, request: Request): """List explicit page grants + the caller's effective role.""" pm = _pm(request) if not pm.can_view_page(page_id): @@ -174,7 +174,7 @@ async def list_page_permissions(page_id: int, request: Request): @router.get("/pages/{page_id}/permissions/mine") -async def my_page_permission(page_id: int, request: Request): +def my_page_permission(page_id: int, request: Request): """Effective role of the current user on a page (UI gating).""" pm = _pm(request) if not pm.can_view_page(page_id): @@ -224,7 +224,7 @@ async def batch_page_permissions(page_id: int, request: Request): @router.delete("/pages/{page_id}/permissions/{perm_id}") -async def revoke_page_permission(page_id: int, perm_id: int, request: Request): +def revoke_page_permission(page_id: int, perm_id: int, request: Request): pm = _pm(request) if not pm.can_manage_page_permissions(page_id): raise HTTPException(403, "Only a page owner can manage its permissions") @@ -243,7 +243,7 @@ async def set_page_permission_type(page_id: int, request: Request): @router.get("/collections/{collection_id}/permissions") -async def list_collection_permissions(collection_id: int, request: Request): +def list_collection_permissions(collection_id: int, request: Request): pm = _pm(request) if not pm.can_view_collection(collection_id): raise HTTPException(404, "Collection not found") @@ -276,7 +276,7 @@ async def grant_collection_permission(collection_id: int, request: Request): @router.delete("/collections/{collection_id}/permissions/{perm_id}") -async def revoke_collection_permission(collection_id: int, perm_id: int, request: Request): +def revoke_collection_permission(collection_id: int, perm_id: int, request: Request): pm = _pm(request) if not pm.can_manage_collection_permissions(collection_id): raise HTTPException(403, "Only a collection owner can manage its permissions") @@ -294,7 +294,7 @@ async def set_collection_permission_type(collection_id: int, request: Request): @router.get("/collections/{collection_id}/properties/visible") -async def visible_properties(collection_id: int, request: Request): +def visible_properties(collection_id: int, request: Request): """Split property ids into visible / hidden for the current user.""" pm = _pm(request) if not pm.can_view_collection(collection_id): @@ -316,7 +316,7 @@ async def visible_properties(collection_id: int, request: Request): @router.get("/collections/{collection_id}/properties/{property_id}/permissions") -async def list_property_permissions(collection_id: int, property_id: int, request: Request): +def list_property_permissions(collection_id: int, property_id: int, request: Request): pm = _pm(request) if not pm.can_view_collection(collection_id): raise HTTPException(404, "Collection not found") @@ -349,7 +349,7 @@ async def grant_property_permission(collection_id: int, property_id: int, reques @router.delete("/collections/{collection_id}/properties/{property_id}/permissions/{perm_id}") -async def revoke_property_permission(collection_id: int, property_id: int, +def revoke_property_permission(collection_id: int, property_id: int, perm_id: int, request: Request): pm = _pm(request) if not pm.can_manage_collection_permissions(collection_id): @@ -362,7 +362,7 @@ async def revoke_property_permission(collection_id: int, property_id: int, @router.get("/groups") -async def list_groups(request: Request, workspace_id: int | None = None): +def list_groups(request: Request, workspace_id: int | None = None): user = _require_user(request) pm = PermissionManager(user["id"]) return {"groups": pm.get_groups_for_workspace(workspace_id)} @@ -405,7 +405,7 @@ async def update_group(group_id: int, request: Request): @router.delete("/groups/{group_id}") -async def delete_group(group_id: int, request: Request): +def delete_group(group_id: int, request: Request): pm = _pm(request) with get_conn() as conn: row = conn.execute( @@ -422,7 +422,7 @@ async def delete_group(group_id: int, request: Request): @router.get("/groups/{group_id}/members") -async def list_group_members(group_id: int, request: Request): +def list_group_members(group_id: int, request: Request): user = _require_user(request) return {"members": PermissionManager(user["id"]).get_group_members(group_id)} @@ -451,7 +451,7 @@ async def add_group_member(group_id: int, request: Request): @router.delete("/groups/{group_id}/members/{user_id}") -async def remove_group_member(group_id: int, user_id: int, request: Request): +def remove_group_member(group_id: int, user_id: int, request: Request): pm = _pm(request) with get_conn() as conn: row = conn.execute( @@ -473,7 +473,7 @@ async def remove_group_member(group_id: int, user_id: int, request: Request): @router.get("/users") -async def list_users(request: Request, workspace_id: int | None = None, q: str = ""): +def list_users(request: Request, workspace_id: int | None = None, q: str = ""): """Workspace members (+ admins) for the grant pickers.""" _require_user(request) q = (q or "").strip().lower() @@ -503,7 +503,7 @@ async def list_users(request: Request, workspace_id: int | None = None, q: str = @router.get("/audit/permissions") -async def permission_audit(request: Request, limit: int = 100): +def permission_audit(request: Request, limit: int = 100): """Full permission change history — workspace owner/admin only.""" user = _require_user(request) uid = user["id"] diff --git a/app/routers/projects.py b/app/routers/projects.py index bc55f32..f082f91 100644 --- a/app/routers/projects.py +++ b/app/routers/projects.py @@ -22,7 +22,7 @@ def _require_admin(request: Request) -> dict: @router.get("") -async def list_projects(request: Request): +def list_projects(request: Request): """List all synced projects (optionally filtered by type).""" proj_type = request.query_params.get("type") or None return {"projects": projects_svc.list_projects(proj_type)} diff --git a/app/routers/public_api.py b/app/routers/public_api.py index 50a21b0..24cb00d 100644 --- a/app/routers/public_api.py +++ b/app/routers/public_api.py @@ -55,7 +55,7 @@ def verify_token(authorization: str | None = Header(None)): @router.post("/token") -async def generate_token(request: Request): +def generate_token(request: Request): """Generate a public API access token (A4 : session obligatoire — plus de « legacy shared token » `user_id=0` créable par un anonymous).""" user = SessionManager.decode_session(request.cookies.get("flowdeck_session", "")) @@ -72,14 +72,14 @@ async def generate_token(request: Request): @router.get("/collections", dependencies=[Depends(verify_token)]) -async def public_list_collections(request: Request): +def public_list_collections(request: Request): with get_conn() as conn: rows = conn.execute("SELECT id, name, description, icon, created_at FROM collections ORDER BY name").fetchall() return {"collections": [dict(r) for r in rows]} @router.get("/collections/{collection_id}", dependencies=[Depends(verify_token)]) -async def public_get_collection(request: Request, collection_id: int): +def public_get_collection(request: Request, collection_id: int): with get_conn() as conn: coll = conn.execute("SELECT * FROM collections WHERE id=?", (collection_id,)).fetchone() if not coll: @@ -92,7 +92,7 @@ async def public_get_collection(request: Request, collection_id: int): @router.get("/collections/{collection_id}/pages", dependencies=[Depends(verify_token)]) -async def public_list_pages(request: Request, collection_id: int): +def public_list_pages(request: Request, collection_id: int): with get_conn() as conn: pages = conn.execute( "SELECT id, title, icon, position, property_values_json, created_at FROM collection_pages WHERE collection_id=? AND parent_id IS NULL ORDER BY position", @@ -102,7 +102,7 @@ async def public_list_pages(request: Request, collection_id: int): @router.get("/pages/{page_id}", dependencies=[Depends(verify_token)]) -async def public_get_page(request: Request, page_id: int): +def public_get_page(request: Request, page_id: int): with get_conn() as conn: p = conn.execute("SELECT * FROM collection_pages WHERE id=?", (page_id,)).fetchone() if not p: @@ -111,7 +111,7 @@ async def public_get_page(request: Request, page_id: int): @router.get("/my-tasks", dependencies=[Depends(verify_token)]) -async def public_my_tasks(request: Request): +def public_my_tasks(request: Request): """Public API: list tasks (requires valid token).""" with get_conn() as conn: pages = conn.execute( diff --git a/app/routers/realtime.py b/app/routers/realtime.py index 3b5b3f4..c6c3375 100644 --- a/app/routers/realtime.py +++ b/app/routers/realtime.py @@ -18,7 +18,7 @@ router = APIRouter(tags=["realtime"]) @router.get("/api/realtime/stats") -async def realtime_stats(request: Request): +def realtime_stats(request: Request): """Observabilité realtime v6.4.0 : rooms, connexions, ops, merges, conflits. Réservé aux utilisateurs authentifiés (données d'activité internes). diff --git a/app/routers/scim.py b/app/routers/scim.py index 82f5d83..e8ceb6d 100644 --- a/app/routers/scim.py +++ b/app/routers/scim.py @@ -60,7 +60,7 @@ def _scim_user(row) -> dict: # ── SCIM resources ───────────────────────────────────────────────────────── @router.get("/scim/v2/Users") -async def scim_list(request: Request): +def scim_list(request: Request): _scim_guard(request) with get_conn() as conn: rows = conn.execute("SELECT * FROM users ORDER BY id LIMIT 100").fetchall() @@ -99,7 +99,7 @@ async def scim_create(request: Request): @router.get("/scim/v2/Users/{user_id}") -async def scim_get(user_id: str, request: Request): +def scim_get(user_id: str, request: Request): _scim_guard(request) with get_conn() as conn: row = conn.execute("SELECT * FROM users WHERE id=?", (user_id,)).fetchone() @@ -164,7 +164,7 @@ async def scim_patch(user_id: str, request: Request): @router.delete("/scim/v2/Users/{user_id}") -async def scim_delete(user_id: str, request: Request): +def scim_delete(user_id: str, request: Request): _scim_guard(request) with get_conn() as conn: row = conn.execute("SELECT id FROM users WHERE id=?", (user_id,)).fetchone() @@ -200,7 +200,7 @@ async def create_scim_token(request: Request): @router.get("/api/v2/scim/tokens") -async def list_scim_tokens(request: Request): +def list_scim_tokens(request: Request): _admin_session(request) with get_conn() as conn: rows = conn.execute("SELECT id, name, created_by, revoked, created_at" @@ -209,7 +209,7 @@ async def list_scim_tokens(request: Request): @router.delete("/api/v2/scim/tokens/{token_id}") -async def revoke_scim_token(token_id: int, request: Request): +def revoke_scim_token(token_id: int, request: Request): admin = _admin_session(request) with get_conn() as conn: conn.execute("UPDATE scim_tokens SET revoked=1 WHERE id=?", (token_id,)) @@ -221,7 +221,7 @@ async def revoke_scim_token(token_id: int, request: Request): # ── domain claims ────────────────────────────────────────────────────────── @router.get("/api/v2/domain-claims") -async def list_domains(request: Request): +def list_domains(request: Request): _admin_session(request) with get_conn() as conn: rows = conn.execute("SELECT * FROM domain_claims ORDER BY domain").fetchall() @@ -288,7 +288,7 @@ async def verify_domain(domain_id: int, request: Request): @router.delete("/api/v2/domain-claims/{domain_id}") -async def delete_domain(domain_id: int, request: Request): +def delete_domain(domain_id: int, request: Request): admin = _admin_session(request) with get_conn() as conn: conn.execute("DELETE FROM domain_claims WHERE id=?", (domain_id,)) diff --git a/app/routers/search.py b/app/routers/search.py index 0d0e4c4..ddb9be3 100644 --- a/app/routers/search.py +++ b/app/routers/search.py @@ -14,7 +14,7 @@ router = APIRouter(tags=["search"]) @router.get("/api/search") -async def search(request: Request, q: str = Query(default="")): +def search(request: Request, q: str = Query(default="")): user = SessionManager.decode_session(request.cookies.get("flowdeck_session", "")) user_id = user.get("id") if user and user.get("id") else None diff --git a/app/routers/search_ai.py b/app/routers/search_ai.py index d86f8dc..901d3f0 100644 --- a/app/routers/search_ai.py +++ b/app/routers/search_ai.py @@ -43,7 +43,7 @@ def _auth_user(request: Request) -> dict: @router.get("/api/v2/search/hybrid") -async def hybrid(request: Request): +def hybrid(request: Request): user = _auth_user(request) q = (request.query_params.get("q") or request.query_params.get("query") or "").strip() if not q: @@ -81,7 +81,7 @@ async def ask_ai(request: Request): @router.get("/api/v2/search/index-status") -async def index_status(request: Request): +def index_status(request: Request): """How many resources are indexed vs pending (owner/admin visibility).""" user = _auth_user(request) with get_conn() as conn: diff --git a/app/routers/security.py b/app/routers/security.py index bc4167e..4f97c45 100644 --- a/app/routers/security.py +++ b/app/routers/security.py @@ -32,7 +32,7 @@ def _current_user_id(request: Request) -> int: @router.get("/tokens") -async def list_tokens(request: Request): +def list_tokens(request: Request): """List the current user's API tokens (prefix only, no secrets).""" uid = _current_user_id(request) with get_conn() as conn: @@ -63,7 +63,7 @@ async def create_token(request: Request): @router.delete("/tokens/{token_id:int}") -async def revoke_token(token_id: int, request: Request): +def revoke_token(token_id: int, request: Request): """Revoke an API token (soft delete).""" uid = _current_user_id(request) with get_conn() as conn: @@ -81,7 +81,7 @@ async def revoke_token(token_id: int, request: Request): @router.get("/sessions") -async def list_sessions(request: Request): +def list_sessions(request: Request): """List the current user's active sessions with their devices.""" uid = _current_user_id(request) current_sid = SessionManager.session_id(request.cookies.get("flowdeck_session", "")) @@ -101,7 +101,7 @@ async def list_sessions(request: Request): @router.post("/sessions/{sid}/revoke") -async def revoke_session(sid: str, request: Request): +def revoke_session(sid: str, request: Request): """Revoke an active session. If it's the current one, the user is logged out.""" uid = _current_user_id(request) with get_conn() as conn: diff --git a/app/routers/sharing.py b/app/routers/sharing.py index 05b0d21..446a0a6 100644 --- a/app/routers/sharing.py +++ b/app/routers/sharing.py @@ -204,7 +204,7 @@ async def update_share_permission(page_id: int, share_id: int, request: Request) @router.delete("/pages/{page_id}/share/{share_id}") -async def remove_share(page_id: int, share_id: int, request: Request): +def remove_share(page_id: int, share_id: int, request: Request): """Remove a share invitation.""" _require_auth(request) @@ -238,7 +238,7 @@ async def remove_share(page_id: int, share_id: int, request: Request): @router.get("/pages/{page_id}/shares") -async def list_shares(page_id: int, request: Request): +def list_shares(page_id: int, request: Request): """Get all shares for a page.""" _require_auth(request) diff --git a/app/routers/sidebar_config.py b/app/routers/sidebar_config.py index 607821d..a3f58d9 100644 --- a/app/routers/sidebar_config.py +++ b/app/routers/sidebar_config.py @@ -35,7 +35,7 @@ DEFAULT_CONFIG = { @router.get("/config") -async def get_sidebar_config(request: Request): +def get_sidebar_config(request: Request): """Get the current user's sidebar customization config.""" user = _get_user(request) with get_conn() as conn: diff --git a/app/routers/sites.py b/app/routers/sites.py index 625e848..f659df2 100644 --- a/app/routers/sites.py +++ b/app/routers/sites.py @@ -315,7 +315,7 @@ async def create_site(request: Request): @router.get("/api/v2/sites") -async def list_sites(request: Request): +def list_sites(request: Request): user = _auth_user(request) limit, offset = parse_pagination(request) with get_conn() as conn: @@ -333,7 +333,7 @@ async def list_sites(request: Request): @router.get("/api/v2/sites/{site_id}") -async def get_site(site_id: int, request: Request): +def get_site(site_id: int, request: Request): user = _auth_user(request) with get_conn() as conn: row = conn.execute("SELECT * FROM sites WHERE id=?", (site_id,)).fetchone() @@ -407,7 +407,7 @@ async def update_site(site_id: int, request: Request): @router.delete("/api/v2/sites/{site_id}") -async def delete_site(site_id: int, request: Request): +def delete_site(site_id: int, request: Request): user = _auth_user(request, require_write=True) with get_conn() as conn: row = conn.execute("SELECT * FROM sites WHERE id=?", (site_id,)).fetchone() @@ -422,7 +422,7 @@ async def delete_site(site_id: int, request: Request): @router.get("/api/v2/sites/{site_id}/pages") -async def list_site_pages(site_id: int, request: Request): +def list_site_pages(site_id: int, request: Request): user = _auth_user(request) with get_conn() as conn: row = conn.execute("SELECT * FROM sites WHERE id=?", (site_id,)).fetchone() @@ -461,7 +461,7 @@ async def add_site_page(site_id: int, request: Request): @router.delete("/api/v2/sites/{site_id}/pages/{page_id}") -async def remove_site_page(site_id: int, page_id: int, request: Request): +def remove_site_page(site_id: int, page_id: int, request: Request): user = _auth_user(request, require_write=True) with get_conn() as conn: row = conn.execute("SELECT * FROM sites WHERE id=?", (site_id,)).fetchone() @@ -478,7 +478,7 @@ async def remove_site_page(site_id: int, page_id: int, request: Request): @router.get("/api/v2/sites/{site_id}/stats") -async def site_stats(site_id: int, request: Request, days: int = 30): +def site_stats(site_id: int, request: Request, days: int = 30): user = _auth_user(request) days = max(1, min(int(days or 30), 365)) with get_conn() as conn: @@ -516,7 +516,7 @@ def _public_guard(site: dict, request: Request): @router.get("/s/{slug}", response_class=HTMLResponse) -async def public_site_home(request: Request, slug: str): +def public_site_home(request: Request, slug: str): with get_conn() as conn: site = _resolve_site(conn, slug=slug, host=request.headers.get("host", "")) @@ -537,7 +537,7 @@ async def public_site_home(request: Request, slug: str): @router.get("/s/{slug}/sitemap.xml", response_class=PlainTextResponse) -async def site_sitemap(request: Request, slug: str): +def site_sitemap(request: Request, slug: str): with get_conn() as conn: site = _resolve_site(conn, slug=slug) if not site or _site_expired(site) or site.get("password_hash"): @@ -556,7 +556,7 @@ async def site_sitemap(request: Request, slug: str): @router.get("/s/{slug}/{page_ref}", response_class=HTMLResponse) -async def public_site_page(request: Request, slug: str, page_ref: str): +def public_site_page(request: Request, slug: str, page_ref: str): with get_conn() as conn: site = _resolve_site(conn, slug=slug, host=request.headers.get("host", "")) if not site: @@ -614,7 +614,7 @@ async def public_site_auth(request: Request, slug: str): # ── Public Forms ─────────────────────────────────────────────────────────── @router.get("/api/v2/collections/{collection_id}/form") -async def get_form_config(collection_id: int, request: Request): +def get_form_config(collection_id: int, request: Request): _auth_user(request) with get_conn() as conn: info = _form_config(conn, collection_id) @@ -662,7 +662,7 @@ def _collection_props(conn, collection_id: int) -> list[dict]: @router.get("/f/{token}", response_class=HTMLResponse) -async def public_form(request: Request, token: str): +def public_form(request: Request, token: str): embed = request.query_params.get("embed") == "1" with get_conn() as conn: row = conn.execute("SELECT * FROM collections").fetchone() diff --git a/app/routers/sso.py b/app/routers/sso.py index 9d54d70..19c15da 100644 --- a/app/routers/sso.py +++ b/app/routers/sso.py @@ -106,7 +106,7 @@ def _login_error(message: str, *, cfg: dict | None, identifier: str = "", reques @router.get("/auth/saml/login") -async def saml_login(request: Request, next: str = DEFAULT_NEXT): +def saml_login(request: Request, next: str = DEFAULT_NEXT): """SP-initiated SSO: issue an AuthnRequest and redirect to the IdP.""" if not _rate_ok(request, "saml"): return _page("Too many attempts", "

Please wait a minute and try again.

", status=429) @@ -218,7 +218,7 @@ async def saml_callback(request: Request): @router.get("/auth/saml/metadata") -async def saml_metadata(request: Request): +def saml_metadata(request: Request): """SP metadata XML — paste into the IdP (Azure AD / Okta / Keycloak…).""" cfg = _sso_config_or_error() if not cfg or cfg["provider_type"] != "saml": @@ -536,7 +536,7 @@ async def _require_admin(request: Request, *, write: bool) -> dict: @router.get("/api/v2/sso/providers") -async def sso_providers(request: Request): +def sso_providers(request: Request): """Public: what the login page should show (button list + sso_only flag).""" cfg = _sso_config_or_error() if not cfg: diff --git a/app/routers/sync.py b/app/routers/sync.py index 0c7b9e3..46c89df 100644 --- a/app/routers/sync.py +++ b/app/routers/sync.py @@ -82,7 +82,7 @@ async def sync_batch(request: Request, authorization: str | None = Header(defaul @router.get("/status") -async def sync_status(request: Request, workspace_id: int = Query(default=None), +def sync_status(request: Request, workspace_id: int = Query(default=None), authorization: str | None = Header(default=None)): """Synchronization status for the workspace (pending server queue, last sync).""" user = _user(request, authorization, required_scope="read") diff --git a/app/routers/web_clipper.py b/app/routers/web_clipper.py index 476e4d7..b3bc01e 100644 --- a/app/routers/web_clipper.py +++ b/app/routers/web_clipper.py @@ -227,7 +227,7 @@ async def revoke_extension_device(device_id: int, request: Request): # ── HTML: /extensions download page ── @router.get("/extensions", response_class=HTMLResponse) -async def extensions_page(request: Request): +def extensions_page(request: Request): from app.routers.dashboard import _sidebar_data from app.templating import ENV diff --git a/app/routers/webauthn.py b/app/routers/webauthn.py index 37a8232..f4eba95 100644 --- a/app/routers/webauthn.py +++ b/app/routers/webauthn.py @@ -56,7 +56,7 @@ def _session_user(request: Request) -> dict: @router.post("/register/begin") -async def register_begin(request: Request): +def register_begin(request: Request): if not _require_lib(): raise HTTPException(501, "WebAuthn library not installed") from webauthn import generate_registration_options, options_to_json @@ -193,7 +193,7 @@ async def login_finish(request: Request): @router.get("/keys") -async def list_keys(request: Request): +def list_keys(request: Request): user = _session_user(request) with get_conn() as conn: rows = conn.execute("SELECT id, name, sign_count, created_at FROM webauthn_credentials" @@ -202,7 +202,7 @@ async def list_keys(request: Request): @router.delete("/keys/{key_id}") -async def delete_key(key_id: int, request: Request): +def delete_key(key_id: int, request: Request): user = _session_user(request) with get_conn() as conn: cur = conn.execute("DELETE FROM webauthn_credentials WHERE id=? AND user_id=?", diff --git a/app/routers/wiki.py b/app/routers/wiki.py index 9abcc56..9be7ea7 100644 --- a/app/routers/wiki.py +++ b/app/routers/wiki.py @@ -98,7 +98,7 @@ def _can_verify(user: dict, page: dict) -> bool: # ── teamspaces ───────────────────────────────────────────────────────────── @router.get("/api/v2/wiki/teamspaces") -async def list_teamspaces(request: Request): +def list_teamspaces(request: Request): user = _user(request) wid = request.query_params.get("workspace_id") if wid: @@ -112,7 +112,7 @@ async def list_teamspaces(request: Request): @router.get("/wiki/teamspaces/{teamspace_id}", response_class=HTMLResponse) -async def teamspace_page(teamspace_id: int, request: Request): +def teamspace_page(teamspace_id: int, request: Request): """Teamspace detail HTML page — sidebar entry point.""" user = _user(request) ts = _teamspace_or_404(teamspace_id, user["id"]) @@ -204,7 +204,7 @@ async def create_teamspace(request: Request): @router.get("/api/v2/wiki/teamspaces/{teamspace_id}") -async def get_teamspace(teamspace_id: int, request: Request): +def get_teamspace(teamspace_id: int, request: Request): user = _user(request) ts = _teamspace_or_404(teamspace_id, user["id"]) ts["role"] = wiki.get_teamspace_role(user["id"], teamspace_id) @@ -213,7 +213,7 @@ async def get_teamspace(teamspace_id: int, request: Request): @router.get("/api/v2/wiki/teamspaces/{teamspace_id}/members") -async def list_members(teamspace_id: int, request: Request): +def list_members(teamspace_id: int, request: Request): user = _user(request) _teamspace_or_404(teamspace_id, user["id"]) with get_conn() as conn: @@ -251,7 +251,7 @@ async def set_member(teamspace_id: int, member_id: int, request: Request): @router.delete("/api/v2/wiki/teamspaces/{teamspace_id}/members/{member_id}") -async def remove_member(teamspace_id: int, member_id: int, request: Request): +def remove_member(teamspace_id: int, member_id: int, request: Request): user = _user(request) _teamspace_or_404(teamspace_id, user["id"]) if not wiki.can_write_teamspace(user["id"], teamspace_id): @@ -268,7 +268,7 @@ async def remove_member(teamspace_id: int, member_id: int, request: Request): # ── verified pages ───────────────────────────────────────────────────────── @router.get("/api/v2/wiki/pages/{page_id}/verification") -async def get_verification(page_id: int, request: Request): +def get_verification(page_id: int, request: Request): _user(request) _page_or_404(page_id) return {"verification": wiki.verification(page_id)} @@ -292,7 +292,7 @@ async def verify_page(page_id: int, request: Request): @router.delete("/api/v2/wiki/pages/{page_id}/verify") -async def unverify_page(page_id: int, request: Request): +def unverify_page(page_id: int, request: Request): user = _user(request) _page_or_404(page_id) if not wiki.unverify_page(page_id): @@ -302,7 +302,7 @@ async def unverify_page(page_id: int, request: Request): @router.get("/api/v2/wiki/verified") -async def list_verified(request: Request): +def list_verified(request: Request): """Verified (non-expired) pages of a workspace — the ✅ wiki index.""" user = _user(request) wid = _workspace_id(request) @@ -331,7 +331,7 @@ async def list_verified(request: Request): # ── follows ──────────────────────────────────────────────────────────────── @router.post("/api/v2/wiki/pages/{page_id}/follow") -async def follow_page(page_id: int, request: Request): +def follow_page(page_id: int, request: Request): user = _user(request) _page_or_404(page_id) now = wiki.toggle_follow(page_id, user["id"]) @@ -339,7 +339,7 @@ async def follow_page(page_id: int, request: Request): @router.get("/api/v2/wiki/pages/{page_id}/followers") -async def list_followers(page_id: int, request: Request): +def list_followers(page_id: int, request: Request): _user(request) _page_or_404(page_id) ids = wiki.followers(page_id) @@ -372,7 +372,7 @@ async def react(comment_id: int, request: Request): @router.get("/api/v2/wiki/comments/{comment_id}/reactions") -async def list_reactions(comment_id: int, request: Request): +def list_reactions(comment_id: int, request: Request): _user(request) return {"comment_id": comment_id, "reactions": wiki.reactions(comment_id)} @@ -402,7 +402,7 @@ async def create_guest(page_id: int, request: Request): @router.get("/api/v2/wiki/pages/{page_id}/guests") -async def list_guests(page_id: int, request: Request): +def list_guests(page_id: int, request: Request): _user(request) _page_or_404(page_id) with get_conn() as conn: @@ -413,7 +413,7 @@ async def list_guests(page_id: int, request: Request): @router.delete("/api/v2/wiki/guests/{share_id}") -async def revoke_guest(share_id: int, request: Request): +def revoke_guest(share_id: int, request: Request): user = _user(request) with get_conn() as conn: if not conn.execute("SELECT 1 FROM guest_shares WHERE id=?", (share_id,)).fetchone(): @@ -436,7 +436,7 @@ Ask the person who shared it with you for a new link.

""" @router.get("/g/{token}", response_class=HTMLResponse) -async def guest_page(token: str, request: Request): +def guest_page(token: str, request: Request): """Account-less page access (read-only or commenter). 404 if inactive.""" share = wiki.resolve_guest_share(token) if not share: @@ -465,7 +465,7 @@ Editing is disabled. # ── page views ───────────────────────────────────────────────────────────── @router.get("/api/v2/wiki/pages/{page_id}/views") -async def page_views(page_id: int, request: Request): +def page_views(page_id: int, request: Request): user = _user(request) page = _page_or_404(page_id) if page.get("teamspace_id") and not wiki.can_write_teamspace(user["id"], page["teamspace_id"]): @@ -476,7 +476,7 @@ async def page_views(page_id: int, request: Request): # ── wiki home ────────────────────────────────────────────────────────────── @router.get("/api/v2/wiki/home") -async def wiki_home(request: Request): +def wiki_home(request: Request): """Aggregated knowledge home: verified pages + recents + teamspaces.""" user = _user(request) wid = _workspace_id(request) @@ -500,7 +500,7 @@ async def wiki_home(request: Request): @router.post("/api/v2/wiki/verify-expiry-sweep") -async def sweep_expiry(request: Request): +def sweep_expiry(request: Request): """Notify verifiers whose ✅ expires within 7 days (idempotent-ish job).""" user = _user(request) with get_conn() as conn: diff --git a/app/routers/workers.py b/app/routers/workers.py index 92ee4f6..2cc0ad3 100644 --- a/app/routers/workers.py +++ b/app/routers/workers.py @@ -72,7 +72,7 @@ async def create_worker(request: Request): @router.get("/api/v2/workers") -async def list_workers(request: Request): +def list_workers(request: Request): _auth_user(request) limit, offset = parse_pagination(request) with get_conn() as conn: @@ -87,7 +87,7 @@ async def list_workers(request: Request): @router.get("/api/v2/workers/{worker_id}") -async def get_worker(worker_id: int, request: Request): +def get_worker(worker_id: int, request: Request): user = _auth_user(request) with get_conn() as conn: row = conn.execute("SELECT * FROM workers WHERE id=?", (worker_id,)).fetchone() @@ -146,7 +146,7 @@ async def update_worker(worker_id: int, request: Request): @router.delete("/api/v2/workers/{worker_id}") -async def delete_worker(worker_id: int, request: Request): +def delete_worker(worker_id: int, request: Request): user = _auth_user(request, require_write=True) with get_conn() as conn: row = conn.execute("SELECT * FROM workers WHERE id=?", (worker_id,)).fetchone() @@ -186,7 +186,7 @@ async def run_worker_endpoint(worker_id: int, request: Request): @router.get("/api/v2/workers/{worker_id}/runs") -async def worker_runs(worker_id: int, request: Request): +def worker_runs(worker_id: int, request: Request): _auth_user(request) limit, _offset = parse_pagination(request, default_limit=20) with get_conn() as conn: @@ -199,7 +199,7 @@ async def worker_runs(worker_id: int, request: Request): @router.post("/api/v2/workers/{worker_id}/fork") -async def fork_worker_endpoint(worker_id: int, request: Request): +def fork_worker_endpoint(worker_id: int, request: Request): user = _auth_user(request, require_write=True) out = worker_service.fork_worker(worker_id, user["id"]) audit_log(user, "worker.fork", "worker", worker_id, "", request) @@ -207,7 +207,7 @@ async def fork_worker_endpoint(worker_id: int, request: Request): @router.get("/api/v2/workers-usage") -async def workers_usage(request: Request): +def workers_usage(request: Request): user = _auth_user(request) ws_raw = request.query_params.get("workspace_id") wid = int(ws_raw) if ws_raw and str(ws_raw).isdigit() else None diff --git a/app/routers/workspace.py b/app/routers/workspace.py index 1e85e6b..c35afa4 100644 --- a/app/routers/workspace.py +++ b/app/routers/workspace.py @@ -79,7 +79,7 @@ async def create_workspace(request: Request): # ── Members ── @router.get("/{ws_id}/members") -async def list_members(request: Request, ws_id: int): +def list_members(request: Request, ws_id: int): if not SessionManager.decode_session(request.cookies.get("flowdeck_session", "")): raise HTTPException(401, "Authentication required") with get_conn() as conn: @@ -122,7 +122,7 @@ async def update_member_role(request: Request, ws_id: int, user_id: int): @router.delete("/{ws_id}/members/{user_id}") -async def remove_member(request: Request, ws_id: int, user_id: int): +def remove_member(request: Request, ws_id: int, user_id: int): _require_ws_admin(request, ws_id) with get_conn() as conn: conn.execute("DELETE FROM workspace_members WHERE workspace_id=? AND user_id=?", (ws_id, user_id)) @@ -133,7 +133,7 @@ async def remove_member(request: Request, ws_id: int, user_id: int): # ── Comments ── @router.get("/pages/{page_id}/comments") -async def list_comments(request: Request, page_id: int): +def list_comments(request: Request, page_id: int): with get_conn() as conn: rows = conn.execute( "SELECT c.*, u.login, u.avatar_url FROM comments c JOIN users u ON c.user_id=u.id WHERE c.page_id=? ORDER BY c.created_at", @@ -187,7 +187,7 @@ async def update_comment(request: Request, comment_id: int): # ── Page History ── @router.get("/pages/{page_id}/history") -async def page_history(request: Request, page_id: int): +def page_history(request: Request, page_id: int): with get_conn() as conn: rows = conn.execute( "SELECT * FROM page_history WHERE page_id=? ORDER BY created_at DESC LIMIT 50", @@ -214,7 +214,7 @@ async def record_history(request: Request, page_id: int): # ── Favorites ── @router.get("/favorites") -async def list_favorites(request: Request): +def list_favorites(request: Request): user = _current_user(request) uid = user.get("id", 1) with get_conn() as conn: @@ -251,7 +251,7 @@ async def add_favorite(request: Request): @router.delete("/favorites/{fav_id}") -async def remove_favorite(request: Request, fav_id: int): +def remove_favorite(request: Request, fav_id: int): with get_conn() as conn: conn.execute("DELETE FROM favorites WHERE id=?", (fav_id,)) conn.commit() @@ -261,7 +261,7 @@ async def remove_favorite(request: Request, fav_id: int): # ── Templates ── @router.get("/templates/database") -async def list_db_templates(request: Request): +def list_db_templates(request: Request): with get_conn() as conn: rows = conn.execute("SELECT * FROM database_templates ORDER BY name").fetchall() return {"templates": [dict(r) for r in rows]} @@ -296,7 +296,7 @@ async def apply_db_template(request: Request, tid: int): @router.get("/collections/{collection_id}/templates/page") -async def list_page_templates(request: Request, collection_id: int): +def list_page_templates(request: Request, collection_id: int): with get_conn() as conn: rows = conn.execute( "SELECT * FROM page_templates WHERE collection_id=? ORDER BY name", (collection_id,) @@ -369,7 +369,7 @@ async def update_page_template(request: Request, collection_id: int, tid: int): @router.delete("/collections/{collection_id}/templates/page/{tid}") -async def delete_page_template(request: Request, collection_id: int, tid: int): +def delete_page_template(request: Request, collection_id: int, tid: int): """Delete a page template.""" with get_conn() as conn: tmpl = conn.execute( @@ -385,7 +385,7 @@ async def delete_page_template(request: Request, collection_id: int, tid: int): # ── v4.2.0: Dashboards ── @router.get("/collections/{collection_id}/dashboards") -async def list_dashboards(request: Request, collection_id: int): +def list_dashboards(request: Request, collection_id: int): """List all dashboards for a collection.""" with get_conn() as conn: rows = conn.execute( @@ -436,7 +436,7 @@ async def update_dashboard(request: Request, collection_id: int, did: int): @router.delete("/collections/{collection_id}/dashboards/{did}") -async def delete_dashboard(request: Request, collection_id: int, did: int): +def delete_dashboard(request: Request, collection_id: int, did: int): """Delete a dashboard.""" with get_conn() as conn: dash = conn.execute( @@ -452,7 +452,7 @@ async def delete_dashboard(request: Request, collection_id: int, did: int): # ── v4.5.0: Sprints ── @router.get("/collections/{collection_id}/sprints") -async def list_sprints(request: Request, collection_id: int): +def list_sprints(request: Request, collection_id: int): """List all sprints for a collection.""" with get_conn() as conn: rows = conn.execute( @@ -528,7 +528,7 @@ async def update_sprint(request: Request, collection_id: int, sid: int): @router.delete("/collections/{collection_id}/sprints/{sid}") -async def delete_sprint(request: Request, collection_id: int, sid: int): +def delete_sprint(request: Request, collection_id: int, sid: int): """Delete a sprint.""" with get_conn() as conn: sprint = conn.execute( @@ -572,7 +572,7 @@ async def assign_page_to_sprint(request: Request, collection_id: int, sid: int): @router.delete("/collections/{collection_id}/sprints/{sid}/assign/{page_id}") -async def remove_page_from_sprint(request: Request, collection_id: int, sid: int, page_id: int): +def remove_page_from_sprint(request: Request, collection_id: int, sid: int, page_id: int): """Remove a page from a sprint.""" with get_conn() as conn: existing = conn.execute( @@ -586,7 +586,7 @@ async def remove_page_from_sprint(request: Request, collection_id: int, sid: int @router.get("/collections/{collection_id}/sprints/burndown/{sid}") -async def sprint_burndown(request: Request, collection_id: int, sid: int): +def sprint_burndown(request: Request, collection_id: int, sid: int): """Calculate burndown data for a sprint.""" with get_conn() as conn: sprint = conn.execute( @@ -660,7 +660,7 @@ async def import_csv(request: Request, collection_id: int): @router.get("/collections/{collection_id}/export/csv") -async def export_csv(request: Request, collection_id: int): +def export_csv(request: Request, collection_id: int): with get_conn() as conn: pages = conn.execute( "SELECT * FROM collection_pages WHERE collection_id=? ORDER BY position", (collection_id,) @@ -690,7 +690,7 @@ async def export_csv(request: Request, collection_id: int): # ── Webhooks Outbound Management ── @router.get("/webhooks") -async def list_webhooks(request: Request): +def list_webhooks(request: Request): _require_admin(request) with get_conn() as conn: rows = conn.execute("SELECT * FROM webhook_subscriptions ORDER BY created_at DESC").fetchall() @@ -723,7 +723,7 @@ async def create_webhook(request: Request): @router.delete("/webhooks/{wh_id}") -async def delete_webhook(request: Request, wh_id: int): +def delete_webhook(request: Request, wh_id: int): _require_admin(request) with get_conn() as conn: conn.execute("DELETE FROM webhook_subscriptions WHERE id=?", (wh_id,)) @@ -734,7 +734,7 @@ async def delete_webhook(request: Request, wh_id: int): # ── Public Sharing ── @router.get("/public/{collection_id}") -async def public_view(request: Request, collection_id: int): +def public_view(request: Request, collection_id: int): """Simple public read-only view — no auth required. A18 : les bases ``restricted``/``private`` (``permission_type``) restent diff --git a/docs/openapi-v2.json b/docs/openapi-v2.json index 78bb870..e704602 100644 --- a/docs/openapi-v2.json +++ b/docs/openapi-v2.json @@ -2,7 +2,7 @@ "openapi": "3.1.0", "info": { "title": "FlowDeck", - "version": "7.7.0" + "version": "7.8.0" }, "paths": { "/auth/register": {