"""FlowDeck — Dashboard : workspaces. Découpe A28 de l'ancien app/routers/dashboard.py (2 735 lignes, 63 routes) — un module par concern, contrat inchangé. """ from __future__ import annotations import logging from fastapi import APIRouter, Body, Request from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse from app.auth.session import SessionManager from app.db import get_conn from ._common import WORKSPACE_COOKIE, _get_active_workspace, _get_user_id, _sidebar_data logger = logging.getLogger(__name__) router = APIRouter(tags=["dashboard"]) @router.get("/workspaces", response_class=HTMLResponse) def workspaces_page(request: Request): """Workspaces list page.""" from app.templating import ENV env = ENV sidebar = _sidebar_data(request, [], include_workspace=False) user = SessionManager.decode_session(request.cookies.get("flowdeck_session", "")) if not user: return RedirectResponse("/auth/login?provider=local&expired=1", status_code=302) ctx = {**sidebar, "user": user} # Pass active workspace for breadcrumb nav menu (null on workspaces home) ws = _get_active_workspace(request, user_id=_get_user_id(request)) ctx["nav_workspace_id"] = ws["id"] if ws else 0 template = env.get_template("workspaces.html") return template.render(**ctx) @router.get("/api/workspaces") 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 with get_conn() as conn: rows = conn.execute( "SELECT w.*, (SELECT COUNT(*) FROM pages WHERE workspace_id=w.id AND collection_row_id IS NULL) as page_count " "FROM workspaces w WHERE w.owner_id=? ORDER BY w.created_at DESC", (uid,), ).fetchall() workspaces = [] for r in rows: d = dict(r) workspaces.append(d) active = _get_active_workspace(request, user_id=_get_user_id(request)) return {"workspaces": workspaces, "active_id": active["id"] if active else None} @router.post("/api/workspaces") def create_workspace(request: Request, body: dict = Body(default={})): """Create a new workspace.""" name = body.get("name", "New Workspace").strip() if not name: return {"error": "Name required"} user = SessionManager.decode_session(request.cookies.get("flowdeck_session", "")) uid = user["id"] if user and user.get("id") else 1 with get_conn() as conn: # Ensure user exists (FK constraint) uid_ok = conn.execute("SELECT id FROM users WHERE id=?", (uid,)).fetchone() if not uid_ok: conn.execute( "INSERT OR IGNORE INTO users (id, login, full_name, is_admin) VALUES (?, ?, ?, 1)", (uid, user.get("login", "admin") if user else "admin", user.get("full_name", "Admin") if user else "Admin"), ) cursor = conn.execute( "INSERT INTO workspaces (name, owner_id) VALUES (?, ?)", (name, uid), ) ws_id = cursor.lastrowid # Add owner as member conn.execute( "INSERT OR IGNORE INTO workspace_members (workspace_id, user_id, role) VALUES (?, ?, 'owner')", (ws_id, uid), ) conn.commit() return {"id": ws_id, "name": name} @router.put("/api/workspaces/{ws_id:int}") def rename_workspace(request: Request, ws_id: int, body: dict = Body(default={})): """Rename a workspace.""" name = body.get("name", "").strip() if not name: return {"error": "Name required"} with get_conn() as conn: conn.execute("UPDATE workspaces SET name=? WHERE id=?", (name, ws_id)) conn.commit() return {"status": "ok"} @router.delete("/api/workspaces/{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,)) conn.execute("DELETE FROM workspace_members WHERE workspace_id=?", (ws_id,)) conn.execute("DELETE FROM workspaces WHERE id=?", (ws_id,)) conn.commit() return {"status": "ok"} @router.post("/api/workspaces/{ws_id:int}/select") def select_workspace(request: Request, ws_id: int): """Set the active workspace via cookie.""" response = JSONResponse({"status": "ok", "workspace_id": ws_id}) response.set_cookie(WORKSPACE_COOKIE, str(ws_id), max_age=86400 * 30, httponly=True, path="/") return response # ═══════════ Settings Page ═══════════