"""FlowDeck — Board : sharing. 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 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.publish import fire_published, fire_unpublished, publish, unpublish from ._common import _sidebar_data logger = logging.getLogger(__name__) router = APIRouter(tags=["board"], prefix="/board") # ═══════════ Favorites API ═══════════ @router.get("/api/favorites") 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 with get_conn() as conn: rows = conn.execute( "SELECT page_id FROM favorites WHERE user_id=? ORDER BY position", (uid,) ).fetchall() return {"favorites": [r["page_id"] for r in rows]} @router.post("/api/favorites/{page_id:int}") def add_favorite(request: Request, page_id: int): """Add a page to favorites.""" 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: existing = conn.execute( "SELECT id FROM favorites WHERE user_id=? AND page_id=?", (uid, page_id) ).fetchone() if not existing: pos = conn.execute( "SELECT COALESCE(MAX(position), -1) + 1 FROM favorites WHERE user_id=?", (uid,) ).fetchone()[0] conn.execute( "INSERT INTO favorites (user_id, page_id, position) VALUES (?,?,?)", (uid, page_id, pos), ) conn.commit() try: run_event_sync(fire_event("favorite.added", {"page_id": page_id, "user_id": uid})) except Exception: logger.exception("add_favorite") return {"status": "added", "page_id": page_id} @router.delete("/api/favorites/{page_id:int}") def remove_favorite(request: Request, page_id: int): """Remove a page from favorites.""" 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: conn.execute("DELETE FROM favorites WHERE user_id=? AND page_id=?", (uid, page_id)) conn.commit() try: run_event_sync(fire_event("favorite.removed", {"page_id": page_id, "user_id": uid})) except Exception: logger.exception("remove_favorite") return {"status": "removed", "page_id": page_id} # ═══════════ Share API ═══════════ # ═══════════ Share API ═══════════ @router.post("/api/share/{page_id:int}") def update_share(request: Request, page_id: int, body: dict = Body(default={})): """Save share settings for a page.""" mode = body.get("mode", "private") published = body.get("published", False) with get_conn() as conn: conn.execute( "UPDATE pages SET share_mode=?, published=? WHERE id=?", (mode, 1 if published else 0, page_id), ) conn.commit() return {"status": "ok", "share_mode": mode, "published": published} @router.post("/api/pages/{page_id:int}/publish") def publish_page(request: Request, page_id: int): """Publish a page to the web (generates publish_slug).""" if not SessionManager.decode_session(request.cookies.get("flowdeck_session", "")): raise HTTPException(401, "Authentication required") slug, title = publish(page_id) run_event_sync(fire_published(page_id, slug)) return {"is_published": True, "publish_slug": slug, "title": title} @router.delete("/api/pages/{page_id:int}/publish") def unpublish_page(request: Request, page_id: int): """Unpublish a page from the web.""" if not SessionManager.decode_session(request.cookies.get("flowdeck_session", "")): raise HTTPException(401, "Authentication required") unpublish(page_id) run_event_sync(fire_unpublished(page_id)) return {"is_published": False} # ═══════════ Trash (must be before /{owner}/{repo} catch-all) ═══════════ # ═══════════ Trash (must be before /{owner}/{repo} catch-all) ═══════════ @router.get("/api/trash") 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] @router.post("/api/trash/{page_id}/restore") def restore_page(request: Request, page_id: int): with get_conn() as conn: conn.execute("UPDATE pages SET deleted_at=NULL WHERE id=?", (page_id,)) conn.commit() try: run_event_sync(fire_event("page.restored", {"page_id": page_id})) except Exception: logger.exception("restore_page") return {"status": "ok", "restored": page_id} @router.delete("/api/trash/{page_id}") 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,)) conn.commit() return {"status": "ok", "deleted": page_id} @router.get("/trash", response_class=HTMLResponse) def trash_page(request: Request): from app.templating import ENV env = ENV template = env.get_template("trash.html") return template.render(**_sidebar_data(request)) # ═══════════════ v5.14.0: Synced Blocks ─═══════════════════ # These routes MUST be registered before the catch-all /{owner}/{repo} below.