"""FlowDeck — Board : page_ops. Découpe A28 de l'ancien app/routers/board.py (2 101 lignes, 53 routes) — un module par concern, contrat inchangé. """ from __future__ import annotations import logging from fastapi import APIRouter, Body, HTTPException, Request from fastapi.responses import HTMLResponse, JSONResponse from app.auth.session import SessionManager from app.db import get_conn from app.services.automations import fire_event, run_event_sync from app.services.permission_manager import PermissionManager from ._common import _sidebar_data logger = logging.getLogger(__name__) router = APIRouter(tags=["board"], prefix="/board") @router.put("/api/pages/{page_id}/move") def move_page(request: Request, page_id: int, body: dict = Body(default={})): """Move a page to another workspace or reorder within tree. Body (JSON): { workspace_id?: int, parent_id?: int, new_order?: int } - workspace_id: move page to a different workspace - parent_id: change parent (0 = root level) - new_order: position among siblings (0 = append) """ new_ws_id = body.get("workspace_id") new_parent_id = body.get("parent_id", 0) new_order = body.get("new_order", 0) with get_conn() as conn: row = conn.execute("SELECT id, workspace, workspace_id FROM pages WHERE id=?", (page_id,)).fetchone() if not row: raise HTTPException(404, "Page not found") if new_ws_id: # Move to a different workspace: get the workspace name ws_row = conn.execute("SELECT name FROM workspaces WHERE id=?", (new_ws_id,)).fetchone() if not ws_row: return JSONResponse({"status": "error", "detail": "Workspace not found"}, status_code=404) conn.execute( "UPDATE pages SET workspace_id=?, workspace=?, parent_id=NULL, updated_at=CURRENT_TIMESTAMP WHERE id=?", (new_ws_id, ws_row["name"], page_id), ) else: # Reorder within same workspace conn.execute( "UPDATE pages SET parent_id=?, updated_at=CURRENT_TIMESTAMP WHERE id=?", (new_parent_id if new_parent_id > 0 else None, page_id), ) conn.execute( "UPDATE pages SET sort_order=?, updated_at=CURRENT_TIMESTAMP WHERE id=?", (new_order, page_id), ) conn.commit() run_event_sync(fire_event("page.moved", {"page_id": page_id, "workspace_id": new_ws_id or 0, "parent_id": new_parent_id})) return {"status": "ok", "id": page_id} @router.delete("/api/pages/{page_id}") def delete_page(request: Request, page_id: int): """Move a page to trash (soft delete).""" user = SessionManager.decode_session(request.cookies.get("flowdeck_session", "")) uid = (user or {}).get("id") if not uid: raise HTTPException(403, "Authentication required") # v6.0.0: granular page permissions — need at least edit access to trash. if not PermissionManager(uid).can_edit_page(page_id): raise HTTPException(403, "You don't have edit access to this page") with get_conn() as conn: row = conn.execute("SELECT id, title FROM pages WHERE id=? AND deleted_at IS NULL", (page_id,)).fetchone() if not row: raise HTTPException(404, "Page not found") import datetime conn.execute("UPDATE pages SET deleted_at=? WHERE id=?", (datetime.datetime.now(datetime.UTC).replace(tzinfo=None).isoformat(), page_id,)) conn.commit() run_event_sync(fire_event("page.deleted", {"page_id": page_id, "title": row["title"] or ""})) from app.services.agent_evenements import emettre, url_page emettre("flowdeck.page.deleted", f"page:{page_id}", row["title"] or "", url=url_page(page_id), actor="user") return {"status": "ok", "deleted": page_id, "title": row["title"]} @router.get("/pages/{page_id}", response_class=HTMLResponse) 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" from app.templating import ENV env = ENV # v6.0.0: granular page permissions — hide restricted pages (404). user_hdr = SessionManager.decode_session(request.cookies.get("flowdeck_session", "")) if user_hdr and user_hdr.get("id") and not PermissionManager(user_hdr["id"]).can_view_page(page_id): return HTMLResponse("